them / container
Autowiring DI Container based on and a drop in replacement for pimple/pimple
Requires
- php: >=8.2
- pimple/pimple: ^3.5
- psr/container: ^2.0
- them/attributes: ^1.1
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.52
- jetbrains/phpstorm-attributes: ^1.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan: ^1.10
- phpstan/phpstan-strict-rules: ^1.5
- phpunit/phpunit: ^11.0
README
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
- the provided id is a name of an existing class
- the class can be instantiated (i. e. not abstract, not an interface)
- the constructor can be invoked (i. e. not private/protected)
- every constructor parameter has a type assigned (not
__construct($value)) - 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.