In this article, the first in a series, I will introduce the concept of a container and show you how to build and implement a very simple, but limited container. This article illustrates the concept using a singleton, which is not optimal but is simple.
Note, this example code will start with using a singleton. I know this isn’t the best practice but it will make it easy to teach about containers. In the end, I will remove the singleton pattern from the container class as a way of explaining the true problem with singletons.
Getting Started With Containers
Think of a container as the central object that stores or can create instances of objects in your application. The need for containers arises from the tension between needing shared instances of objects while still using dependency injection.
With that, let’s start by looking at the problem we’re trying to avoid. Imagine you’re writing a class to interact with a Twitter feed. This is going to require a way to work with and store a Twitter account, which should be its own class — so that you can have multiple accounts and reuse that class. Also, this will require an API client, which should be its own class, as that is both a separate concern and is independently useful.
So now, we have one class, call it TwitterFeed, that requires two other classes to work. The simplest way to do this would be like this:
<?php class TwitterAccount{ //A class that gives us a Twitter accounts details } class TwitterAPI { //A class for interacting with Twitter API } class TwitterFeed{ /** @var TwitterAccount */ protected $twitterAccount; /** @var TwitterAPI */ protected $twitterAPI; public function __construct(){ $this->twitterAccount = new TwitterAccount(); $this->twitterAPI = new TwitterAPI(); } }
To get this implementation working, we’d have to add logic to the TwitterFeed class to get the right account and connect that to the API client. We just added two new responsibilities to TwitterFeed without actually adding the ability for TwitterFeed to get a TwitterFeed.
An anthropomorphic representation of the single responsibility principle enters the room and looks at us with disappointment. It’s not angry, but it knows we know better and is disappointed.
Here is a new version of the TwitterFeed class, that takes the other two objects as dependencies.
<?php class TwitterFeed{ /** @var TwitterAccount */ protected $twitterAccount; /** @var TwitterAPI */ protected $twitterAPI; public function __construct( TwitterAccount $twitterAccount, TwitterAPI $twitterAPI){ $this->twitterAccount = $twitterAccount; $this->twitterAPI = $twitterAPI; } }
By using dependency injection, we have solved the problem of too many concerns in this object. But this has actually gotten harder to use. It’s probably best to only have one instance of the TwitterAPI class, so we will want one globally accessible instance of that class.
On the other hand, we should want to have multiple accounts. Even if we start our plugin with 1 account only, architecting for the possibility of multiple accounts is a good long-term solution. Still, we don’t want multiple instances of TwitterAccount per account. That’s wasteful in terms of resources and will make the plugin perform unpredictably since we never know which instance we are updating.
Structuring Around A Container
Let’s start by creating a basic plugin container and adding a single instance of the TwitterAPI to it. In both of the scenarios laid out above, this needs to be globally accessible and therefore, implementing the singleton pattern for our container could solve that problem. It’s not ideal, but it’s simple, so let’s do it first to understand the pattern.
Here is the class, which uses the singleton pattern to enforce one instance, and then implements the repository pattern to store other objects in this instance. The first object added is the TwitterAPI via the getTwitterAPI method. Notice that it is lazy-loaded — the instance is only created when needed and only once.
<?php class Container { /** @var Container */ protected static $instance; /** * Contains tracked objects * * @var array */ protected $objects; protected function __construct(){} /** * Get container instance * * @return Container */ public static function getInstance() : Container { if( ! static::$instance ){ static::$instance = new static(); } return static::$instance; } /** * Get Twitter client * * @return TwitterAPI */ public function getTwitterAPI() : TwitterAPI { if( ! isset( $this->objects[ __METHOD__ ] ) ){ $this->objects[ __METHOD__ ] = new TwitterAPI(); } return $this->objects[ __METHOD__ ]; } }
Now we have one globally accessible instance of the TwitterAPI class, without having to enforce the singleton on TwitterAPI. We can now write tests that use mock TwitterAPI classes, or create new instances if needed later.
Factories In The Container
In the last section, we added a method to get one single instance of the TwitterAPI class from the container. Our next step is to allow for multiple instances of the TwitterAccount class to be used, but only one per account.
Before we can implement this in the container, we will need a factory for TwitterAccount objects. That’s a separate concern from the factory.
First, let’s refactor the TwitterAccount object to accept a username as a dependency. This makes each object unique.
<?php class TwitterAccount{ /** @var string */ protected $usersname; public function __construct( string $username ) { $this->usersname = $username; } }
Now we can build a factory to create these objects that get them from the database if possible. In this factory I’m using the Options API in place of a proper database abstraction for simplicity.
class TwitterAccountFactory { /** * Create instances of TwitterAccount, possibly from saved data * * @param string $username * * @return TwitterAccount */ public static function get( string $username ) : TwitterAccount { $saved = get_option( '_prefixAccount' . $username ); if( $saved ){ return $saved; } return new TwitterAccount( $username ); } }
Note that this factory doesn’t implement the repository pattern. It’s tempting to add one, but that’s not this classes job. Leaving this to just creating objects means that we can use the main container as the repository or we can create a separate repository. Let’s do the first option because it’s simpler.
<?php class Container { /** @var Container */ protected static $instance; /** * Contains tracked objects * * @var array */ protected $objects; protected function __construct(){} /** * Get container instance * * @return Container */ public static function getInstance() : Container { if( ! static::$instance ){ static::$instance = new static(); } return static::$instance; } /** * Get Twitter client * * @return TwitterAPI */ public function getTwitterAPI() : TwitterAPI { if( ! isset( $this->objects[ __METHOD__ ] ) ){ $this->objects[ __METHOD__ ] = new TwitterAPI(); } return $this->objects[ __METHOD__ ]; } /** * Get a TwitterAccount by user name * * @param string $username * * @return TwitterAccount */ public function getTwitterAccount( string $username ) : TwitterAccount { $arrayKey = md5( __METHOD__ . $username ); if( ! isset( $this->objects[ $arrayKey ] ) ){ $this->objects[ $arrayKey ] = TwitterAccountFactory::get( $username ); } return $this->objects[ $arrayKey ]; } }
The new method in the container getTwitterAccount() first checks if an object for this account is in the container’s repository of objects. If not, it uses the TwitterAccountFactory class to create a new one and add it to the repository.
Bringing It All Together
Now that our container can provide TwitterAccount objects and the “main instance” of the TwitterAPI class, we can create a factory for TwitterAccounts that brings together that uses our container to provide dependencies to the TwitterFeed class.
<?php class TwitterFeedFactory { /** * Get instance of TwitterFeed class * * @param string $username * * @return TwitterFeed */ public static function get( string $username ) : TwitterFeed { return new TwitterFeed( Container::getInstance()->getTwitterAccount( $username ), Container::getInstance()->getTwitterAPI() ); } }
Now we have an easy way to create TwitterFeed objects by username without having to build the API client, or the logic of accounts or the database interactions for account detail storage into TwitterFeed.
Mission partially accomplished.
Why This Is Sub-Optimal
By avoiding using singletons in TwitterAccount and TwitterAPI they are easier to reuse and to write unit tests for. But, the whole plugin is now strongly tied to this one instance of the Container. A major drawback of singletons is you can’t rest their state, which is something you have to do between each test. Also, we can only have one container per plugin with this and can’t reuse the class in other plugins properly.
So, let’s refactor the Container to not rely on the singleton, but keep it globally accessible.
That’s easy, we just remove the instance property and getInstance method and make the constructor public. We might also want to use a more generic repository for our container, like the one I showed in my repository article, or use a prebuilt container like Pimple. I’ll show the latter option in my next article.
For now, let’s build it from scratch. Here is the container, without the singleton pattern.
Now, we can create a new instance of this container for each unit test, create multiple containers. But, with the singleton pattern gone, we have no way of getting the instance of this container, which is a problem. So let’s create a function to track that instance.
<?php /** * Get main instance of Container * * @return Container */ function container() : Container { static $container; if( ! $container ){ $container = new Container; } return $container; }
This new function is basically a singleton. If the instance is stored in the static variable that variable is returned. If not, a new instance is created and saved for later.
Why is this better than a class that implements the singleton? A class that implements the singleton pattern has at least two responsibilities — managing its state and doing its actual function. Our original container managed its own state and provided a container for the state of other objects.
Its goal was to allow other classes to follow the single responsibility principle but didn’t get that same benefit for itself. Now our container class’ single responsibility is to manage state of shared instances. The container function’s single responsibility is to manage the state of the container class.
Conclusion
In this article, I’ve shown you how to create a container for your WordPress plugin. Containers can be a repository of objects like I’ve discussed here. They can also be used to get and set plugins settings.
What’s really great about using a container is you can start to treat the classes that perform your plugin’s business logic as independent and isolated services that interact through your container. Working with the Laravel framework has shown me the value of approaching different parts of code this way.
In the next article in this series, I will show how to use the popular PHP container library Pimple as your container and start attaching services to it.
No Comments