Dependency Injection
(DI) is a lightweight extensible PHP
library for dependency injection
for stateful
application.
The library is designed for PHP 8.4
using the LazyProxy API
.
- Zero configuration. (Ability to inject dependencies without modifying the code in the dependent class).
- Support for the concept of
environment
/scope
for dependency lookup. - Support parent/child containers.
- Constructor injection of dependencies
- Injection of dependencies into properties
- Injecting configuration values as a Dependency
- Lazy loading of dependencies
- Auto dereferencing a
WeakReference
inside the container - Handling circular dependencies
- Support php-attributes for describing dependencies
- Custom dependency providers
- Custom descriptor providers
You can install Dependency Injector using Composer. Run the following command:
composer require ifcastle/di
Please read the Project Philosophy section before using the library.
The example below demonstrates how the library works with the SomeClass
class,
which implements the SomeInterface interface.
The class definition does not depend on the Dependency Injection
implementation.
Dependencies are injected via the class constructor
.
The library automatically binds dependencies to their interfaces.
declare(strict_types=1);
use IfCastle\DI\ContainerBuilder;
use IfCastle\DI\Lazy;
readonly class SomeClass implements SomeInterface
{
public function __construct(
// Required dependency
private SomeRequiredInterface $required,
// Optional dependency (can be null)
private SomeOptionalInterface $optional = null,
// Support a complex dependency type
private Interface1|Interface2 $someElseUnion,
// Support a complex dependency type with interception
private Interface1&Interface2 $someElseInterception,
// Dependency as configuration value
private int $configValue = 42,
) {}
}
// 1. Create a container builder
$builder = new ContainerBuilder();
// 2. Define the constructible dependencies
$builder->bindConstructible(SomeInterface::class, SomeClass::class);
// 2. Bind several interfaces-aliases or string-key to one class
$builder->bindConstructible([Interface1::class, Interface2::class, 'string-key'], SomeElseClass::class);
// 2. Support WeakReference dereferencing
$builder->bindObject(SomeOptionalInterface::class, WeakReference::create($someObject));
// 2. Define the configuration values
$builder->set('configValue', 42);
// 3. Build the container
$container = $builder->buildContainer(new Resolver());
// 4. Get the dependency
$some = $container->resolveDependency(SomeInterface::class);
Attributes provide a more precise way to describe features for dependency resolution. This library supports several attributes that can be used:
Dependency
- a general descriptor for a dependency.FromConfig
- indicates that the dependency should be retrieved from the configuration.FromRegistry
- indicates that the dependency should be retrieved from the registry.
use IfCastle\DI\Dependency;
use IfCastle\DI\FromConfig;
readonly class SomeClass implements SomeInterface
{
public function __construct(
#[Dependency(key: SomeRequiredInterface::class)]
private mixed $required,
#[Dependency(isLazy: true)]
private SomeOptionalInterface $optional = null
#[FromConfig('someClass.configValue')]
private int $configValue = 0
) {}
}
The library supports lazy loading of dependencies by special attribute:
use IfCastle\DI\Lazy;
readonly class SomeClass implements SomeInterface
{
public function __construct(
#[Lazy] private SomeRequiredInterface $lazy,
) {}
}
Warning: Lazy dependencies are implemented using the PHP
LazyProxy
API, so the same dependencies in different classes will be different objects!This means the
===
operation will returnfalse
, andspl_object_id()
will return different values.
The library allows resolving circular dependencies if the dependency is not used during resolution.
If a circular dependency occurs, the library will create a LazyProxy
object and return it.
Warning: Lazy or circular dependencies cannot be used BEFORE the process of resolving all dependencies is completed!
readonly class SomeClass implements SomeInterface { public function __construct( #[Lazy] private SomeRequiredInterface $lazy, ) { $lazy->someMethod(); // Error: CircularDependencyException } }
You can create your own attributes to describe dependencies by implementing the DescriptorInterface.
To define a custom algorithm for dependency resolution,
DescriptorInterface
can implement the getProvider
method,
which returns the dependency.
Below is an example of a method that retrieves a value from the configuration:
#[\Override]
public function provide(
ContainerInterface $container,
DescriptorInterface $descriptor,
?DependencyInterface $forDependency = null,
array $resolvingKeys = []
): mixed {
$config = $container->findDependency(ConfigInterface::class);
if ($config === null) {
return null;
}
if ($config instanceof ConfigInterface === false) {
throw new \TypeError('Config is not an instance of ' . ConfigInterface::class);
}
return $config->findValue($this->getKey());
}
This way, you can extend the DI
logic without modifying the library's code.
This project implements a particular algorithm for dependency management, adhering to the following rule:
If Class A requires a dependency by the contract Interface, and Class B also requires a dependency Interface, both classes will receive the same dependency Class D within a single runtime environment.
The following statements are true:
- The mapping scheme between
contracts
(interfaces
) anddependencies
is called theRuntime Environment
. - The mapping scheme is shared across all dependencies.
- A single
runtime environment
cannot have two different dependencies linked to the same contract (interface). - A single
dependency
can be associated withmultiple contracts
. - An application can have multiple
runtime environments
simultaneously, each with its own mapping schemes anddependencies
. - Two
runtime environments
can have a relationship: Parent -> Child.
Aspect | IfCastle DI | Symfony |
---|---|---|
Dependency Resolution | Single container for all dependencies in one runtime environment (Service Locator approach) |
Each service explicitly defines its dependencies, configured individually |
Same Contract Handling | One contract always linked to one dependency in a runtime environment | Allows different implementations of the same interface through qualifiers, tags, or aliases |
Runtime Environments | Supports multiple runtime environments with hierarchical relationships (Parent -> Child) | Global container; separate environments typically require separate configurations |
Different Dependencies for Same Key | Not possible within the same runtime environment | Fully supported using tags, aliases, or contextual bindings |
Approach to Dependency Management | Service Locator-like: dependencies resolved from a shared container | Strict DI Container approach: dependencies injected explicitly |
Parent-Child Relationship | Direct support for hierarchical environments with inheritance | No direct concept of parent-child environments; containers are isolated |
The approach of this library has several benefits:
- Minimal code volume due to significant simplification of logic.
- The ability to inject dependencies into a class that has not been preconfigured,
provided the class has access to the
Runtime Environment
. - The dependency container is created once at the application startup (
Bootloader Strategy
) and can be reused by various components without prior configuration. - This project does not mention AutoWire because dependencies are already linked based on interface names, and this method is the PRIMARY approach.
The task of dependency resolution is typically addressed by separating the information about how to locate dependencies from the objects that require them. For example:
- There is a
Target
class. - The Target class has an array of dependencies.
- Define a
TargetDependencies
class, responsible for resolving these dependencies.
Does this approach meet the SOLID principles?
The TargetDependencies
class must duplicate knowledge about the required dependencies
of the Target
class.
However, the Target
class is the single source of truth.
Another drawback of this solution is the increased code volume and complexity. The developer must now remember two points of definition related to initialization.
For this reason, modern DI approaches combine metadata for dependency resolution directly within the Target class.
To store metadata, attributes are used, which are directly placed in the Target
class.
Now the knowledge about how to resolve dependencies flows into the Target
class,
binding it to the DI implementation.
This coupling results in components requiring a specific library for dependency resolution and being unable to function independently. In the absence of a standard, this hinders code reusability.
The Zero configuration principle
suggests avoiding the use of metadata within the Target
class,
making it independent of any specific DI implementation.
To reduce the coupling of Target
classes,
our library proposes moving dependency metadata knowledge to the contract domain.
In other words, information about how to resolve dependencies can be stored
in an interface rather than in the implementation.
This keeps dependency definitions as transparent as possible.
Example:
declare(strict_types=1);
use IfCastle\DI\DependencyContract;
#[DependencyContract(new DependencyProvider())]
interface InterfaceWithDependencyContact
{
public function someMethod(): void;
}
final readonly class ClassWithDependencyContact
{
public function __construct(
private InterfaceWithDependencyContact $some
) {}
}
In this case, the ClassWithDependencyContact
class is a Target
class,
and the InterfaceWithDependencyContact
interface is a Contract
.
Using the DependencyContract
attribute,
the interface specifies how the dependency should be resolved.
This solution is also not ideal, but in many cases, it results in cleaner code.
See more: Complex use cases
The library does not include any compilers for dependency descriptors
,
although their implementation is possible.
All dependency descriptors are resolved dynamically at application startup using the Reflection API
.
Is this a performance issue?
Yes, if you are using PHP
in a stateless mode, where the PHP process terminates after each request.
For stateful applications, dependency resolution occurs once during the application's warm-up phase
or on-demand
, which aligns with the purpose of this library.
The library consists of four core components that interact with each other:
Container
— a dependency container. A key-value object that stores dependencies, where the key is typically the full name of an interface, and the value is a scalar value, a dependency descriptor, or an already initialized object.Builder
— a container builder that constructs the container based on the specified dependencies.Resolver
— a strategy that directly resolves dependencies.Dependency
— an object that describes a dependency.
All these components are interchangeable, and by modifying them, you can change the behavior.
The dependency container follows the ServiceLocator
/Environment
pattern.
This means that when attempting to resolve a dependency, the dependencies required by it will also be retrieved
from the same container.
In other words,
All dependencies in the
container
share thecontainer
as a common executionenvironment
.
You can leverage this fact to create multiple execution environments, each holding its unique dependencies.
This allows you to implement Scope logic
, where dependency initialization depends on the environment.
By default, the container is an immutable object in terms of associating keys with dependencies. However, the container's values change during execution, as dependencies are initialized on their first use, replacing dependency descriptors with the actual value.
This container behavior ensures that dependencies are single instances, meaning they are the same object.
To provide developers with a powerful tool for managing dependencies,
the container
supports inheritance based on the override principle.
This means you can create two separate containers
with different sets of dependencies and then link them as PARENT and CHILD containers.
use IfCastle\DI\Container;
$parent = new Container(new Resolver(), ['dependency1' => 'value1']);
$child = new Container(new Resolver(), ['dependency1' => 'value2'], $parent);
echo $child->resolveDependency('dependency1'); // value2
In this case, an attempt to resolve a dependency in the child container will result in the following behavior:
- if the dependency is not found in the child container, the search will continue in the parent container.
- however, if the child container has a definition for the dependency, it will be used.
The container supports dereferencing weak references if they are detected as a value.
Using weak references is typically useful when defining multiple aliases for dependencies or a reference to the container itself. In such cases, weak references help avoid additional work for the garbage collector and prevent memory leaks.
Sometimes, it is necessary to initialize a dependency not directly through the constructor but with additional code. For example, loading a database driver based on configuration or context.
To solve this problem, an initializer can be used: a special object executed only once at the moment of dependency resolution.
To implement this approach, the library uses the InitializerInterface
.
If the container holds a value of type InitializerInterface
, the container uses the executeInitializer
method
to obtain the dependency.
See SelfReferenceInitializer as an example of an initializer that resolves a dependency from the container.
Container values can be exception
objects (Throwable
).
When attempting to resolve a dependency whose value is an exception object,
that exception will be thrown.
This applies to both the resolveDependency
method and the findDependency
method.
However, you can change this behavior by specifying the $returnThrowable
flag
for the findDependency
.
Thus,
if a dependency was resolved with an error once, any later attempt to retrieve this dependency will result in the same error without attempting to resolve it again.
The container builder is responsible for constructing the container based on the specified dependencies.
The current implementation uses PHP Reflection
to build the container and supports the following types of dependencies:
- Dependency initialized through a constructor.
- Dependency initialized through a specific method.
- Dependency initialized via an Initializer or a closure.
- Dependency with a ready-to-use object that does not require resolution.
- Constant value.
use IfCastle\DI\ContainerBuilder;
use IfCastle\DI\ContainerInterface;
$builder = new ContainerBuilder();
// Constructible dependencies
$builder->bindConstructible(SomeInterface::class, SomeClass::class);
// Injectable dependencies
$builder->bindInjectable(SomeRequiredInterface::class, SomeRequiredClass::class);
// Bind to Initializer
$builder->bindInitializer(SomeOptionalInterface::class, static function (?ContainerInterface $container = null, array $resolvingKeys = []) {
return $container->resolveDependency(SomeOptionalInterface::class, resolvingKeys: $resolvingKeys);
});
// Bind to Object
$builder->bindObject('stdClass', new stdClass());
// 3. Build the container
$container = $builder->buildContainer(new Resolver());
The builder creates a dependency descriptor where the key for the dependency is the data type. If a parameter has a default value, the dependency is marked as optional. If there is no default value, the dependency is marked as required.
A dependency can have a complex data type defined through PHP UNION
or INTERSECTION
expressions.
In this case, the container will look for the dependency using multiple type keys.
You can override the behavior of the Builder
by implementing the DescriptorProviderInterface
.
There are two ways to do this:
- Implement the
DescriptorInterface::getDescriptorProvider()
method in a custom attribute.
use Attribute;
use IfCastle\DI\Dependency;
use IfCastle\DI\DescriptorInterface;
use IfCastle\DI\DescriptorProviderInterface;
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
final class CustomDescriptor extends Dependency implements DescriptorProviderInterface
{
#[\Override]
public function getDescriptorProvider(): DescriptorProviderInterface|null
{
return $this;
}
#[\Override]
public function provideDescriptor(
DescriptorInterface $descriptor,
\ReflectionClass $reflectionClass,
\ReflectionParameter|\ReflectionProperty $reflectionTarget,
object|string $object,
): DescriptorInterface {
// Custom logic here
}
}
- Use
DependencyContract
on Interface.
use IfCastle\DI\DependencyContract;
// Define custom descriptor provider for the interface
#[DependencyContract(descriptorProvider: new DescriptorProvider())]
interface InterfaceWithDependencyContact
{
public function someMethod(): void;
}
final readonly class ClassWithDependencyContact
{
public function __construct(
private InterfaceWithDependencyContact $some
) {}
}
The DependencyContract
attribute is inherited according to specific rules:
- First,
DependencyContract
is checked in the current class or interface. - If it is not found,
DependencyContract
is checked in the FIRST inherited interface of the class. - If it is still not found,
DependencyContract
is checked further across all first descendants of the interfaces. - If it is not found there either,
DependencyContract
is checked for the child class, and the algorithm loops back.
In other words, the first interface has priority in the DependencyContract inheritance logic.
This inheritance algorithm is chosen to reduce the complexity for developers when searching for the DependencyContract
.
Initialization through the constructor assumes that all dependencies are defined as constructor parameters. In this case, the builder uses the PHP Reflection API to read the constructor's parameter list and their data types.
For method-based injection, a special interface InjectableInterface
and a trait InjectorTrait
are used,
which implement the injection method.
In this case, dependencies are described using attributes above the class properties. The class can be constructed before the dependencies are resolved.
use IfCastle\DI\Dependency;
use IfCastle\DI\InjectableInterface;
use IfCastle\DI\InjectorTrait;
use IfCastle\DI\Lazy;
final class InjectableClass implements InjectableInterface
{
use InjectorTrait;
#[Dependency]
protected UseConstructorInterface $required;
#[Dependency]
protected UseConstructorInterface|null $optional;
#[Dependency] #[Lazy]
protected UseConstructorInterface $lazy;
protected string $data = '';
}
The Resolver component is responsible for the dependency resolution process. It handles finding dependencies of a dependency and initializing the dependency's class.
Let's consider a realistic example of using the library. Our goal is to develop a telemetry component (Prometheus-like) that allows defining counters as dependencies.
declare(strict_types=1);
class DataBase
{
public function __construct(
private string $dsn,
private string $user,
private string $password,
private TelemetryCounterInterface $queryCounter,
private TelemetryCounterInterface $errorCounter,
) {}
}
In the example above, we define two dependencies of type TelemetryCounterInterface
.
TelemetryCounterInterface is a telemetry counter contract that increments over time.
The initialization of such a counter is performed in two stages:
// 1. Look up the registry
$registry = $container->resolveDependency(TelemetryRegistryInterface::class);
// 2. Register the counter
$counter = $registry->getOrRegisterCounter('test', 'some_counter', 'it increases', ['type']);
Let's define the TelemetryCounterInterface
interface:
class TelemetryProvider implements \IfCastle\DI\ProviderInterface
{
public function provide(
ContainerInterface $container,
DescriptorInterface $descriptor,
?DependencyInterface $forDependency = null,
array $resolvingKeys = [],
): mixed
{
// Here, you can use the ReflectionAPI for more complex logic
// to determine counter-attributes
// based on knowledge about the class where they need to be injected.
$forClass = $forDependency->getDependencyName() ?? 'global';
$registry = $container->resolveDependency(TelemetryRegistryInterface::class);
return $registry->getOrRegisterCounter($forClass.'_'.$descriptor->getDependencyProperty());
}
}
#[\IfCastle\DI\DependencyContract(new TelemetryProvider())]
interface TelemetryCounterInterface
{
public function inc(): void;
}
Now we can define telemetry counters in Target
classes, fully hiding their implementation
details as well as the method of dependency resolution from the Target
object.
The library's design is oriented towards usage following the Environment
and Scope
paradigm.
The Container
component acts as the environment
,
defining and limiting the set of dependencies that will be interconnected.
An application can define multiple containers, managing which dependencies will be applied in each case. Using the override mechanism, containers can inherit or override each other's dependencies.
By managing the logic of the ContainerBuilder and Resolver, which are unique to each container,
the developer can modify the dependency resolution logic in different contexts
without altering the code of Target
classes.