Time for a new version of di52

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.