Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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:

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.

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

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.

Public contracts

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.

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.

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.

Definitions

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.

Configuration

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.

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.

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

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,
    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.

Extension points

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.

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.

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.

Exceptions

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages