Lazy built instances in di52

Lazy instance creation support is coming to di52 in the next version.

Follow up

I’ve published a series of posts detailing features, both upcoming and existing, of DI52, the PHP 5.2 compatible dependency injection container.
Those include support for decorated object bindings, contextual binding and support for dynamic callback functions.
This will be the last of such posts to show the why and how of another upcoming feature.

Why callback support again?

The latter functionality (dynamic callback functions support) tries to get around PHP 5.2 lack of closures. In the context of WordPress actions and filters callback registration it’s easy to see where the benefit could lie:

$container = new tad_DI52_Container();

$container->bind('OneI', 'ClassOne');

add_action('some_action', array($container->make('OneI'), 'onSomeAction');

In the last line the simple act of setting a callback on the some_action action hook forces the container to build the OneI implementation, an instance of the ClassOne class, immediately and return it; the instance is then registered as a callback together with its onSomeAction method.
Two ways I do not like to solve this “eager” instantiation problem are:

  • using a static method with the downside of having then to hard-code concrete implementations in each callback registration and writing static methods in the class in a way that will end, sooner or later, in Singleton implementations or similar (untestable) code:
    add_action('some_action', array('ClassOne', 'onSomeAction'));
    
  • using a function which means renouncing the benefits of Object-oriented development completely and hardcoding everything:
    add_action('some_action', 'acme_onSomeAction');
    

I personally do not like the two solutions above, and the others based on proxy pattern, factories and the like for a personal preference of style, readability, testability and coupling; classes should not be aware of how, when and by whom they are used.

Closures would lessen the burden a little:

$container = new tad_DI52_Container();

$container->bind('OneI', 'ClassOne');

$onSomeAction = function() use($container){
    $instance = $container->make('OneI');
    return $instance->onSomeAction(func_get_args());
};

add_action('some_action', $onSomeAction);

And once the drill is clear a utility function to generate callback closures can be put in place:

function acme_getCallback($alias, $method, $container) {
    return function() use($alias, $method, $container){
        $instance = $container->make($alias);
        return call_user_func_array(array($instance, $method), func_get_args());
    }
}

$container = new tad_DI52_Container();

$container->bind('OneI', 'ClassOne');

add_action('some_action', acme_getCallback('OneI', 'onSomeAction', $container));

This is the code I started from until I realized it would have been neat to have it on the container and with PHP 5.2 support too.
Here is why DI52 implements the aptly named callback method:

$container = new tad_DI52_Container();

$container->bind('OneI', 'ClassOne');

add_action('some_action', $container->callback('OneI', 'onSomeAction'));

Once my obsession to avoid eager instantiation (“let’s build it as we might need it”) in favour of lazy instantiation (“let’s build it when and if we need it”) is understood it’s easy to introduce the instance method.

Lazy building things

Sometimes there is no need for a callback (an object and a method called on it) as much as there is need for the object instance itself.
This is especially true when using contextual binding to build the same class with different parameters like in this example:

// default implementation of the `Acme_RepositoryI` interface
$this->container->bind('Acme_RepositoryI', function () {
    return new Acme_PostRepository('post', 'date', 'DESC');
});

// exceptions to the default rule
$this->container->when('Acme_MoviesEndpoint')
    ->needs('Acme_RepositoryI')
    ->give(
        function () {
            return new Acme_PostRepository('movie', 'date', 'DESC');
        }
);

$this->container->when('Acme_ActorsEndpoint')
    ->needs('Acme_RepositoryI')
    ->give(
        function () {
            return new Acme_PostRepository('actor', 'comment_count', 'DESC');
        }
);

// use the bindings using the `callback` method to avoid building them immediately
register_rest_route(
    'acme/v1', '/post/', array(
        'methods'  => 'GET',
        'callback' => $this->container->callback('Acme_PostsEndpoint', 'getArchive'),
    )
);

register_rest_route(
    'acme/v1', '/movies/', array(
        'methods'  => 'GET',
        'callback' => $this->container->callback('Acme_MoviesEndpoint', 'getArchive'),
    )
);

register_rest_route(
    'acme/v1', '/actors/', array(
        'methods'  => 'GET',
        'callback' => $this->container->callback('Acme_ActorsEndpoint', 'getArchive'),
    )
);

The Acme_PostRepository class is built in 3 different ways to satisfy the needs of 3 different clients and the code above is perfectly fine in PHP 5.3+ where closures are available.
In PHP 5.2 the code above is replicable putting in place a factory class:

class Acme_DeferredInstances {
    protected $args = array();

    public function instance($class array $arguments = array()) {
        $argsSlug = !empty($args) ? md5(serialize($arguments)) : 'noArgs';
        $slug = str_replace('\\', '_', $class) . '__' . $argsSlug;

        if(!empty($arguments)) {
            $this->args[$argsSlug] = $arguments;
        }

        return array($this, $slug);
    }

    public function __call($name, array $args = array()) {
        if(false === strpos($name, '__') ) {
            throw new RuntimeException('Instance method should be in the <class>__<argsSlug> format');
        }

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

        $args = isset($this->args[$argsSlug]) ? $this->args[$argsSlug] : array();

        $ref = new ReflectionClass($class);

        return $ref->getConstructor() ? $ref->newInstanceArgs(array_map(array($this, 'getArg'), $args)) : $ref->newInstance();
    }
}

This first implementation would allow to rewrite the code above like this:


$deferred = new Acme_DeferredInstances(); // default implementation of the `Acme_RepositoryI` interface $this->container->bind('Acme_RepositoryI', $deferred->instance('Acme_PostRepository', array('post', 'date', 'DESC'))) // exceptions to the default rule $this->container->when('Acme_MoviesEndpoint') ->needs('Acme_RepositoryI') ->give($deferred->instance('Acme_PostRepository', array('movie', 'date', 'DESC'))); $this->container->when('Acme_ActorsEndpoint') ->needs('Acme_RepositoryI') ->give($deferred->instance('Acme_PostRepository', array('actor', 'comment_count', 'DESC')));

Which slims down the code quite a bit.

Taking bindings into account

The problem with the solution above is that information I’ve stored in the container in the form of bindings is lost completely while building the object.
Say that an object type hints an interface in its __construct method:

class Acme_CachingPostRepository {
    public function __construct($postType, $orderBy, $order, Acme_CacheI $cache){
        //...
    }
}

and I want to lazily instance it:

$this->container->when('Acme_ActorsEndpoint')
    ->needs('Acme_RepositoryI')
    ->give($deferred->instance('Acme_CachingPostRepository', array('actor', 'comment_count', 'DESC', 'Acme_Cache')));

the factory would throw while trying to build the object passing it the Acme_Cache string in place of an instance of the Acme_Cache object. I could solve the problem like this:

$cache = $container->make('Acme_Cache');

$this->container->when('Acme_ActorsEndpoint')
    ->needs('Acme_RepositoryI')
    ->give($deferred->instance('Acme_CachingPostRepository', array('actor', 'comment_count', 'DESC', $cache)));

But I would be hardcoding the dependencies in each instance call and building all the dependencies immediately.
Updating the factory to use the container itself to resolve dependencies would solve the problem:

class Acme_DeferredInstances {
    protected $container;
    protected $args = array();

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

    public function instance($class array $arguments = array()) {
        $argsSlug = !empty($args) ? md5(serialize($arguments)) : 'noArgs';
        $slug = str_replace('\\', '_', $class) . '__' . $argsSlug;

        if(!empty($arguments)) {
            $this->args[$argsSlug] = $arguments;
        }

        return array($this, $slug);
    }

    protected function getArg($arg) {
        try {
            return $this->container->make($arg);
        } catch (RuntimeException $e) {
            return $arg;
        }
    }

    public function __call($name, array $args = array()) {
        if(false === strpos($name, '__') ) {
            throw new RuntimeException("Instance method should be in the <class>__<argsSlug> format");
        }

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

        $args = isset($this->args[$argsSlug]) ? $this->args[$argsSlug] : array();

        $ref = new ReflectionClass($class);

        return $ref->getConstructor() ? $ref->newInstanceArgs(array_map(array($this, 'getArg'), $args)) : $ref->newInstance();
    }
}

The code can be finally rewritten like this:

$cache = $container->bind('Acme_CacheI', 'Acme_Cache');

$this->container->when('Acme_ActorsEndpoint')
    ->needs('Acme_RepositoryI')
    ->give($deferred->instance('Acme_CachingPostRepository', array('actor', 'comment_count', 'DESC', 'Acme_CacheI')));

The convenience of having it in the container

The needs and code above pushed me to bake all of the above, “smart resolution” by the container too, in the instance method.
The Acme_DeferredInstances class can be completely avoided in favour of it.

$cache = $container->bind('Acme_CacheI', 'Acme_Cache');

$this->container->when('Acme_ActorsEndpoint')
    ->needs('Acme_RepositoryI')
    ->give($container->instance('Acme_CachingPostRepository', array('actor', 'comment_count', 'DESC', 'Acme_CacheI')));

Next

Time to release and benchmark the container.

I appreciate your input