A dependency injection container with autowiring

Installation

composer require avris/container

Usage

Basics

Container resolves dependencies between defined services, in order to simplify the development process, avoid duplication of code, facilitate interoperability and improve maintainability and testability. See: Dependency Injection pattern.

$parameterProvider = new SimpleParameterProvider(['ROOT_DIR' => __DIR__]);
// parameter provider is optional

$container = new Container($parameterProvider);

$container->set('number', 4);
$container->set(Foo::class, new Foo);
$container->setDefinition(Bar::class, [
    'arguments' => [
        '$foo' => '@' . Foo::class,
        '$dir' => '%ROOT_DIR%/bar',
        '$number' => '@number',
        '$float' => 69.123,
    ],
    'public' => true,
]);
$container->setDefinition(BarInterface::class, Bar::class); // alias

$container->get('number');              // 4
$container->get(Foo::class);            // new Foo
$container->get(BarInterface::class);   // new Bar(new Foo, __DIR__ . '/bar', 4, 69.123)
$container->getParameter('ROOT_DIR');   // __DIR__

Options

  • class – the class of the service, will default to the service name if not given. If the given class implements the Resolver interface, it will be instantiated, and it’s resolve method executed to provide an actual value to be put in the container.
  • arguments – constructor arguments.
  • calls – method calls to be executed right after constructing the service (setter injection etc.)

    'calls' => [
        ['setLogger', ['@logger']],
        ['registerListener', ['@listenerA']],
        ['registerListener', ['@listenerB']],
    ],
  • tags – an array of string that help group similar services together; tagged services can be injected with #tagName:

    $container->setDefinition(HandlerA::class, ['tags' => 'handler']);
    $container->setDefinition(HandlerB::class, ['tags' => 'handler']);
    $container->setDefinition(HandlerC::class, ['tags' => 'handler']);
    $container->setDefinition(Manager::class, ['arguments' => ['$handlers' => '#handler']]);
  • factory – determines if each get should create a new service (true), or should one service be reused (false, default).
  • resolve – instead of using class + arguments to construct the service, you can use resolve to define how it should be created:

    $container->setDefinition('foo', ['resolve' => 4]); // 4
    $container->setDefinition('language', ['resolve' => '@Request.locale.language']); // $container->get('Request')->getLocale()->getLanguage()
  • public – determines if the service should be accessible directly with get, or can it only be injected into other services.

ContainerCompiler: autowiring and autoconfiguration

Usually it’s obvious, which service should be injected into another. For instance when your service has a constructor argument Psr\Cache\CacheItemPoolInterface $cache, and the container does have a service named Psr\Cache\CacheItemPoolInterface, then explicitly writing ['arguments' => ['$cache' => '@Psr\Cache\CacheItemPoolInterface'] is redundant. You can always specify the dependencies manually, then autowiring won’t overwrite them.

Autowiring is not magic – it just follows simple rules to determine, which service should be injected into the constructor:

  • if the argument is a class which is defined in the container, use this service,
  • if the argument is a class which is not defined in the container, try to autowire that class and create a private service out of it,
  • if the argument is an array and its name ends with s (e.g. array $helpers), inject an array of services with a specific tag (#helper).
  • if the argument is of type Bag, inject the config value with its name (e.g. Bag $localisation -> @config.localisation),
  • if its name starts with env, inject a parameter: (e.g. string $envCacheDir -> %CACHE_DIR%)
  • if none of the above is true, but there is a default value, just use it,
  • if none of the above is true, throw an exception – this argument should be defined explicitly.

Autoconfiguration is another way to make your life simpler. For instance, if you’re using Twig, you might want all the classes in your code that extend Twig\Extension\AbstractExtension to be automatically registered as twig extension. Autoconfiguration lets you define what default config (tags, public etc.) should be added to them.

To use autowiring and autoconfiguration, run the ContainerCompiler:

$container = new Container;

$services = [
    'App\' => [
        'dir' => '%MODULE_DIR%/src/',
        'exclude' => ['#^Entity/#'],
    ],

    'App\Foo' => [
        'arguments' => [
            '$bar' => 5,
        ],
    ],

    'App\Bar' => [
        'public' => true,
    ],
];

$autoconfiguration = [
    'Twig\Extension\AbstractExtension' => [
        'tags' => ['twigExtension'],
    ],
];

$definitions = new ContainerCompiler(
    $container,
    $services,
    $autoconfiguration
))->compile();

/** @var ServiceDefinition $definition */
foreach ($definitions as $name => $definition) {
    if (!$container->has($name)) {
        $container->setDefinition($name, $definition);
    }
}

In this example, the whole %MODULE_DIR%/src/ except for (the /src/Entity dir) will be scanned for PHP files and all the found classes will be autowired as private services. If some of them are not used and not public, they will be removed from the container.

Compiling the container has no impact on performance on production environment, as long as you cache the result of compile().

ServiceLocator

Service locator restricts access to services in the container only to a selected list of names:

$container = new Container();
$container->set('foo', 'abc');
$container->set('bar', 'def');
$container->set('secret', 'XYZ');

$locator = new ServiceLocator($container, ['foo', 'bar']);

$locator->get('foo');     // 'abc'
$locator->get('bar');     // 'def'
$locator->get('secret');  // Exception

ContainerAssistedBuilder

ContainerAssistedBuilder can be used to join together a couple of ContainerBuilderExtensions which encapsulate a set of service definitions that form a library together. For an example, see Avris Localisator.

Micrus

This container was originally built as a part of the Micrus framework.