Skip to content

Repository files navigation

Componenta DI

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.

English | Russian

Package boundary

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.

Installation

composer require componenta/di

The package requires PHP 8.4 or newer. The main runtime dependencies are:

PackagePurpose
psr/containerPSR-11 contracts.
psr/http-messagePSR-7 request attributes and DTO mapping.
componenta/configConfiguration, environment values, and factory ContainerValue.
componenta/caster#[Cast] and request-value casting.
componenta/validationOptional request DTO validation.
componenta/reflectionCached reflection helpers and PHP 8.4 lazy-object access.
componenta/priority-listPriority-ordered parameter resolver registration.
componenta/var-exportPHP configuration cache generation.

Core behavior

  • 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']) passes value to a constructor or setup method parameter. It does not write an ordinary public property.
  • Attributed properties are processed by the attribute-handler pipeline.

Quick start

useApp\Logging\FileLogger;
useApp\Logging\LoggerInterface;
useApp\Service\UserService;
useComponenta\DI\ContainerBuilder;
$container = (newContainerBuilder())
->addService(LoggerInterface::class, newFileLogger('/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.

Public contracts

Parameter names are part of the public API because PHP named arguments may use them.

ContractSignaturePurpose
Psr\Container\ContainerInterfaceget(string $id), has(string $id)Shared service lookup.
FactoryInterfacemake(string $entry, array $params = [])Fresh object creation.
CallableInvokerInterfacecall(mixed $callable, array $params = [])DI-aware invocation.
CallableResolverInterfaceresolve(mixed $callable)Callable normalization.
CallableExecutorInterfaceresolve(...) and call(...)Both callable capabilities.
LazyObjectFactoryInterfacemakeLazy(string $class, callable $initializer)Native lazy ghost creation.
VirtualProxyFactoryInterfacemakeProxy(string $class, callable $factory)Native virtual proxy creation.
ProxyFactoryInterfaceboth lazy methodsA combined lazy-object contract.
AliasResolverInterfaceresolve, set, hasLow-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.

Resolution lifecycle

Container::get($id) uses this order:

  1. Return a decorated result already cached for the requested id.
  2. Resolve the requested id to its canonical alias target.
  3. Enter circular-dependency protection for the canonical id.
  4. Return a locally cached base entry when present.
  5. If no local base exists, ask registered external PSR-11 containers.
  6. If no external container owns the id, run the local entry-resolver chain and cache its base result.
  7. 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

ContainerBuilder is the supported assembly API.

MethodEffect
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, callablestring
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,
staticfn (ContainerValue$container, array$context): MailerInterface =>
newSmtpMailer($container->get(SmtpConfig::class)),
);

ContainerValue implements ContainerInterface and also exposes typed/config-aware lookup helpers.

Definitions

Definition creates immutable entry descriptions:

useComponenta\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.

Configuration

Container::create(Config $config) and ContainerBuilder::configure(Config $config) read ConfigKey::DEPENDENCIES.

KeyShape
ConfigKey::FACTORIES`array<string, callable
ConfigKey::INVOKABLESlist<class-string> or array<string, class-string>
ConfigKey::ALIASESarray<string, string>
ConfigKey::DELEGATORS`array<string, callable
ConfigKey::SERVICESarray<string, mixed>
ConfigKey::PARAMETER_RESOLVERS`array<int, class-string
ConfigKey::PARAMETER_RESOLVERS_REPLACEbool
ConfigKey::ATTRIBUTE_HANDLERS`list<class-string
ConfigKey::ATTRIBUTE_HANDLERS_REPLACEbool

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.

Attributes

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.

AttributeTarget 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().

Callable invocation

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.

Lazy objects and proxies

A lazy initializer mutates the uninitialized object it receives. A virtual-proxy factory returns the real backing object:

$lazy = $container->makeLazy(
Service::class,
staticfunction (Service$instance): void {
$instance->__construct();
},
);
$proxy = $container->makeProxy(
Service::class,
staticfn (object$proxy): Service => newService(),
);

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.

Extension points

A parameter resolver implements:

interface ParameterResolverInterface
{
publicfunctionsupports(ParameterTarget$target): bool;
publicfunctionresolveParameter(
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.

Production compiled factories

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.

useComponenta\DI\Compile\Autowire\AutowireEntry;
useComponenta\DI\ConfigKey;
useComponenta\DI\ContainerBuilder;
$builder = ContainerBuilder::configure($config);
$compiled = $builder->compileFactories(
entries: [newAutowireEntry(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.

Exceptions

ExceptionMeaning
NotFoundExceptionNo entry resolver can handle the id.
CircularDependencyExceptionA resolution cycle was detected.
ResolutionExceptionObject, parameter, property, factory, or constructor resolution failed.
InvalidConfigurationExceptionConfiguration or a definition is invalid.
InvalidCallableExceptionA callable cannot be normalized.
DelegatorExceptionA delegator failed.

All package exceptions implement Componenta\DI\Exception\ExceptionInterface.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages