Contextual binding in PHP 5.2 with di52

Version 2 of the PHP 5.2 compatible dependency injection container will support contextual binding, what is it?

Previous posts

While working on version 2.0 of DI52, a PHP 5.2 compatible dependency injection container, I’ve taken the time to present some the new ad-hoc callback functions support and the refactored decorator pattern support.
Today I will dive into a new feature that will be introduced in the new version: contextual binding support.

Thanks to Laravel and Pimple

Making DI52 PHP 5.2 compatible was a necessity born out of my everyday working context: WordPress plugins.
While WordPress recommended requirements list PHP 7.0 its minimum requirements still list PHP 5.2; I wanted to use a reliable and fast dependency injection container to get rid, on the legacy code I work on, of widespread use of the Singleton pattern, apply the Dependecy Inversion Principle where possible and, in general, make the code (more) testable.
If I’ve not forked Laravel’s code I’ve openly copied its Service Container API to model DI52 API; along the road I’ve also tried to reproduce part of the API Pimple exposes.
Both are great projects and giants lending me their shoulders in inspirational terms.

Contextual binding

Talking of “openly copying” I’ve tried to model Laravel contextual binding API ending up with the same exact method names for, I guess, the same purpose.
“Binding an implementation” means telling the container what concrete class (or object or closure return value) should be injected in a class constructor requiring (with type hinting) a certain interface.
In the case below the Acme_PostRepository concrete class is “bound” to the Acme_RepositoryI interface.

<?php
/**
 * Plugin Name:     Acme REST Thing
 * Plugin URI:      http://theaveragedev.local
 * Description:     ACME REST Thing
 * Author: Luca Tumedei
 * Author URI: http://theaveragedev.local
 * Version:         0.1.0
 */

include 'vendor/autoload_52.php';

class Acme_Plugin
{
    /**
     * @var tad_DI52_Container
     */
    protected $container;

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

    public function start()
    {
        $this->container->bind('Acme_RepositoryI', 'Acme_PostRepository');

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

add_action('init', [new Acme_Plugin(), 'start']);

The Acme_RepositoryI interface is simple enough:

interface Acme_RepositoryI {

    /**
     * Returns an array of the latest posts.
     * 
     * @param int $n How many posts to return.
     * 
     * @return array
     */
    public function getLatest($n = 10);
}

as is simple the Acme_PostsEndpoint class:

class Acme_PostsEndpoint implements Acme_EndpointI {
    protected $repository;

    public function __construct(Acme_RepositoryI $repository) {
        $this->repository = $repository;
    }

    // from the interface
    public function getArchive(WP_REST_Request $request) {
        return $this->repository->getLatest(10, 'date');
    }
}

The need for contextual binding becomes evident when I add a second endpoint handler for movies and a third one for actors:

<?php
/**
 * Plugin Name:     Acme REST Thing
 * Plugin URI:      http://theaveragedev.local
 * Description:     ACME REST Thing
 * Author: Luca Tumedei
 * Author URI: http://theaveragedev.local
 * Version:         0.1.0
 */

include 'vendor/autoload_52.php';

class Acme_Plugin
{
    /**
     * @var tad_DI52_Container
     */
    protected $container;

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

    public function start()
    {
        $this->container->bind('Acme_RepositoryI', 'Acme_PostRepository');

        $this->container->when('Acme_MoviesEndpoint')
            ->needs('Acme_RepositoryI')
            ->give('Acme_MoviesRepository');

        $this->container->when('Acme_ActorsEndpoint')
            ->needs('Acme_RepositoryI')
            ->give('Acme_ActorsRepository');

        register_rest_route(
            'acme/v1', '/posts/', 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'),
            )
        );
    }
}

add_action('init', [new Acme_Plugin(), 'start']);

The endpoint classes implementations do not vary much and the relevant part of the code is the one outlined by the when, needs, give methods.
I’ve used the same ablative method naming Laravel Service Container uses as it conveys in coding terms what could be said, in plain English, like:

By default when a class needs an implementation of the Acme_RepositoryI interface build and give it an instance of theAcme_PostRepository class. If the requesting class is Acme_MoviesEndpoint give it an instance of the Acme_MoviesRepository class. If the requesting class is Acme_ActorsEndpoint then give it an instance of the Acme_ActorsRepository class.

The power of this construct is that, with little refactoring, the code could be slimmed down even further.
If getting out of PHP 5.2 constraints then the presence of closures would allow for a unique implementation of the Acme_PostRepository class to be used for all:

class Acme_PostRepository implements Acme_RepositoryI {
    protected $postType;
    protected $orderBy;
    protected $order;

    /**
     * @param  string $postType The post type to query
     * @param  string $orderBy   The ordering criteria
     * @param  string $order    The ordering order
     */
    public function __construct($postType, $orderBy, $order) {
        $this->postType = $postType;
        $this->orderBy = $orderBy;
        $this->order = $order;
    }

    public function getLatest($n){
        return get_posts(array(
            'post_type' => $this->postType,
            'orderby' => $this->orderBy,
            'order' => $this->order
        ));
    }
}

and refactoring the plugin code:

<?php
/**
 * Plugin Name:     Acme REST Thing
 * Plugin URI:      http://theaveragedev.local
 * Description:     ACME REST Thing
 * Author: Luca Tumedei
 * Author URI: http://theaveragedev.local
 * Version:         0.1.0
 */

include 'vendor/autoload_52.php';

class Acme_Plugin
{
    /**
     * @var tad_DI52_Container
     */
    protected $container;

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

    public function start()
    {
        $this->container->bind('Acme_RepositoryI', function () {
            return new Acme_PostRepository('post', 'date', 'DESC');
        });

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

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

add_action('init', [new Acme_Plugin(), 'start']);

Sadly closures are not an option in PHP 5.2…

Next

I will show next how DI52 will provide a limited solution to the lack of closures in PHP 5.2 with the new instance method.