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.