di52 and its Decorator support pattern

Getting to know the why and how of decorator support in the PHP 5.2 compatible Dependency Injection container.

Due to release soon

I’m taking my time to refactor and rework DI52 to avoid breaking what’s already there and add new features; all the while I’m keeping tabs on its speed and efficiency using what I’ve learnt along the way to improve the code.
While I do want to get back to a series of posts focused on WordPress test-driven development di52 proved to be such a base for any recent project that I want to have that in order and working before devoting myself to other development projects.
It won’t be long and I want to use the time to show some features of the PHP 5.2 compatible dependency injection container.

Decorating

The decorator is a pattern meant to:

[allow a] behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.

As any pattern definition it’s foggy until some code shows up; an example then.
I might want to implement a custom WordPress REST API endpoint providing the top 3 most commented posts on my blog; the first piece would be to register the custom endpoint and the handler class:

<?php

// hooking the handler to the custom endpoint
add_action( 'rest_api_init', 'acme_register_endpoints' );

function acme_register_endpoints() {
    $top = new Acme_TopEndpoint();

    register_rest_route( 'acme/v1', '/top/comments', array(
        'methods'  => 'GET',
        'callback' => array( $top, 'get' )
    ) );
}

The first implementation of the Acme_TopEndpoint class would be rather simple returning an associative array of post IDs and titles:

interface Acme_EndpointI {

    /**
     * Handles a GET request on an endpoint.
     *
     * @param WP_REST_Request $request
     */
    public function get( WP_REST_Request $request );
}

class Acme_TopEndpoint implements Acme_EndpointI {

    public function get( WP_REST_Request $request ) {
        $mostCommented = get_posts( array(
            'orderby'        => 'comment_count',
            'order'          => 'DESC',
            'posts_per_page' => 3,
        ) );

        if ( ! empty( $mostCommented ) ) {
            $ids = wp_list_pluck( $mostCommented, 'ID' );
            $titles = wp_list_pluck( $mostCommented, 'post_title' );
            $mostCommented = array_combine( $ids, $titles );
        }

        return new WP_REST_Response( $mostCommented );
    }
}

Further down the development road the endpoint gets so much traffic that some caching is in order.
Keeping the “Open/Closed” principle in mind in place of running off and modifying the class code I will extend the class functionalities to cache the requests.
Taking some time to reflect about the probable future developments I foresee some logging and a possible change in the class caching mechanism and it takes little to know that extending the class, using PHP extends construct, would lead to an unwanted funneling.
If I create an Acme__CachingTopEndpoint extending Acme_TopEndpoint how would I call the next class in charge of adding logging too? Acme__LoggingCachingTopEndpoint?
And what if I want to swap some wp_cache_ based caching solution for a Redis based one keeping the logging? I would need to move the code in charge of logging from Acme__LoggingCachingTopEndpoint to Acme__LoggingRedisCachingTopEndpoint.
Things will get ridiculous quickly.
Now that I’ve emerged from this nightmare of extension and locking I remember decorating is a thing:

class Acme_CachingEndpoint implements Acme_EndpointI {
    private $decorated;

    public function __construct( Acme_EndpointI $decorated ) {
        $this->decorated = $decorated;
    }

    public function get( WP_REST_Request $request ) {
        $key = get_class( $this->decorated );

        $data = wp_cache_get( $key, 'get', false, $found );

        if ( false === $found ) {
            $data = $this->decorated->get( $request );
            wp_cache_set( $key, $data, 'get' );
        }

        return $data;
    }
}

Adding the logging on top of is now easy and flexible:

class Acme_LoggingEndpoint implements Acme_EndpointI {
    private $decorated;
    private $logger;

    public function __construct( Acme_EndpointI $decorated, Acme_LoggerI $logger ) {
        $this->decorated = $decorated;
        $this->logger = $logger;
    }

    public function get( WP_REST_Request $request ) {
        $data = $this->decorated->get( $request );
        $this->logger->log( '/top/comments => ' json_encode( $data ));

        return $data;
    }
}

The two decorating classes will now be re-usable on any endpoint implementing the Acme_EndpointI interface thinning down the code base and the coupling.
Furthermore the decoration does not require the __construct methods to extend each other avoiding violations of Liskov’s Substitution principle.
To finish the caching and logging work the last piece is to modify the acme_register_enpoints function:

<?php

// hooking the handler to the custom endpoint
add_action( 'rest_api_init', 'acme_register_endpoints' );

function acme_register_endpoints() {
    $logger = new Acme_Logger();
    $top = new Acme_LoggingEndpoint( new Acme_CachingEndpoint( new Acme_TopEndpoint ), $logger );

    register_rest_route( 'acme/v1', '/top/comments', array(
        'methods'  => 'GET',
        'callback' => array( $top, 'get' )
    ) );
}

Swapping stuff

Later in the development cycle I’m adding a Redis-based caching solution and want the endpoint to use that in place of a wp_cache_ based solution.
Having put in place a decorating system changing the endpoint to use that is easy:

class Acme_RedisCachingEndpoint implements Acme_EndpointI {
    private $decorated;
    private $redis;

    public function __construct( Acme_EndpointI $decorated, Acme_RedisI $redis ) {
        $this->decorated = $decorated;
        $this->redis = $redis;
    }

    public function get( WP_REST_Request $request ) {
        $key = __CLASS__ . 'top-get';

        $data = $this->redis->fetch( $key, array() );

        if ( $data === false ) {
            $data = $this->decorated->get( $request );
            $this->redis->store( $key, $data );
        }

        return $data;
    }
}

and again it’s just a question of swapping out the constructors chain to use the new caching solution:

<?php

// hooking the handler to the custom endpoint
add_action( 'rest_api_init', 'acme_register_endpoints' );

function acme_register_endpoints() {
    $logger = new Acme_Logger();
    $redisServer = Acme_RedisServer::build();
    $redis = new Acme_Redis($redisServer);
    $top = new Acme_LoggingEndpoint( new Acme_RedisCachingEndpoint( new Acme_TopEndpoint, $redis ), $logger );

    register_rest_route( 'acme/v1', '/top/comments', array(
        'methods'  => 'GET',
        'callback' => array( $top, 'get' )
    ) );
}

Adding the dependency injection container

The latter code presents a performance problem: the logger, the Redis handling class and the endpoint all are built immediately and that’s fine if the endpoint receives any request.
If the endpoint does not receive any request then the time and resources spent building the $top object are wasted.
Using a dependency injection container allows for “lazy” instantiation on one side and “magic” dependency resolution on the other; the example below used DI52 syntax and API but that’s the case with almost any other:

<?php

// hooking the handler to the custom endpoint
add_action( 'rest_api_init', 'acme_register_endpoints' );

global $container;

$container = new tad_DI52_Container();

// bind the implementations, this is done only once!
$container->singleton('Acme_LoggerI', 'AcmeLogger');
$container->singleton('Acme_RedisServerI', array('Acme_RedisServer','build'));
$container->bind('Acme_RedisI', 'Acme_Redis');

function acme_register_endpoints() {
    global $container;

    // the "base" class must be the last
    $topChain = array( 'Acme_RedisCachingEndpoint', 'Acme_LoggingEndpoint', 'Acme_TopEndpoint');
    $container->singletonDecorators( 'top-endpoint', $topChain );

    register_rest_route( 'acme/v1', '/top/comments', array(
        'methods'  => 'GET',
        'callback' => $container->callback( 'top-endpoint', 'get')
    ) );
}

To avoid relying on the use of closures to build “chains” of decorating classes, a possibility out of reach on PHP 5.2, DI52 offers the bindDecorators and singletonDecorators method to bind and lazily resolve a chain of decorating classes into a usable object.
The $container->callback() method is one of the container new features that I will detail in a next post.

Next

The example code above lends itself to more examples to explain some benefits of a DI container and of DI52 in a PHP 5.2 context in particular.