them/container

Autowiring DI Container based on and a drop in replacement for pimple/pimple

2.0.0 2024-03-20 16:35 UTC

This package is auto-updated.

Last update: 2024-04-20 16:52:21 UTC


README

them/container on packagist GPLv3

them/container

them/container is a dependency injection container for PHP >= 8.1.

Basically it's Pimple with autowiring capabilities and the psr/container interface on top.

Installation

Before using them/container in your project, add it to your composer.json file:

composer require them/container

Usage

If you don't already use Pimple, please read https://github.com/silexphp/Pimple#readme first.

them/container is a drop-in replacement for Pimple. When creating your container use the class \Them\Container\Container instead of Pimple\Container.

If you use service provider (https://github.com/silexphp/Pimple#extending-a-container) you may make them implement \Them\Container\ServiceProviderInterface instead of \Pimple\ServiceProviderInterface. The method register will then receive not a \Pimple\Container but a \Them\Container\Container instance.

Changes to Pimple

PSR-11

In contrast to Pimple, them/container is PSR-11 compliant by default. So no need to do things like

$container = new \Pimple\Container();
$psr11 = new \Pimple\PsrContainer($container);

Registering services

Besides the Pimple ways to register a service, \Them\Container\Container provides the method

set(string $id, mixed $value): self

Pre-registered services

Upon initialization, the container instance already has one service registered which is available under the following two ids, \Them\Container\Container::class and \Psr\Container\ContainerInterface::class where both point to the container instance itself.

Service aliases

Sometimes you need to register a service with more than one key. Think of a logger, that needs to be available under \Psr\Log\LoggerInterface but also under \Monolog\Logger:


declare(strict_types=1);

use Psr\Log\LoggerInterface;
use Monolog\Logger;
use Them\Container\Container;

$container = new Container();

$container[Logger::class] = fn() => new Logger('logger');

$container->aka(Logger::class, LoggerInterface::class);

Autwiring

When trying to get a service by id from them/container which is not already known to the container, it tries to instantiate it.

This, of course can only work if

  1. the provided id is a name of an existing class
  2. the class can be instantiated (i. e. not abstract, not an interface)
  3. the constructor can be invoked (i. e. not private/protected)
  4. every constructor parameter has a type assigned (not __construct($value))
  5. every parameter type can be resolved by the container using its name.

Constructor injection

Given a class

<?php

declare(strict_types=1);

use Psr\Log\LoggerInterface;

final readonly class SomeService
{
    public function __construct(
        private LoggerInterface $logger,
    ) {}
}

If you call $container->get(SomeService::class), the container will search for the id \Psr\LoggerInterface to resolve the value for the parameter $logger - and miserably fail if there is no such key registered and due to the fact that an interface cannot be instantiated.

To tell the container which key to use instead, just add the Attribute \Them\Container\Attribute\Constructor to the service class:

<?php

declare(strict_types=1);

use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Them\Container\Attribute\Constructor;

#[Constructor(['logger' => Logger::class])]
final readonly class SomeService
{
    public function __construct(
        private LoggerInterface $logger,
    ) {}
}

In this case the container will use the key \Monolog\Monolog for resolving the parameter $logger.

Setter injection

Sometimes you need to inject a service by a setter method, for example a logger when working with the \Psr\Log\LoggerAwareInterface.

For this to achieve you add one or more Them\Container\Attribute\Method Attributes to the service class, telling the container to call a method with parameter values resolved by parameter types. If you need to override this, use the attribute's 2nd parameter to assign a key to a parameter:

<?php

declare(strict_types=1);

use Monolog\Logger;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Scn\Container\Attribute\Method;

#[Method('setLogger', ['logger' => Logger::class])]
final class SomeService implements LoggerAwareInterface
{
    protected ?LoggerInterface $logger = null;

    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }
}

Resolving dependencies to interfaces (or other non instantiatable classes)

If you are coding against interfaces, you will always have to tell the container which implementation to use when you ask for a service by an interface name. This can be done with aliases (see above) but also directly at the interface by adding the attribute \Them\Container\Attribute\Specific with the "real" class as parameter to the interface:

<?php

declare(strict_types=1);

use Them\Container\Attribute\Specific;
use Them\Container\Container;

require_once __DIR__ . '/../vendor/autoload.php';

#[Specific(Service::class)]
interface ServiceInterface {}

final class Service implements ServiceInterface {}

$c = new Container();
var_dump($c->get(ServiceInterface::class));

If you now ask the container for the service by the id ServiceInterface it will instantiate Service and return that instance instead.

Autowiring service providers

When registering a service provider to the container, you can not only provide a \Pimple\ServiceProviderInterface or a \Them\Container\ServiceProviderInterface instance, but also just the class name of one of the above. The container will try to autowire and register them.