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 isAcme_MoviesEndpoint
give it an instance of theAcme_MoviesRepository
class. If the requesting class isAcme_ActorsEndpoint
then give it an instance of theAcme_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.