Skip to content
docsv0.2.1

DIContainer

Reflection-autowiring dependency injection container. This is the maximal implementation of {@see DIContainerInterface}: the interface only guarantees resolution of identifiers explicitly registered via {@see registerService()} and documents auto-resolution of {@see get()} and {@see has()} as a MAY, not a MUST. This class exercises that MAY — it additionally auto-wires and caches unregistered-but-existing classes. Concretely, beyond the interface's baseline, this implementation guarantees: - {@see get()} and {@see has()} auto-resolve any existing, non-abstract, non-interface class whose constructor dependencies are themselves resolvable (recursively) — not just identifiers registered via {@see registerService()}. - Auto-resolved classes are cached as singletons on first resolution: subsequent {@see get()} calls for the same identifier return the same instance, unless {@see resolve()} was called directly with `$singleton = false`. - {@see resolve()} detects circular dependency chains (A needs B needs A, directly or transitively) and throws {@see CircularDependencyException} with the full chain of class names that produced the cycle. - {@see tryGet()} never throws: it returns `null` for anything that is not registered and cannot be auto-resolved, instead of propagating {@see ServiceNotFoundException} or {@see ContainerResolutionException}. - Classes found to be non-resolvable (interfaces, abstract classes, classes with unresolvable constructor parameters) are cached as such, so repeated lookups do not re-run reflection.

DIContainer::__construct()

public function __construct():

DIContainer::getContainer()

public function getContainer(): Psr\Container\ContainerInterface

DIContainer::registerService()

public function registerService(string $id, object|string $classOrInstance): void

Registers a service under the given identifier. A class-string is registered on the underlying `ContainerBuilder` as a public service definition (resolved and instantiated on first `get()`); an object is set directly, bypassing definition-based resolution entirely. Either way, this takes precedence over on-demand auto-wiring via {@see resolve()} for the same identifier. Registering the SAME service twice is a no-op: the state does not change, so there is nothing to report. Registering a DIFFERENT one under an id already taken throws {@see ServiceRedefinitionException} instead of replacing it silently — see that class for why failing is the honest placeholder while nothing decides cardinality (ADR-0037). Measured on a real boot before choosing this: 96 registrations, 2 duplicates, and BOTH are the same instance registered twice (`HostBoot` and `Kernel` wiring the same dispatcher and the same tool registry). So no live path registers a different service under a taken id, and this refusal breaks nothing that works today.

Parameters

Parameters of registerService()
NameTypeDescription
$idstringService identifier (usually FQCN)
$classOrInstance(string | object)A class name to register for lazy resolution, or an already-built instance

Throws

ServiceRedefinitionException If `$id` already holds a DIFFERENT service.

DIContainer::replaceService()

public function replaceService(string $id, object|string $classOrInstance): void

Replaces whatever is registered under `$id`, saying so. The escape hatch {@see registerService()} deliberately lacks. Substitution is a legitimate operation — a test installing a double over a plugin's production wiring is the case that forced this method to exist — and the defect was never that it happened. The defect was that it happened WITHOUT SAYING SO, so nobody could tell an intended override from an accident. Replacing something that was never registered is allowed and behaves like a first registration: refusing it would turn "declare your intent" into a rule about ordering, which is a different decision and not one this repair is making.

Parameters

Parameters of replaceService()
NameTypeDescription
$idstringService identifier (usually FQCN)
$classOrInstance(string | object)A class name to register for lazy resolution, or an already-built instance

DIContainer::compileContainer()

public function compileContainer(): void

Compiles the underlying container, freezing its service definitions. Delegates to Symfony `ContainerBuilder::compile()`: runs the builder's compiler passes and locks the definitions in place. Auto-wired services resolved before this call remain cached as singletons; explicit {@see registerService()} calls made after compiling still work (this container does not otherwise restrict post-compile mutation), but compiling is meant to mark the point past which the service graph is considered final.

DIContainer::get()

public function get(string $id): ?mixed

Get a service from the container. If the service is not registered but the class exists, it will be auto-resolved and registered as a singleton.

Parameters

Parameters of get()
NameTypeDescription
$idstringService identifier (usually FQCN)

Returns

Service instance

Throws

ServiceNotFoundException If no entry exists for $id and it cannot be auto-resolved.

ContainerResolutionException If auto-resolution is attempted but fails.

DIContainer::has()

public function has(string $id): bool

Checks whether an identifier is resolvable. True for anything registered via {@see registerService()}. For an unregistered identifier, this implementation additionally returns true when it names an existing, non-abstract, non-interface class whose constructor dependencies are (recursively) resolvable — the auto-wiring guarantee documented on the class. The check never instantiates anything; it walks constructor parameter types to decide resolvability.

Parameters

Parameters of has()
NameTypeDescription
$idstringService identifier

Returns

True if registered or auto-resolvable

DIContainer::tryGet()

public function tryGet(string $id): ?mixed

Get a service or return null if not available.

Parameters

Parameters of tryGet()
NameTypeDescription
$idstringService identifier

Returns

Service instance or null

DIContainer::resolve()

public function resolve(string $className, bool $singleton = true): ?mixed

Resolve a class with auto-wiring.

Parameters

Parameters of resolve()
NameTypeDescription
$classNamestringFully qualified class name
$singletonboolRegister as singleton (default: true)

Returns

Class instance

Throws

CircularDependencyException If resolving $className re-enters itself via its own dependency chain.

ContainerResolutionException If the class does not exist, is abstract/an interface, or a constructor dependency cannot be resolved.