Revisiting the PHP 5.2 compatible dependency injection container to add a missing feature.
Some miles under the belt
I’ve been using DI52 for some time now in WordPress plugins, themes and full site projects.
The use of a dependency injection(DI) container allows me to write object-oriented code tapping into a technique that has been a steady tool of PHP developers for quite some time now.
When working on PHP 5.3 and above compatible code the options for a DI container are abundant and I’ve personally used pimple and Laravel service container with profit.
The lack of a PHP 5.2
compatible solution drove me to develop DI52 trying to emulate the Laravel Service Container in its API.
Some missing features
The spirit behind DI52 original implementation was more “can I do it?” then anything else and while I’ve refactored the code over and over for performance and efficiency purposes I’ve missed Laravel container contextual binding greatly.
I’ve found myself, while developing a WordPress-based site routing component, writing code like this:
<?php
namespace AT\ServiceProviders;
use AT\Actions\View404;
use AT\Actions\ViewSearchResults;
use AT\Interfaces\RegistrarInterface;
class Routes extends \tad_DI52_ServiceProvider
{
/**
* Binds and sets up implementations.
*/
public function register()
{
// needed to implement the service locator pattern
$container = $this->container;
$container->singleton(\tad_DI52_Container::class, $container);
$container->singleton('AT\Interfaces\PostRepositoryInterface', 'AT\Domain\Repositories\Posts');
$container->singleton('AT\Interfaces\PostArchiveContextInterface', 'AT\Contexts\PostArchive');
$container->singleton('AT\Interfaces\TaxonomyArchiveContextInterface', 'AT\Contexts\TaxonomyArchive');
$container->singleton('AT\Interfaces\PostSingleContextInterface', 'AT\Contexts\PostSingle');
$container->singleton('AT\Interfaces\SeriesSingleContextInterface', 'AT\Contexts\SeriesSingle');
$container->singleton('AT\Interfaces\PageContextInterface', 'AT\Contexts\Page');
$container->tag([
'AT\ActionGroups\Period',
'AT\ActionGroups\Person',
'AT\ActionGroups\Theme',
'AT\ActionGroups\Venue',
'AT\ActionGroups\Series',
'AT\ActionGroups\Pages',
// this one last as it includes the '/' route
'AT\ActionGroups\Posts',
], 'actionGroups');
/** @var RegistrarInterface $actionGroup */
foreach ($container->tagged('actionGroups') as $actionGroup) {
$actionGroup->register();
}
// search
respond('GET', '/search/[:search]/?', function (\_Request $request, \_Response $response) use ($container) {
$action = $container->make(ViewSearchResults::class);
$action->respond($request, $response);
});
// finally...
respond('GET', '404', function (\_Request $request, \_Response $response) use ($container) {
$action = $container->make(View404::class);
$action->respond($request, $response);
});
add_filter('do_parse_request', [$this, 'parseRequest'], 1);
}
public function parseRequest()
{
return dispatch_or_continue();
}
}
I’m using klein as router component hooked on WordPress do_parse_request
filter to skip WordPress routing mechanism completely.
Without contextual binding
The part of the code that really leaves me not satisfied is this:
$container->singleton('AT\Interfaces\PostArchiveContextInterface', 'AT\Contexts\PostArchive');
$container->singleton('AT\Interfaces\TaxonomyArchiveContextInterface', 'AT\Contexts\TaxonomyArchive');
$container->singleton('AT\Interfaces\PostSingleContextInterface', 'AT\Contexts\PostSingle');
$container->singleton('AT\Interfaces\SeriesSingleContextInterface', 'AT\Contexts\SeriesSingle');
$container->singleton('AT\Interfaces\PageContextInterface', 'AT\Contexts\Page');
What I’m doing is binding different WhateverContextInterface
implementations to provide each class depending on a context with the right type of context; as an example the ViewPostsArchive
class depends on an implementation of the PostArchiveContextInterface
interface:
<?php
namespace AT\Actions;
use AT\Interfaces\ActionInterface;
use AT\Interfaces\PostArchiveContextInterface;
use AT\Interfaces\PostRepositoryInterface;
use AT\Interfaces\ResponderInterface;
use AT\Interfaces\RoutesMapInterface;
class ViewPostsArchive extends PostAction implements ActionInterface
{ public function __construct(
ResponderInterface $responder,
PostRepositoryInterface $repository,
PostArchiveContextInterface $context,
RoutesMapInterface $routesMap
) {
parent::__construct($responder, $repository);
$this->context = $context;
$this->routesMap = $routesMap;
}
}
but it really should depend, as any other class depending on a context, from a common ContextInterface
that’s being implemented in different ways:
<?php
namespace AT\Actions;
use AT\Interfaces\ActionInterface;
use AT\Interfaces\ContextInterface;
use AT\Interfaces\PostRepositoryInterface;
use AT\Interfaces\ResponderInterface;
use AT\Interfaces\RoutesMapInterface;
class ViewPostsArchive extends PostAction implements ActionInterface
{ public function __construct(
ResponderInterface $responder,
PostRepositoryInterface $repository,
ContextInterface $context,
RoutesMapInterface $routesMap
) {
parent::__construct($responder, $repository);
$this->context = $context;
$this->routesMap = $routesMap;
}
}
and the dependency injection container should support that requirement.
A solution
I’ve never hidden my attempt at emulating Laravel service container when developing DI52 and will borrow another good idea from it.
Contextual binding can be done, in the context of a Laravel application, like this (see original documentation):
use Illuminate\Support\Facades\Storage;
use App\Http\Controllers\PhotoController;
use App\Http\Controllers\VideoController;
use Illuminate\Contracts\Filesystem\Filesystem;
$this->app->when(PhotoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('local');
});
$this->app->when(VideoController::class)
->needs(Filesystem::class)
->give(function () {
return Storage::disk('s3');
});
I do not like the broken down chain API that much and will try to trim it down to two methods like this:
$container->whenRequestedBy(ViewPostsArchive::class)
->singleton(ContextInterface::class, PostArchive::class);
$container->whenRequestedBy(ViewTaxonomyArchive::class)
->singleton(ContextInterface::class, TaxonomyArchive::class);
$container->whenRequestedBy(ViewPostSingle::class)
->singleton(ContextInterface::class, PostSingle::class);
$container->whenRequestedBy(ViewSeriesSingle::class)
->singleton(ContextInterface::class, SeriesSingle::class);
$container->whenRequestedBy(ViewPage::class)
->singleton(ContextInterface::class, Page::class);
That to avoid interfaces proliferation for no other purpose but to overcome a container limitation.
Next
Time to get down to some TDD and get the refactoring done, the refactoring will happen in version-2.0
branch.