HAPPYACCIDENTS

Revisiting: Benchmarking Dependency Injection Containers

      COMMENTS
  Dependency Injection, Zend Framework, Laravel, Orno, Aura, PHP-DI, Symfony, Pimple

A few months ago I wrote a post detailing and benchmarking several dependency injection containers. Recently there have been some changes to some of these containers and some new packages have appeared in the community so I decided to revisit the post and update it a little.

When I say a little, I'm going to be re-writing the entire post to give you a little more information about the containers and some more in-depth benchmarks.

Original Aim of the Post

The post was originally born out of some extensive research I had been undertaking to find a feature rich container that wouldn't slow down my applications by a noticable amount. I was met with a few comments about "micro-optimisations" and that the article was a waste of time and unfair. I have to point out now that I was in no way trying to make micro-oprimisations but rather found the information from the benchmarks quite interesting and decided to share my findings. Addressing the fairness of the benchmarks is another matter, each benchmark sets out a task, if the container can achieve this task, it is included in the benchmark and the simplest route is taken. This is the same for all of the containers I have looked at, we cannot consider a benchmark unfair because I am not using a caching feature or mapping functionality of a particular package. That is the point where the benchmarks would become unfair.

Original Conclusion

Each of the containers had pros and cons. The larger enterprise packages, although packed with functionality and covering the most use cases, were particularly slow. The smaller packages were all fast and all had great features, but not all of the features I wanted.

Illuminate\Di from Laravel 4 stood out for me as my favourite but I wanted the ability to configure my objects a little more before the container returned them to me, for example, method injection is not available but can be very useful throughout applications to change state of objects and allow better re-usability.

After my research, I made the decision that I had enough knowledge of the subject to write my own container. Using what I had learned, I built Orno\Di and added it to the benchmarks to ensure that as feature rich as I made it, it remained fast.

So, What's Changed?

Since open sourcing the benchmarks, a few developers have forked the repo and/or contacted me to add containers to the benchmarks.

So with that in mind, it makes sense to build a more comprehensive set of benchmarks with a little more analysis of the packages.

The Extreme Case

The extreme case of nested objects stays the same.

Object Foo depends on Bar.

<?php

class Foo
{
    public $bar;

    public function __construct(Bar $bar = null)
    {
        $this->bar = $bar;
    }

    public function setBar(Bar $bar)
    {
        $this->bar = $bar;
    }
}

Object Bar depends on an implementation of BazInterface.

<?php

class Bar
{
    public $baz;

    /**
     * @param Baz $baz
     */
    public function __construct(BazInterface $baz = null)
    {
        $this->baz = $baz;
    }

    public function setBaz(BazInterface $baz)
    {
        $this->baz = $baz;
    }
}

Interface BazInterface.

<?php

interface BazInterface
{
    public function requiredMethod();
}

Object Baz, implementation of BazInterface depends on Bam.

<?php

class Baz implements BazInterface
{
    public $bam;

    public function __construct(Bam $bam = null)
    {
        $this->bam = $bam;
    }

    public function setBam(Bam $bam)
    {
        $this->bam = $bam;
    }

    public function requiredMethod()
    {
        // ...
    }
}

Object Bam depends on an implementation of BartInterface.

<?php

class Bam
{
    public $bart;

    /**
     * @param Bart $bart
     */
    public function __construct(BartInterface $bart = null)
    {
        $this->bart = $bart;
    }

    public function setBart(BartInterface $bart)
    {
        $this->bart = $bart;
    }
}

Interface BartInterface.

<?php

interface BartInterface
{
    public function requiredMethod();
}

Object Bart implementation of BartInterface.

<?php

class Bart implements BartInterface
{
    public function requiredMethod()
    {
        // ...
    }
}

Conditions

Benchmark 1

The container must resolve the object with all dependencies in place automatically only by registering aliases for interfaces to concrete implementations.

Excluded: Symfony\DependencyInjection, Pimple, Aura.Di

Zend\Di

<?php

$container = new Zend\Di\Di;

// alias interfaces to concretes
$container->instanceManager()->addTypePreference('BazInterface', 'Baz');
$container->instanceManager()->addTypePreference('BartInterface', 'Bart');

// resolve object
$container = $zend->get('Foo');

Illuminate\Container (Laravel)

<?php

$container = new Illuminate\Container\Container;

$container->bind('Foo', 'Foo');
$container->bind('BazInterface', 'Baz');
$container->bind('BartInterface', 'Bart');

$foo = $container->make('Foo');

Orno\Di

<?php

$container = new Orno\Di\Container;

$container->register('BazInterface', 'Baz', false, true);
$container->register('BartInterface', 'Bart', false, true);

$foo = $container->resolve('Foo');

League\Di

<?php

$container = new League\Di\Container;

$container->bind('BazInterface', 'Baz');
$container->bind('BartInterface', 'Bart');

$foo = $container->resolve('Foo');

PHP-DI

<?php

$container = new DI\Container;
$container->useAnnotations(false);

$container->addDefinitions(
    array(
         'BazInterface' => array(
             'class' => 'Baz'
         ),
         'BartInterface' => array(
             'class' => 'Bart'
         )
    )
);

$foo = $container->get('Foo');

Results

Benchmark 1 Results

Benchmark 2

The container must resolve the object with all dependencies in place by registering ALL objects. Expecting faster resolution than Benchmark 1.

Excluded: Symfony\DependencyInjection, Pimple, Aura.Di

Illuminate\Container (Laravel)

<?php

$container = new Illuminate\Container\Container;

$container->bind('Foo', 'Foo');
$container->bind('Bar');
$container->bind('Bam');
$container->bind('BazInterface', 'Baz');
$container->bind('BartInterface', 'Bart');

$foo = $container->make('Foo');

Zend\Di

<?php

$container = new Zend\Di\Di;

$container->instanceManager()->addTypePreference('Foo', 'Foo');
$container->instanceManager()->addTypePreference('Bar', 'Bar');
$container->instanceManager()->addTypePreference('Bam', 'Bam');
$container->instanceManager()->addTypePreference('BazInterface', 'Baz');
$container->instanceManager()->addTypePreference('BartInterface', 'Bart');

$foo = $container->get('Foo');

Orno\Di

<?php

$container = new Orno\Di\Container;

$container->register('Foo', 'Foo', false, true);
$container->register('Bar', 'Bar', false, true);
$container->register('Bam', 'Bam', false, true);
$container->register('BazInterface', 'Baz', false, true);
$container->register('BartInterface', 'Bart', false, true);

$foo = $container->resolve('Foo');

League\Di

<?php

$container = new League\Di\Container;

$container->bind('Foo');
$container->bind('Bar');
$container->bind('Bam');
$container->bind('BazInterface', 'Baz');
$container->bind('BartInterface', 'Bart');

$foo = $container->resolve('Foo');

PHP-DI

<?php

$container = new DI\Container;
$container->useReflection(false);
$container->useAnnotations(false);

$container->addDefinitions(
    array(
         'Foo' => array(),
         'Bar' => array(),
         'Bam' => array(),
         'BazInterface' => array(
             'class' => 'Baz'
         ),
         'BartInterface' => array(
             'class' => 'Bart'
         )
    )
);

$foo = $container->get('Foo');

Results

Benchmark 3

The container must resolve the object with all dependecies in place by using a factory closure.

Excluded: Symfony\DependencyInjection, Zend\Di

Illuminate\Container (Laravel)

<?php

$container = new Illuminate\Container\Container;

$container->bind('foo', function() {
    $bart = new Bart;
    $bam = new Bam($bart);
    $baz = new Baz($bam);
    $bar = new Bar($baz);
    return new Foo($bar);
});

$foo = $container->make('foo');

Pimple

<?php

$container = new Pimple;

$container['foo'] = function() {
    $bart = new Bart;
    $bam = new Bam($bart);
    $baz = new Baz($bam);
    $bar = new Bar($baz);
    return new Foo($bar);
};

$foo = $container['foo'];

Orno\Di

<?php

$container = new Orno\Di\Container;

$container->register('foo', function() {
    $bart = new Bart;
    $bam = new Bam($bart);
    $baz = new Baz($bam);
    $bar = new Bar($baz);
    return new Foo($bar);
});

$foo = $container->resolve('foo');

Aura.Di

<?php

$container = new Aura\Di\Container(new Aura\Di\Forge(new Aura\Di\Config));

$container->set('foo', function() {
    $bart = new Bart;
    $bam = new Bam($bart);
    $baz = new Baz($bam);
    $bar = new Bar($baz);
    return new Foo($bar);
});

$foo = $container->get('foo');

PHP-DI

<?php

$container = new DI\Container;
$container->useReflection(false);
$container->useAnnotations(false);

$container->set('foo', function() {
    $bart = new Bart;
    $bam = new Bam($bart);
    $baz = new Baz($bam);
    $bar = new Bar($baz);
    return new Foo($bar);
});

$foo = $container->get('foo');

League\Di

<?php

$container = new League\Di\Container;

$container->bind('foo', function() {
    $bart = new Bart;
    $bam = new Bam($bart);
    $baz = new Baz($bam);
    $bar = new Bar($baz);
    return new Foo($bar);
});

$foo = $container->resolve('foo');

Results

Benchmark 3 Results

Benchmark 4

The container must resolve the object with all dependencies in place by injecting predefined constructor arguments.

Excluded: Pimple, Illuminate\Container (Laravel)

Symfony\DependencyInjection

<?php

$container = new Symfony\Component\DependencyInjection\ContainerBuilder;

$container->register('foo', 'Foo')
          ->addArgument(new Symfony\Component\DependencyInjection\Reference('bar'));
$container->register('bar', 'Bar')
          ->addArgument(new Symfony\Component\DependencyInjection\Reference('baz'));
$container->register('baz', 'Baz')
          ->addArgument(new Symfony\Component\DependencyInjection\Reference('bam'));
$container->register('bam', 'Bam')
          ->addArgument(new Symfony\Component\DependencyInjection\Reference('bart'));
$container->register('bart', 'Bart');

$foo = $container->get('foo');

Zend\Di

<?php

$container = new Zend\Di\Di;

$container->instanceManager()->setInjections('Foo', ['Bar']);
$container->instanceManager()->setInjections('Bar', ['Baz']);
$container->instanceManager()->setInjections('Baz', ['Bam']);
$container->instanceManager()->setInjections('Bam', ['Bart']);

$foo = $container->get('Foo');

Orno\Di

<?php

$container = new Orno\Di\Container;

$container->register('Foo')->withArgument('Bar');
$container->register('Bar')->withArgument('Baz');
$container->register('Baz')->withArgument('Bam');
$container->register('Bam')->withArgument('Bart');
$container->register('Bart');

$foo = $container->resolve('Foo');

Aura.Di

<?php

$container = new Aura\Di\Container(new Aura\Di\Forge(new Aura\Di\Config));

$container->params['Bam'] = ['bart' => $container->lazyNew('Bart')];
$container->params['Baz'] = ['bam' => $container->lazyNew('Bam')];
$container->params['Bar'] = ['baz' => $container->lazyNew('Baz')];
$container->params['Foo'] = ['bar' => $container->lazyNew('Bar')];

$foo = $container->newInstance('Foo');

League\Di

<?php

$container = new League\Di\Container;

$container->bind('Bart');
$container->bind('Bam')->addArg('Bart');
$container->bind('Baz')->addArg('Bam');
$container->bind('Bar')->addArg('Baz');
$container->bind('Foo')->addArg('Bar');

$foo = $container->resolve('Foo');

PHP-DI

<?php

$container = new DI\Container;
$container->useReflection(false);
$container->useAnnotations(false);

$container->addDefinitions(
    array(
         'Bam' => array(
             'constructor' => array('Bart'),
         ),
         'Baz' => array(
             'constructor' => array('Bam'),
         ),
         'Bar' => array(
             'constructor' => array('Baz'),
         ),
         'Foo' => array(
             'constructor' => array('Bar'),
         )
    )
);

$foo = $container->get('Foo');

Results

Benchmark 4 Results

Benchmark 5

The container must resolve the object with all dependencies in place by using setter method calls.

Excluded: Pimple, Illuminate\Container (Laravel), Zend\Di

Symfony\DependencyInjection

<?php

$container = new Symfony\Component\DependencyInjection\ContainerBuilder;

$container->register('foo', 'Foo')
          ->addMethodCall('setBar', [new Symfony\Component\DependencyInjection\Reference('bar')]);
$container->register('bar', 'Bar')
          ->addMethodCall('setBaz', [new Symfony\Component\DependencyInjection\Reference('baz')]);
$container->register('baz', 'Baz')
          ->addMethodCall('setBam', [new Symfony\Component\DependencyInjection\Reference('bam')]);
$container->register('bam', 'Bam')
          ->addMethodCall('setBart', [new Symfony\Component\DependencyInjection\Reference('bart')]);
$container->register('bart', 'Bart');

$foo = $container->get('foo');

Orno\Di

<?php

$container = new Orno\Di\Container;

$container->register('Foo')->withMethodCall('setBar', ['Bar']);
$container->register('Bar')->withMethodCall('setBaz', ['Baz']);
$container->register('Baz')->withMethodCall('setBam', ['Bam']);
$container->register('Bam')->withMethodCall('setBart', ['Bart']);
$container->register('Bart');

$foo = $container->resolve('Foo');

Aura.Di

<?php

$container = new Aura\Di\Container(new Aura\Di\Forge(new Aura\Di\Config));

$container->setter['Bam']['setBart'] = $container->lazyNew('Bart');
$container->setter['Baz']['setBam'] = $container->lazyNew('Bam');
$container->setter['Bar']['setBaz'] = $container->lazyNew('Baz');
$container->setter['Foo']['setBar'] = $container->lazyNew('Bar');

$foo = $container->newInstance('Foo');

League\Di

<?php

$container = new League\Di\Container;

$container->bind('Bart');
$container->bind('Bam')->withMethod('setBart', ['Bart']);
$container->bind('Baz')->withMethod('setBam', ['Bam']);
$container->bind('Bar')->withMethod('setBaz', ['Baz']);
$container->bind('Foo')->withMethod('setBar', ['Bar']);

$foo = $container->resolve('Foo');

PHP-DI

<?php

$container = new DI\Container;
$container->useReflection(false);
$container->useAnnotations(false);

$container->addDefinitions(
    array(
         'Bart' => array(
             'class' => 'Bart'
         ),
         'Bam' => array(
             'methods' => array(
                 'setBart' => 'Bart',
             )
         ),
         'Baz' => array(
             'methods' => array(
                 'setBam' => 'Bam',
             )
         ),
         'Bar' => array(
             'methods' => array(
                 'setBaz' => 'Baz',
             )
         ),
         'Foo' => array(
             'methods' => array(
                 'setBar' => 'Bar',
             )
         )
    )
);

$foo = $container->get('Foo');

Results

Benchmark 5 Results

Analysis

Zend\Di

Zend\Di was the first container I used as I was quite excited about the progress it was making during the development of Zend Framework 2. However, once the performance loss was discovered, Zend focused their efforts on Zend\ServiceManager, a powerful service locator that is now the heart of Zend Framework courses, certification and documentation. The container itself never really got the attention on performance that it deserved. In summary, this means that if you are planning on building an application around Zend\Di, as the project grows, it could lead to a performance loss of around 25% over other containers as your objects and dependencies become more complex.

Symfony\DependencyInjection

As is with all Symfony components, Symfony\DependencyInjection is extremely well engineered and a pleasure to use. It doesn't suffer from as large a performance loss as it's enterprise competitors, however, there is a loss. The decision on whether to use this container would come down to a balance between customisation and performance. You could not ask for a more configurable container. It also offers object dump features that will inspect your objects and cache the results for faster resolution.

Illuminate Container (Laravel 4)

Illuminate\Container was originally my favourite of all the containers I researched. It's only fall down is that you are limited to what you can do with an object before the container returns it to you. For any projects that don't require that level of customisation, this would be my choice. The syntax is nice, the Reflection API is used sparingly and well to provide easy development, and I can only see this package growing.

PHP-DI

Earlier versions of PHP-DI put all emphasis on it's magic annotation injection which I think hurt the project. It was assumed when reading the documentation that PHP-DI was some sort of black magic and not actually a dependency injection container, for these reasons, it was excluded from my original post. It turns out that the features to facilitate proper dependency injection were always there but not well documented. Since the original post, version 3 has been released with more focus on the packing being a dependency injection container. The annotations are still there, just not needed. It also offers caching to improve performance. On the downside, the container returns a shared object by default when resolving an object more than once, my personal preference would be a new object every time unless stated.

Aura.Di

The entire Aura project is PHP 5.4+ and is exploring design patterns and new techniques. The container has nice syntax and pretty good performance.

Pimple

Pimple has been around for a while now, one of Fabien Potencier's side projects to provide a lightweight extensible container. It doesn't claim to be more than it is. Pimple stores objects for lazy loading with a few extra features for object sharing etc... It would require definitions of your entire project to handle all dependency injection.

League\Di

This package was brought to my attention recently, my first look at the source code revealed that is very similar to a couple of other containers including my own. I have since been given a credit in the Github README and made aware that the aim of the package is to improve even further on performance when resolving objects.

Summary

I should point out again that this post is not an attempt to micro-optimise anything, micro-seconds are nothing in the short term, but as projects grow and objects/dependencies become more complex, they add up. This is simply a resource and documentation of something I found quite interesting.

Phil Bennett

I'm a Yorkshireman living and working in London as a Senior Developer for VideoGamer.com. I also create and contribute to Open Source Software.

comments powered by Disqus