In my last article for Torque, I talked about the advantages of using a container in your WordPress plugin or app. I walked through the process of designing a container from scratch to manage three classes Twitter – Feed, Account, and Client.

This week I’m going to go over very similar concepts, but I will talk more about structuring a plugin around the container. Also, we will use Pimple – an excellent, and fairly standard composer package — for the container instead of rolling our own.

A Look At What We Are Improving

Here is where we left off last time –

class Container {

/**
 * Contains tracked objects
 *
 * @var array
 */
protected $objects;

/**
 * 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 ];
}

}

This works, but adding new items to the container is going to get tedious. I resisted the urge to add abstractions to make it trivial to add a new item. Not only was I trying to illustrate one thing at a time, I knew that Pimple provides everything we need and it’s standard many PHP developers are familiar with.

If you’ve never used Pimple before, it’s a pretty simple dependency injection container. It can solve a lot of the problems for when you need a flexible, testable, central store for your plugins shared class instances and need the flexibility to mix single instances with object factories.

Getting Started With Pimple

Pimple only has one class that we need to worry about – Pimple\Container. We can instantiate this or extend it to create Container. As a quick explanation, let’s create a container and add our single “main” instance of our Client class to it.

<?php
use \Pimple\Container;
$container = new Container();
$container['twitter.client'] = $container->factory(function ($c ) {
    return new Client();
});

Notice that this object implements ArrayAccess. That’s why we’re able to access items in it using array syntax. As a result, we can now access our instance of the Twitter Client like this

$client = $container['twitter.client'];

While ArtrayAccess is neat, I personally prefer to extend Pimple and add get() and set() methods. The get() method just calls offsetGet() and set() just calls offsetSet(). But I like having explicit get() and set() that I can easily override in subclasses without actually touching the ArrayAccess implementation that Pimple provides.

<?php
class Container extends  \Pimple\Container{
/**
 * Get item from container
 *
 * @param string $id
 *
 * @return mixed
 */
public function get( string $id )
{
return $this->offsetGet( $id );
}
/**
 * Set item in container
 *
 * @param string $id
 * @param mixed $value
 */
public function set( string $id, $value )
{
return $this->offsetSet( $id, $value );
}
}

Now with this new subclass of Pimple, I can use ArrayAccess or the new methods to set and access items from the container.

The Container At The Center

In my last article, I looked at creating a function to hold the main instance of the container, and how this was better than using the singleton pattern in the container class to achieve the same goal. Here is how we can use this same approach to make our plugin’s Pimple container globally accessible without forcing the singleton pattern on the Container class:

<?php
/**
 * Get plugin's container
 *
 * @return \Pimple\Container
 */
function pimpleExample() : \Pimple\Container
{
static $container;
if( ! $container ){
$container = new \Pimple\Container();
}
return $container;
}

Now we can use this function to get the “main” instance of the container, but still are able to reuse that container later. In fact, that’s something I’m going to do shortly — add a container to the container.

Containers act to manage the state of objects in an application. This allows us to start conceptualizing each part of our app or plugin as a service that is connected to the other parts through this central container.

For example, one very important API in WordPress is the plugins API — AKA hooks. We don’t have to use an intermediary for the Plugins API. We can just call functions like add_filter() directly. But, by introducing an intermediary we can turn the plugins API into a service.

Treating this key way of communicating with WordPress has several benefits. Using an intermediary adds a level of decoupling from WordPress that makes the code more testable and aids in future refactoring. This decoupling could come in handy if some of the hooks need to be rerouted, possibly to another API, such as the HTTP API if say you are adopting a microservices architecture later on in the application development. Also, a good intermediary should help in tracking should aid in removing hooks, which is always a difficult task.

There are a few good object-oriented abstractions for the WordPress plugins API out there. One I like is netrivet/wp-event-emitter. This library works whether WordPress is being used or not and has handles removing hooks well.

Let’s use adding an instance of the NetRivet\WordPress\EventEmitter class to start discussing how to structure our plugin. The first step in our plugin is to create a class to set up the state of our plugin, starting with registering services into the container –

<?php
namespace calderaLearn\pimpleExample;
use NetRivet\WordPress\EventEmitter;
/**
 * Class Plugin
 *
 * Initializes the plugin
 *
 * @package calderaLearn\pimpleExample\Plugin
 */
class Plugin {
/**
 * Add default services to our Container
 *
 * @param Container $container
 */
public function registerServices( Container $container )
{
$container->set( 'hooks', new EventEmitter() );
}
}

 

Now, our main plugin file just needs to do three things –

  • Include Composer’s autoloader.
  • Provide the function to access the main Container instance.
  • Call the Plugin class to register the services

<?php
/**
 Plugin Name: Pimple Example
 */
use calderaLearn\pimpleExample\Container;
include_once __DIR__ . '/vendor/autoload.php';
/**
 * Create our main instance of Container early
 */
add_action( 'plugins_loaded', function(){
( new \calderaLearn\pimpleExample\Plugin())->registerServices( pimpleExample() );
});
/**
 * Get plugin's container
 *
 * @return Container
 */
function pimpleExample() : Container
{
static $container;
if( ! $container ){
$container = new Container();
}
return $container;
}

Using Services

Again, I find working with objects attached to an array, or in this case an array-like object, is confusing and hard to work out. Technically, we can now add a filter like this –

<?php
$container = pimpleExample();
$container[ 'hooks' ]->filter( 'the_content', function ($content){
$content .= '<p>Hi Shawn</p>';
return $content;
});

I’m not a fan of this for two reasons. The first is that it’s not terribly change resistant. If I were to change what was attached to “hooks” or what key to use, I’d have to chance down a lot of uses. The other reason I don’t like it is phpStorm will not autocomplete the parameters or show me if I’m using the wrong types. Yes, I’m pedantic, but this is a top reason why IDEs are good – they make writing code faster and less error-prone.

For this, I think it makes sense to create a wrapper class that provides access to the EventEmitter object and methods for the two most common uses — adding a filter and adding an action –

<?php
namespace calderaLearn\pimpleExample;
use NetRivet\WordPress\EventEmitterInterface;
/**
 * Class Hooks
 *
 * Handles interactions with Plugins API via plugins API service
 *
 * @package calderaLearn\pimpleExample
 */
class Hooks {
/**
 * Add a hook through plugins API service
 *
 * @param string $hook
 * @param $function_to_add
 * @param int $priority
 * @param int $acceptedArgs
 *
 * @return EventEmitterInterface
 */
public static function addAction( string $hook, $function_to_add, int $priority = 10, int $acceptedArgs = 1 ) : EventEmitterInterface
{
return self::getHookManager()->on( $hook, $function_to_add, $priority, $acceptedArgs );
}
/**
 * Add a filter through plugins API service
 *
 * @param string $hook
 * @param $function_to_add
 * @param int $priority
 * @param int $acceptedArgs
 *
 * @return EventEmitterInterface
 */
public static function addFilter( string $hook, $function_to_add, int $priority = 10, int $acceptedArgs = 1 ) : EventEmitterInterface
{
return self::getHookManager()->filter( $hook, $function_to_add, $priority, $acceptedArgs );
}
/**
 * @return EventEmitterInterface
 */
public static function getHookManager() : EventEmitterInterface
{
return pimpleExample()->get( 'hooks' );
}
}

Then we can refactor how we add a filter, to look like this –

<?php
\calderaLearn\pimpleExample\Hooks::addFilter( 'the_content', function ( $content ){
$content .= '<p>Hi Shawn</p>';
return $content;
});

Factories In The Container

So far we’ve used the container to make a globally accessible “main” instance of an object. In the last article, our container did this, but it also had a way to get objects specific to a username from the container.

By the way, this code is using my refactored Twitter “Account” and “Feed” objects from last week. You can see their source here for Account and here for Feed.

For this requirement, we will need to implement the repository pattern. I covered repositories — objects that store other objects — previously for Torque. No need to roll our own this time, because Pimple is a great implementation of the repository pattern.

We could just attach a new instance of Pimple\Container to our main Container, but I think an object that decorates that container is called for. Using the decorator pattern, which I covered here, will let us make our code more obvious, but also add logic like calling the Account factory inside of the Feed factory.

<?php
namespace calderaLearn\pimpleExample\Twitter;
use calderaLearn\pimpleExample\Container;
/**
 * Class Factory
 * @package calderaLearn\pimpleExample\Twitter
 */
class Factory {
/** @var Container  */
protected $settingsContainer;
/** @var Container  */
protected $mainContainer;
/**
 * Factory constructor.
 *
 * @param Container $settingsContainer
 * @param Container $mainContainer
 */
public function __construct( Container $settingsContainer, Container $mainContainer )
{
$this->settingsContainer = $settingsContainer;
$this->mainContainer = $mainContainer;
}
/**
 * Get Account object from container - ads to container if not present.
 *
 * @param string $username
 *
 * @return Account
 */
public function Account( string $username ) : Account
{
$key = 'account.' . sanitize_key( $username );
if( $this->settingsContainer[ $key ] ){
$this->settingsContainer[ $key ] = new Account(  $username, $settings[ 'token' ], $settings[ 'tokenSecret' ] );
}
return $this->settingsContainer[ $key ];
}
/**
 * Get Feed object from container - ads to container if not present.
 *
 * @param string $username
 *
 * @return Feed
 */
public  function Feed( string  $username ) : Feed
{
$key = 'feed.' . sanitize_key( $username );
if( ! $this->settingsContainer[ $key ] ){
$this->settingsContainer[ $key ] = new Feed(
$this->Account( $username ),
$this->mainContainer[ 'twitter.feed' ]
);
}
return static::getContainer()[ $key ];
}
/**
 * Get accounts container
 *
 * @return Container
 */
public function getContainer() : Container
{
return $this->settingsContainer;
}
}

Notice that this class takes its container, and the main container as dependencies. Originally I just wrote it as a static class, but then it was too strongly tied to the main instance of the container. Also, I suspect I would probably turn this into an abstract class for other similar repositories.

Here is the updated container registration to create this instance in the main container –

<?php
namespace calderaLearn\pimpleExample;
use calderaLearn\pimpleExample\Twitter\Client;
use calderaLearn\pimpleExample\Twitter\Factory;
use NetRivet\WordPress\EventEmitter;
/**
 * Class Plugin
 *
 * Initializes the plugin
 *
 * @package calderaLearn\pimpleExample\Plugin
 */
class Plugin {
/**
 * Set main instance of plugin container with all default services
 *
 * @param Container $container
 */
public function registerServices( Container $container )
{
//Add main example of the client
$container->set( 'twitter.client',
$container->factory(function ($c ) {
return new Client();
})
);
//Add hooks manager
$container->set( 'hooks', new EventEmitter() );
//Add a factory for Twitter accounts
$container->set( 'twitter.accounts', new Factory( new Container(), $container ) );
}
}

Last Thoughts

This article is an introduction to using Pimple as a container in your WordPress plugin or application. I showed the same thing, without Pimple in my last article. Personally, I’m shifting from rolling my own, which was helpful in learning the pattern, to using Pimple, because it’s better and more powerful than my own implementation.

I’ve been thinking a lot about the microservices architecture pattern and the service provider patterns and how they are similar, but on different scales. The former connects applications to form one modular application around a central application. The latter connects the different pieces of a single application around a central part of the application.

These approaches are very different than how we normally use WordPress. WordPress is simple because it doesn’t enforce strong encapsulation. Structuring your plugin or app around a service container or containers helps you make your code more reusable, maintainable and testable.

Josh is a WordPress developer and educator. He is Founder/ Lead Developer/ Space Astronaut Grade 3 for Caldera Labs, makers of awesome WordPress tools including Caldera Forms — a drag and drop, responsive WordPress form builder. He teaches WordPress development at Caldera Learn.

The post Using Pimple as a Service Container in WordPress Development appeared first on Torque.

Share This