di52 and the new callback function support

Callback creation is coming to di52 next version: why?

Brief introduction

The DI52 is a PHP 5.2 compatible dependency injection container that exposes an API similar to Laravel Service container.
Being a WordPress plugin developer I find myself constrained, at times, by PHP 5.2 limits and wanted to try and overcome those.
While I do believe limits exist I also do believe many PHP 5.2 limits are more perceived than real; like “you cannot use a DI container in PHP 5.2” or “you cannot use this pattern because PHP 5.2”.
This post follows a previous one introducing some features of DI52 version 2.0 and tries to shed some light over a problem and di52 solution to it.
The below code is WordPress-oriented and PHP 5.2 compatible yet understandable enough to apply to any PHP version.

The code so far

The core of this sudo-application I’ve used in the previous article is a WP REST API endpoint handler in charge of returning the top 3 most commented posts in the database.
Any endpoint handler has to conform to the simple interface below:

interface Acme_EndpointI {

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

The basic implementation of the endpoint does the bare minimum to get the job done:

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);
    }
}

Tapping into the power of the Decorator pattern I’ll add and remove functionalities from the endpoint as required; such functionalities might be caching tapping into WordPress wp_cache_ functions:

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', HOUR_IN_SECONDS);
        }

        return $data;
    }
}

Logging the requests:

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;
    }
}

And finally tapping into heavier caching artillery:

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, HOUR_IN_SECONDS);
        }

        return $data;
    }
}

The pieces are finally glued together in the plugin main class (reworked from previous version to avoid using globals):

/*
Plugin Name: Acme Plugin
Description: Yet another Acme plugin
Plugin URI: http://theaveragedev.com
Author: Luca Tumedei
Author URI: http://theaveragedev.com
Version: 1.0
License: GPL2
*/

// the PHP 5.2 compatible autoload file, generated using xrstf/composer-php52
require_once __DIR__ . '/vendor/autoload_52.php';

class Acme_Plugin{

    protected $container;

    public function bind() {
        $this->container = new tad_DI52_Container();

        // bind the implementations, this is done only once!
        $this->container->singleton('Acme_LoggerI', 'AcmeLogger');

        // build a connection to the server with default values
        $this->container->singleton('Amce_RedisServerI', array('Acme_RedisServer','build'));

        // while the Redis server should be one, the client should be new each time
        // to handle concurrent requests
        $this->container->bind('Acme_RedisI', 'Acme_Redis');

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

        return $this;
    }

    public function hook(){
        add_action('rest_api_init', array( $this, 'register_endpoints'));

        return $this;
    }

    public function register_endpoints() {
        register_rest_route('acme/v1', '/top/comments', array(
            'methods'  => 'GET',
            'callback' => $this->container->callback('top-endpoint', 'get')
        ));

        return $this;
    }
}

$acme =  Acme_Plugin();
$acme->bind()->hook();

Callbacks

The code above shows a $container->callback call being used in the register_rest_route function to register the method, “callback”, that should be called to handle GET requests on the /top/comments endpoint.
This feature is best understood taking a step back and looking at how the code would would be without relying on the container completely or just in part; the Acme_Plugin::register_endpoints method would read like:

public function register_endpoints() {
    // build it
    $handler = $this->container->make('top-endpoint');

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

    return $this;
}

This is pretty standard code that tells WordPress:

IF you have to handle a GET request on the /top/comments route then call get on the instance of Acme_Endpoint I passed; $top in this case.

The problem lies in the fact that the $top object is fully built, and with it all its dependencies, and maybe never used. In the example above multiple endpoints might be registered in the method and at most one will be used.
There are solutions to this problem each one aiming at not building an object that might end up unused.
The first one might be to bake a lazy instantiation method in the used class itself (or in the many used classes themselves):

class Acme_LazyTopEndpoint implements Acme_EndpointI {
    private static $instance;

    public static function lazyGet(WP_REST_Request $request){
        if(empty(self::$instance)){
            self::$instance = new self();
        }

        return self::$instance->get($request);
    }

    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);
    }
}

This works as long as one does not want to decorate (and will then end up violating the Single Responsibility Principle and Open-Closed principle while trying to make the class log, cache, handle the request and whatever) or does not need to replicate this across multiple implementations; PHP 5.2 lack of late static binding and __callStatic support makes things even worse (possibly copy-and-paste worse).
Personally what I also do not like is the class becomes aware of how it’s used exposing a method just for the sake of the current implementation; it’s the same problem I have with the Singleton pattern.
Another solution is a factory class relying on the container:

class Acme_Factory {

    protected $container; 

    public function __construct(tad_DI52_ContainerInterface $container) {
        $this->container = $container;
    }

    public function callback($class, $method) {
        return array($this, $class . '__' . $method);
    }

    public function __call($name, array $arguments){
        if(false == strpos($name, '__')){
            throw new RuntimeException($name . ' is not something factory can handle.');
        }

        list($class, $method) = explode('__', $name);

        $instance = $this->container->make($class);

        return call_user_func_array(array($instance, $method), $args);
    }
}

This works with a little modification to the using code:

// in the `Acme_Plugin` class

public function bind() {
    $this->container = new tad_DI52_Container();

    // bind the implementations... as before

    $this->container->singleton('factory', new Acme_Factory($this->container));

    // this time the decorator chain is bound to the `topEndpoint` alias
    $topChain = array('Acme_RedisCachingEndpoint', 'Acme_LoggingEndpoint', 'Acme_TopEndpoint');
    $this->container->singletonDecorators('topEndpoint', $topChain);

    return $this;
}

public function register_endpoints() {
    $factory = $this->container->make('factory')

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

    return $this;
}

Since the Acme_Factory class will need to return a legit PHP method name the alias to which the chain of Acme_TopEndpoint decorators is bound is changed to topEndpoint; this will mean the return value of $factory->callback('topEndpoint', 'get') call will return array($factory, 'topEndpoint__get').

The factory solution allows used classes not to be modified for a lazy build and avoids building objects that could never be used or having to rely on static methods.
The final step is to rely on one of the new features of DI52 version 2.0: a ready to use factory in the form of the callback method.
Skipping the implementation of the Acme_Factory class completely the Acme_Plugin::register_endpoints method can now be rewritten as seen above:

public function register_endpoints() {
    register_rest_route('acme/v1', '/top/comments', array(
        'methods'  => 'GET',
        'callback' => $this->container->callback('top-endpoint', 'get')
    ));

    return $this;
}

In PHP 5.3+ terms this is like using a closure to build the object just in time:

$container = $this->container;

$callback = function(WP_REST_Request $request) use($container){
   return $container->make('top-endpoint')->get($request);
}

public function register_endpoints() {
    register_rest_route('acme/v1', '/top/comments', array(
        'methods'  => 'GET',
        'callback' => $callback
    ));

    return $this;
}

Which can be another form of drag by itself when done for each object that’s to be lazily instantiated.

Next

One other relevant and upcoming feature is the possibility of contextually binding implementations and I will detail some use cases of that in a next post.

I appreciate your input