tereta / di
Dependency injection container with a PSR-11 interface.It stores services, creates objects with their dependencies and returns the same instance when it is needed again.
Requires
- php: >=8.2
- psr/container: ^2.0
Requires (Dev)
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.0
- squizlabs/php_codesniffer: ^3.13
Suggests
None
Provides
Conflicts
None
Replaces
None
README
π English | Π ΡΡΡΠΊΠΈΠΉ | Π£ΠΊΡΠ°ΡΠ½ΡΡΠΊΠ°
Dependency container for PHP with a PSR-11 interface. It creates objects together with everything they need in the constructor.
Requirements
PHP 8.2+, psr/container ^2.0.
Installation
composer require tereta/di
Quick start
use Tereta\Di\Container;
class Mailer
{
}
class Newsletter
{
public function __construct(private Mailer $mailer)
{
}
}
$container = new Container();
$newsletter = $container->create(Newsletter::class);
The container sees that Newsletter needs a Mailer, creates it and passes it to the constructor.
New object - create()
create() returns a new object every time.
The second parameter lets you pass constructor values by parameter name:
class Report
{
public function __construct(private int $id, private string $format = 'pdf')
{
}
}
$report = $container->create(Report::class, ['id' => 15]);
$csv = $container->create(Report::class, ['id' => 15, 'format' => 'csv']);
If a value is not passed, the default value from the constructor is used.
If a value from the second argument does not fit the parameter type, a Runtime error is thrown. A value from $shared that does not fit is skipped.
Values from the second argument are only for the class being created, nested objects do not receive them.
Values that nested objects must receive too are passed in the third argument, $shared.
A value is matched by parameter name, and an object also by type. A value from $arguments wins over the same name in $shared.
A value whose name matches a constructor parameter goes only to that parameter, it is not used for other parameters of the same type:
class Connection
{
public function __construct(private string $host = 'localhost')
{
}
}
class Repository
{
public function __construct(private Connection $connection, private string $table = 'users')
{
}
}
$repository = $container->create(Repository::class, ['table' => 'orders'], ['host' => 'db.local']);
// Repository gets table = orders, and the nested Connection gets host = db.local
$repository = $container->create(Repository::class, ['host' => 'db.local', 'table' => 'orders']);
// host is ignored: Repository has no such parameter, and Connection does not receive it
- One name gets one value on all levels, different values for different levels can not be set.
- Singletons do not receive passed values, they use only the parameters from
setSingleton().
One shared object - setSingleton() and get()
Some objects are needed as a single instance for the whole application, for example a database connection outside swoole or in standard mode.
Such a class is registered with setSingleton() and retrieved with get():
$container->setSingleton(Database::class, ['dsn' => 'mysql:host=localhost;dbname=app']);
$db = $container->get(Database::class);
$same = $container->get(Database::class); // the same object
- Singleton parameters are set only in
setSingleton(),get()takes no parameters. - The object is created on the first
get()call, not on registration. - If another object needs the class in its constructor, it gets this shared instance too.
get()works only with registered singletons, usecreate()for other classes.create()can not be called for a singleton.- Configure the container with
set()andsetSingleton()before the firstget(). A singleton that is already created is not recreated ifset()is changed later.
Interface and its implementation - set()
If a constructor expects an interface or a class override, tell the container which class to create:
interface LoggerInterface
{
}
class FileLogger implements LoggerInterface
{
}
class Service
{
public function __construct(private LoggerInterface $logger)
{
}
}
$container->set(LoggerInterface::class, FileLogger::class);
$service = $container->create(Service::class); // gets FileLogger
set() also works together with singletons:
$container
->set(DatabaseInterface::class, Database::class)
->setSingleton(Database::class, ['dsn' => 'mysql:host=localhost;dbname=app']);
$db = $container->get(DatabaseInterface::class);
A singleton can also be registered on the interface - then one shared object is used both for the interface and for the class:
$container
->set(DatabaseInterface::class, Database::class)
->setSingleton(DatabaseInterface::class, ['dsn' => 'mysql:host=localhost;dbname=app']);
$container->get(DatabaseInterface::class); // shared Database object
$container->get(Database::class); // the same object
$container->has(Database::class); // true
If two interfaces point to one class and both are registered as singletons, each of them has its own object.
Then get(Database::class) throws a Runtime error, because it is not clear which object to return.
Only get() returns the shared object by class.
create(Database::class) and constructors that expect exactly the Database class, not the interface, get a separate new object.
If the shared object is needed everywhere, register the class: setSingleton(Database::class, [...]).
Check - has()
has() tells whether a singleton object can be retrieved with get():
$container->has(Database::class); // true if registered with setSingleton()
Interfaces
Tereta\Di\Interfaces\Factory - an interface with the create() method.
Use it in your classes when you need to create objects but do not need the whole container:
use Tereta\Di\Interfaces\Factory as FactoryInterface;
class ReportService
{
public function __construct(private FactoryInterface $factory)
{
}
public function make(int $id): Report
{
return $this->factory->create(Report::class, ['id' => $id]);
}
}
$service = $container->create(ReportService::class);
The container passes itself into parameters of the Factory, Provider, Psr\Container\ContainerInterface or Container type.
Tereta\Di\Interfaces\Resolver - the interface of the part that builds objects for the container.
You can pass your own implementation to the container constructor, for example to log object creation:
$container = new Container(new MyResolver());
Tereta\Di\Interfaces\Provider - the interface the resolver uses to ask the container for nested objects.
The container implements it, so a custom resolver gets the container itself.
Errors
Tereta\Di\Exceptions\NotFound- the class is not found, orget()is called for a class that is not registered as a singleton.Tereta\Di\Exceptions\Circular- classes depend on each other in a circle.Tereta\Di\Exceptions\Runtime- the object could not be created, for example a constructor parameter has no value or a passed value has the wrong type.
The error message contains the chain of classes where the problem happened.