PSR-11 dependency injection container for PHP 8.4+. It provides shared-entry caching, reflection autowiring, fresh object creation, DI-aware callable invocation, attribute-based parameter and property injection, PSR-7 request mapping, native lazy objects, virtual proxies, aliases, delegators, external-container bridging, and build-time compiled factory shards.
componenta/di owns runtime dependency resolution. It does not scan an application or choose its configuration providers. Class discovery, provider compilation, deployment cache orchestration, and entry-point bootstrapping belong to the application layer, normally componenta/app.
Property injection is supported only through attributes and attribute handlers. Ahead-of-time compilation produces ordinary factory definitions; runtime reflection remains the fallback for dynamic classes.
composer require componenta/diThe package requires PHP 8.4 or newer. The main runtime dependencies are:
| Package | Purpose |
|---|---|
psr/container |
PSR-11 contracts. |
psr/http-message |
PSR-7 request attributes and DTO mapping. |
componenta/config |
Configuration, environment values, and factory ContainerValue. |
componenta/caster |
#[Cast] and request-value casting. |
componenta/validation |
Optional request DTO validation. |
componenta/reflection |
Cached reflection helpers and PHP 8.4 lazy-object access. |
componenta/priority-list |
Priority-ordered parameter resolver registration. |
componenta/var-export |
PHP configuration cache generation. |
get(string $id)returns the shared, cached entry for an id.make(string $entry, array $params = [])creates a fresh object and does not read or populate the entry cache.call(mixed $callable, array $params = [])resolves missing callable arguments and invokes it.- Constructor and callable arguments can be supplied by name or position in
$params. make(Target::class, ['value' => 'provided'])passesvalueto a constructor or setup method parameter. It does not write an ordinary public property.- Attributed properties are processed by the attribute-handler pipeline.
use App\Logging\FileLogger;
use App\Logging\LoggerInterface;
use App\Service\UserService;
use Componenta\DI\ContainerBuilder;
$container = (new ContainerBuilder())
->addService(LoggerInterface::class, new FileLogger('/var/log/app.log'))
->addAlias('logger', LoggerInterface::class)
->build();
$logger = $container->get('logger');
$first = $container->make(UserService::class, ['userId' => 7]);
$second = $container->make(UserService::class, ['userId' => 7]);
assert($first !== $second);When an id has no explicit binding, the reflection resolver can autowire any eligible class whose constructor parameters can be resolved.
Parameter names are part of the public API because PHP named arguments may use them.
| Contract | Signature | Purpose |
|---|---|---|
Psr\Container\ContainerInterface |
get(string $id), has(string $id) |
Shared service lookup. |
FactoryInterface |
make(string $entry, array $params = []) |
Fresh object creation. |
CallableInvokerInterface |
call(mixed $callable, array $params = []) |
DI-aware invocation. |
CallableResolverInterface |
resolve(mixed $callable) |
Callable normalization. |
CallableExecutorInterface |
resolve(...) and call(...) |
Both callable capabilities. |
LazyObjectFactoryInterface |
makeLazy(string $class, callable $initializer) |
Native lazy ghost creation. |
VirtualProxyFactoryInterface |
makeProxy(string $class, callable $factory) |
Native virtual proxy creation. |
ProxyFactoryInterface |
both lazy methods | A combined lazy-object contract. |
AliasResolverInterface |
resolve, set, has |
Low-level alias management. |
The concrete Container additionally exposes set(), alias(), delegator(), and addContainer() for bootstrap code. Ordinary services should depend on the narrow contract they use.
Container::get($id) uses this order:
- Return a decorated result already cached for the requested id.
- Resolve the requested id to its canonical alias target.
- Enter circular-dependency protection for the canonical id.
- Return a locally cached base entry when present.
- If no local base exists, ask registered external PSR-11 containers.
- If no external container owns the id, run the local entry-resolver chain and cache its base result.
- Apply delegators registered for the requested id and cache the decorated result.
Local entries therefore take precedence over external containers. has() converts only container-level resolution failures to false; programming errors inside resolver code remain visible.
make() resolves aliases but deliberately skips runtime entry caches, external containers, and delegators. It always requires an object result.
ContainerBuilder is the supported assembly API.
| Method | Effect |
|---|---|
addFactory(string $id, callable $factory) |
Register a factory. |
addFactories(array $factories) |
Register factories in bulk. |
addInvokable(string $classOrAlias, ?string $class = null) |
Register an invokable class; the two-argument form also creates an alias. |
addInvokables(array $invokables) |
Register invokables in bulk. |
addAlias(string $alias, string $target) |
Register an alias. |
addAliases(array $aliases) |
Register aliases in bulk. |
| `addDelegator(string $id, callable | string |
addDelegators(array $delegators) |
Register decorators in bulk. |
addService(string $id, mixed $service) |
Register a prebuilt shared value. |
addServices(array $services) |
Register shared values in bulk. |
addParameterResolver(mixed $resolver, int $priority = 0) |
Extend the parameter pipeline. |
replaceParameterResolvers(bool $replace = true) |
Omit built-in parameter resolvers. |
addAttributeHandler(mixed $handler) |
Extend the attribute pipeline. |
replaceAttributeHandlers(bool $replace = true) |
Omit built-in attribute handlers. |
compileFactories(iterable $entries, string $directory, ?ParameterResolverCodeGeneratorRegistry $generators = null, int $maxShardBytes = 131072, string $namespace = 'Componenta\DI\Generated') |
Compile known autowiring roots and their concrete dependency graph into factory shards. |
toArray() |
Export the current configuration. |
build() |
Build a sealed runtime container. |
A normal factory receives Componenta\Config\ContainerValue and the per-resolution context:
$builder->addFactory(
MailerInterface::class,
static fn (ContainerValue $container, array $context): MailerInterface =>
new SmtpMailer($container->get(SmtpConfig::class)),
);ContainerValue implements ContainerInterface and also exposes typed/config-aware lookup helpers.
Definition creates immutable entry descriptions:
use Componenta\DI\Definition\Definition;
$container->set(
ReportService::class,
Definition::autowire(ReportService::class)
->constructor(['format' => 'pdf'])
->method('boot'),
);Available definitions are factory(), autowire(), reference(), and invokable(). A ReferenceDefinition is intended for constructor or setup arguments inside a class definition.
Container::create(Config $config) and ContainerBuilder::configure(Config $config) read ConfigKey::DEPENDENCIES.
| Key | Shape |
|---|---|
ConfigKey::FACTORIES |
`array<string, callable |
ConfigKey::INVOKABLES |
list<class-string> or array<string, class-string> |
ConfigKey::ALIASES |
array<string, string> |
ConfigKey::DELEGATORS |
`array<string, callable |
ConfigKey::SERVICES |
array<string, mixed> |
ConfigKey::PARAMETER_RESOLVERS |
`array<int, class-string |
ConfigKey::PARAMETER_RESOLVERS_REPLACE |
bool |
ConfigKey::ATTRIBUTE_HANDLERS |
`list<class-string |
ConfigKey::ATTRIBUTE_HANDLERS_REPLACE |
bool |
Unknown keys and malformed shapes are rejected with InvalidConfigurationException.
configureFromCache($config, $cache, $baseDir) accepts either a versioned cache envelope or a raw dependency array. When $baseDir is provided, relative paths in compiled factory definitions are resolved against it.
ConfigProvider registers optional casting, current-user, and PSR-7 request resolvers. Componenta application bootstrap can discover it through package metadata.
Property values are written only by registered attribute handlers. Constructor/callable parameters use parameter resolvers; attributes that target both parameters and properties participate in both pipelines.
| Attribute | Target and behavior |
|---|---|
#[Inject] |
Property: resolve by declared class/interface type. |
#[EntryId('id')] |
Parameter/property: resolve an explicit entry id. |
#[Config('path')] |
Parameter/property: read application config. |
#[Env('NAME')] |
Parameter/property: read the environment, with optional default. |
#[Make(Service::class)] |
Parameter/property: create a fresh object. |
#[Init(callable, params)] |
Property: initialize from a callable. |
#[Cast(...)] |
Parameter/property: cast a resolved value. |
#[CurrentUser] |
Parameter/property: inject the request user when its provider is configured. |
#[SetUp('method', params)] |
Class: call a setup method after construction; repeatable. |
#[NoConstructor] |
Class: allocate without running the constructor. |
#[Lazy] |
Class: construct as a native lazy ghost. |
#[Proxy] |
Class or injection point: use a virtual proxy. |
PSR-7 scalar attributes are #[QueryParam], #[PayloadParam], #[Header], #[Cookie], #[RequestAttribute], #[ServerParam], and #[UploadedFile].
Request mappers are #[MapQueryString], #[MapRequestPayload], #[MapHeaders], #[MapCookies], #[MapRequestAttributes], #[MapServerParams], and #[MapUploadedFiles]. They can transform an array or create a class-typed DTO through FactoryInterface::make().
call() accepts closures, global function names, "Class::method" strings, invokable service ids, [object, 'method'], and [class-string, 'method']. Explicit parameters win over resolver output by name or position. Exceptions thrown by the target callable propagate unchanged.
A lazy initializer mutates the uninitialized object it receives. A virtual-proxy factory returns the real backing object:
$lazy = $container->makeLazy(
Service::class,
static function (Service $instance): void {
$instance->__construct();
},
);
$proxy = $container->makeProxy(
Service::class,
static fn (object $proxy): Service => new Service(),
);Factory-bound services are eager unless their factory implements LazyServiceFactoryInterface. Class-level #[Lazy] and #[Proxy] apply to reflection/invokable construction, not arbitrary objects returned by factories.
A parameter resolver implements:
interface ParameterResolverInterface
{
public function supports(ParameterTarget $target): bool;
public function resolveParameter(
ParameterTarget $target,
ParameterResolutionContext $context,
): ?array;
}A successful result is [position, value]; null lets the next resolver try. Higher priorities run first.
An attribute handler implements AttributeHandlerInterface, exposes immutable phase and priority properties, and defines supportsAttribute() plus handle(). Handlers that can emit generated PHP may additionally implement CompilableAttributeHandlerInterface.
The builder seals both extension registries after assembly. Mutating a resolved registry at runtime is rejected.
Known autowiring roots can be compiled into ordinary entries in ConfigKey::FACTORIES. The compiler follows concrete constructor, #[Inject], and #[SetUp] dependencies. Existing services, invokables, and explicitly configured factories keep ownership and are never replaced.
use Componenta\DI\Compile\Autowire\AutowireEntry;
use Componenta\DI\ConfigKey;
use Componenta\DI\ContainerBuilder;
$builder = ContainerBuilder::configure($config);
$compiled = $builder->compileFactories(
entries: [new AutowireEntry(CreateOrder::class, 'application command')],
directory: __DIR__ . '/var/cache/build',
);
$dependencies = $config->get(ConfigKey::DEPENDENCIES, []);
$dependencies[ConfigKey::FACTORIES] = array_replace(
$compiled,
$dependencies[ConfigKey::FACTORIES] ?? [], // explicit factories win
);Each CompiledFactoryDefinition contains a relative shard file, generated class, and factory method. Shards have content-addressed names, are loaded only when one of their entries is first resolved, and are then reused by that container. No source SHA-256 is recalculated during bootstrap. Dynamic classes continue through reflection autowiring.
Application integration normally owns root discovery. componenta/app provides the build-only AutowireEntryContributorInterface flow and recognizes #[Autowire]; Router, CQRS, and boot discovery contribute their known runtime entry classes automatically.
DiCacheGeneratorInterface::generate(array $config, string $path) atomically writes the exact supplied array as PHP. It does not discover classes or compile factories. Runtime entry caches remain inside each Container instance; persistent cache files and OPcache are deployment concerns.
| Exception | Meaning |
|---|---|
NotFoundException |
No entry resolver can handle the id. |
CircularDependencyException |
A resolution cycle was detected. |
ResolutionException |
Object, parameter, property, factory, or constructor resolution failed. |
InvalidConfigurationException |
Configuration or a definition is invalid. |
InvalidCallableException |
A callable cannot be normalized. |
DelegatorException |
A delegator failed. |
All package exceptions implement Componenta\DI\Exception\ExceptionInterface.