Pragmatism in the real world

Replacing Pimple in a Slim 3 application

One feature of Slim 3 is that the DI container is loosely coupled to the core framework. This is done in two ways:

  1. The App composes the container instance rather than extending from it.
  2. Internally, App depends on the container implementing the container-interop interface.

You can see this decoupling in the way that you instantiate a Slim 3 application:

$settings = [];
$container = new Slim\Container($settings);
$app = new Slim\App($container);

Slim 3 ships with Pimple by default, but my preference is for Zend\ServiceManager, so I decided to integrate ServiceManager into Slim 3.

RKA\ZsmSlimContainer is the result of this work.

Usage

To use RKA\ZsmSlimContainer, simply add it via composer:

composer require akrabat/rka-slim-zfsm-container

and then update your index.php:

$settings = [];
$container = new RKA\ZsmSlimContainer\Container($settings);
$app = new Slim\App($container);

As you can see, due to the decoupling, the change required is very small and everything still works! The only thing that doesn’t work is Pimple\ServiceProviderInterface‘s register() method as that is tightly coupled to the Pimple container itself.

Implementation

To implement this, I extended Zend\ServiceManager and in the constructor registered Slim’s default services. This is easy enough to do with ServiceManager’s setFactory method. For example:

    $this->setFactory(
        'callableResolver',
        function ($c) { return new CallableResolver($c); },
        false
    );

In this example, the callableResolver service needs to return a new instance every time it is retrieved from the container, so I pass false in as the third parameter to setFactory as it will default to shared otherwise.

This was the minimum effort required, but I wanted to be able to use code that had been written for Pimple (as far as possible), such as Twig-View. To do this, I implemented ArrayAccess as that’s how Pimple works. Implementing offsetGet, offsetExists and offsetUnset was easy as I simply had to call ServiceManager’s get, has and unregisterService methods respectively, but offsetSet required a bit more work.

When setting a service via array access, you can assign settings, closures or instances:

$container['foo'] = 'a setting';
$container['bar'] = function($c) { return new Bar($c->get('foo)); }
$container['baz'] = new Bar('another setting');

In ServiceManager, you would use different methods, such as setService or setFactory.

I needed to detect what was intended and call the correct internal method, so I came up with this:

    public function offsetSet($id, $value)
    {
        if (is_object($value)) {
            if ($value instanceof \Closure) {
                return $this->setFactory($id, $value);
            }
            return $this->setService($id, $value);
        }

        if (is_string($value) && class_exists($value)) {
            return $this->setInvokableClass($id, $value);
        }

        return $this->setService($id, $value);
    }

I also support the invokable feature of ServiceManager, where if the value is a class name string, then ServiceManager can instantiate when required. This would be used like this:

$container['qux'] = \My\Fun\Thing::class;

 

All in all, I was pleased at how the decoupled nature of Slim 3 made it easy to replace Pimple with my preferred DI container. I particularly like how it is seamless enough that I can continue to use Twig-View.