luizfilipezs / container
Dependency injection container
Requires
- php: ^8.4
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpunit/phpunit: ^12.0
This package is auto-updated.
Last update: 2025-07-31 00:19:45 UTC
README
This library implements a dependency injection system.
Minimum requirements
- PHP 8.4
- Composer
Installation
Inside your project folder, run:
composer require luizfilipezs/container
Usage
Getting a definition
$container = new Container(); $container->set(MyInterface::class, MyClass::class); $myObject = $container->get(MyInterface::class);
The above example creates a new instance of MyClass
, typed as MyInterface
.
Setting a definition
Class string
$container->set(UserServiceInterface::class, UserService::class);
Class instance
$container->set(UserServiceInterface::class, new UserService());
Closure
$container->set(UserServiceInterface::class, fn() => new UserService());
Checking for a definition
if ($container->has(UserServiceInterface::class)) {
Removing a definition
$container->remove(UserServiceInterface::class);
Setting a singleton
Singleton classes are automatically recognized via #[Singleton]
attribute.
use Luizfilipezs\Container\Attributes\Singleton; #[Singleton] class UserService { public string $foo = 'bar'; } $userService1 = $container->get(UserService::class); $userService2 = $container->get(UserService::class); $userService1->foo = 'baz'; echo $userService2->foo; // 'baz'
If you set a singleton class as definition for an interface, both the interface and the class will be set with the actual instance of the class as soon as it is created:
$container->set(UserServiceInterface::class, UserService::class); $userServiceViaInterface = $container->get(UserServiceInterface::class); $userServiceViaInterface->setFoo('baz'); $userServiceViaClass = $container->get(UserService::class); $userServiceViaClass->getFoo(); // 'baz'
Setting a class with lazy constructor
Lazy constructor was natively implemented in PHP 8.4. It allows an object to be created without its __construct
method getting called until
an attribute is used.
use Luizfilipezs\Container\Attributes\Lazy; #[Lazy] class UserService { public function __contruct(private readonly UserRepositoryInterface $userRepository) { echo 'Constructor was called.'; } public function getAll(): array { return $this->userRepository->findAll(); } } $userService = $container->get(UserService::class); $users = $userService->getAll(); // 'Constructor was called.'
It is also possible to disable initialization on reading specific properties:
use Luizfilipezs\Container\Attributes\{Lazy, LazyInitializationSkipped}; #[Lazy] class MyClass { #[LazyInitializationSkipped] public string $skippedProp = 'foo'; public string $normalProp = 'bar'; public function __construct() { echo 'Constructor was called.'; } } $myInstance = $container->get(MyClass::class); $myInstance->skippedProp; $myInstance->normalProp; // 'Constructor was called.'
Capturing lazy construction
To know when a lazy __construct
gets called you can call Container::eventHandler
:
use Luizfilipezs\Container\Enums\ContainerEvent; $container->eventHandler->on( event: ContainerEvent::LAZY_CLASS_CONSTRUCTED, callback: static function (string $className, object $instance) { echo "{$className}::__construct was called."; }, ); $instance = $container->get(MyClass::class); $instance->foo; // 'MyClass::__construct was called.'
Setting non-class definitions
You can set a definition for any value, allowing it to get automatically injected even if it is not a class.
use Luizfilipezs\Container\Attributes\Inject; enum ApiConstant: string { case API_KEY = 'API_KEY'; case API_SECRET = 'API_SECRET'; } $container->setValue(ApiConstant::API_KEY, 'my_api_key'); $container->setValue(ApiConstant::API_SECRET, 'my_api_secret'); class ApiService { public function __construct( #[Inject(ApiConstant::API_KEY)] string $apiKey, #[Inject(ApiConstant::API_SECRET)] string $apiSecret, ) {} } // call __construct with: 'my_api_key', 'my_api_secret' $apiService = $container->get(ApiService::class);
Any type of value can be set, but it will be strictly compared to the parameter type:
$container->setValue('SOME_INT', 123); class MyClass { public function __contruct(#[Inject('SOME_INT')] float $value) {} } $object = $container->get(MyClass::class); // ContainerException: Container cannot inject "SOME_INT". It is not the same // type as the parameter. Expected float, got int.
Value definition methods
$container->setValue('key', 'value'); $value = $container->getValue('key'); // 'value' $exists = $container->hasValue('key'); // true $container->removeValue('key');
Advanced options
Container
constructor has three parameters:
strict
(defaults tofalse
): iftrue
, only definitions set explicitly (viaset()
) will be provided.skipNullableClassParams
(defaults totrue
): iftrue
, nullable constructor parameters typed as a class or an interface will always be set tonull
, except if the parameter has theInject
attribute bound to it.skipNullableValueParams
(defaults totrue
): iftrue
, nullable constructor parameters typed as a primitive type will always be set tonull
, except if the parameter has theInject
attribute bound to it.
Example with strict
:
$container = new Container(strict: true); // ContainerException, because there is no explicit definition for "SomeClass" $instance = $container->get(SomeClass::class);
Examples with skipNullableClassParams
If true
:
$container = new Container(skipNullableClassParams: true); $instance = $container->get(SomeClass::class); $instance->nullableDependency; // null
If false
:
$container = new Container(skipNullableClassParams: false); $instance = $container->get(SomeClass::class); $instance->nullableDependency; // object
Examples with skipNullableValueParams
If true
:
$container = new Container(skipNullableClassParams: true); $instance = $container->get(SomeClass::class); $instance->nullableString; // null
If false
:
$container = new Container(skipNullableClassParams: false); $instance = $container->get(SomeClass::class); $instance->nullableString; // error, because string cannot be injected
In both examples above, if the nullable parameter was bound to the Inject
attribute, the value would be injected anyway, because it forces injection.
Contributing
Forking
At the top of the Github repository page, click the Fork button. Then clone the forked repository to your machine.
Installing dependencies
npm install # install Prettier package composer update # install composer dependencies
Testing
Run:
./vendor/bin/phpunit tests
When making changes to the codebase, remember to create tests that cover all scenarios.