Every class in src/ is marked either @api or @internal.
@api — covered by semantic versioning. These will not break within a major
version:
| Class | |
|---|---|
Container |
the container itself (final) |
ContainerInterface |
the contract to type-hint against |
FullContainerInterface |
deprecated empty alias of ContainerInterface; removed at 3.0 |
ContextualBindingBuilder |
returned by when() |
CompilationReport, CompilationSkipReason |
returned by compileReport() |
ValidationReport, ValidationIssue, ValidationProblem |
returned by validate() — see performance |
ClassSource |
where the compile calls find their classes — see performance |
Console\Cli |
vendor/bin/gacela-container, @internal — see performance |
DependencyNode |
returned by dependencyGraph() |
Attribute\Inject, Attribute\Singleton, Attribute\Factory, Attribute\Lazy |
the PHP 8 attributes |
Exception\ContainerException |
|
Exception\CircularDependencyException |
|
Exception\DependencyNotFoundException |
|
Exception\DependencyInvalidArgumentException |
For the exceptions, the class and its PSR-11 interface are stable. Exception messages are not — do not parse or assert on them.
@internal — not covered. AliasRegistry, BindingResolver,
DefinitionLoader, DependencyCacheManager, DependencyResolver,
DependencyTreeAnalyzer, FactoryManager, FuzzyMatcher, InstanceRegistry,
PlanRegistry, and TagRegistry are implementation details of Container. They may change
signature, behaviour, or disappear entirely in any release, including a
patch. Do not import them.
Every instance method below is declared on ContainerInterface. Type-hint
it and you get the whole surface — there is nothing left that only the concrete
class can do:
public function __construct(
private ContainerInterface $container,
) {
}It also extends ArrayAccess, so $c[Id::class], isset(), assignment and
unset() are part of the contract rather than a concrete-class convenience.
createScope() is typed static, so a scope of a decorator is a decorator.
Through 1.x the interface was frozen — nothing could be added to it — which left
twelve methods reachable only through final class Container, and 1.5 answered
that additively with FullContainerInterface. 2.0 merged them.
FullContainerInterface survives as a deprecated empty alias, so a 1.5
type-hint keeps compiling and keeps accepting a Container; it is removed at
3.0. Nothing is added to ContainerInterface within 2.x.
What remains on Container alone is what an instance contract cannot express:
the constructor, and the static methods create(), loadCompiledCache(),
loadCompiledFactories() and resetStaticCaches() — there is no instance that
owns that state to ask, and so nothing for a decorator to forward.
Two caveats. getStats() is on the interface, but the shape of the array it
returns is not covered by backward compatibility — treat it as debug output,
or use stats(), whose shape is covered. And the memory figure in both is the
whole PHP process, not the container; every other field is container-scoped,
so it is the one number that does not answer "what does this container hold".
See introspection.
| Method | Description |
|---|---|
get(string $id): mixed |
Retrieve or create a service |
getOrFail(string $id): mixed |
Like get(), but throws when the id resolves to null |
make(string $className, array $parameters = []): object |
Resolve a class to a typed, non-null instance; $parameters override constructor args by name |
has(string $id): bool |
PSR-11: whether get() will resolve the id — includes autowirable classes |
afterResolving(string $id, Closure $callback): void |
Run a callback after the id is resolved ($instance, $container). A class/interface id also matches nested dependencies before the receiving constructor runs; any other id matches exactly |
bound(string $id): bool |
Whether the id was explicitly registered — a binding or a stored instance (alias-aware) |
provides(string $id): bool |
Whether this container or an ancestor already owns the id — a binding, a stored instance, or a resolved singleton |
bindIf(string $abstract, string|callable|object $concrete): void |
Bind only if not already bound |
singletonIf(string $abstract, string|callable|object|null $concrete = null): void |
Singleton-bind only if not already bound |
bind(string $abstract, string|callable|object $concrete): void |
Register a binding after construction |
singleton(string $abstract, string|callable|object|null $concrete = null): void |
Register a binding resolved once and reused |
lazy(string $abstract, string|callable|null $concrete = null): void |
Register a binding whose construction is deferred to first use — see bindings |
load(array $definitions, ?callable $onRegistered = null): array |
Register services from a definitions array; returns the ids it registered — see definitions |
loadFile(string $file, ?callable $onRegistered = null): array |
Load definitions from a .php file returning an array, a .json file, or a .yaml/.yml one when a YAML parser is installed; returns the ids it registered |
set(string $id, mixed $instance): void |
Register a service |
remove(string $id): void |
Remove a stored service or evict a locally cached singleton |
forget(string $id): void |
Drop what was built for a service, including cached singleton values, and keep how to build it: a closure-registered service is built again on the next get(), a factory() is left as it is. On Container only, not on ContainerInterface, which does not grow within 2.x; a wrapper forwards it by hand |
resolve(callable $callable, array $parameters = []): mixed |
Execute a callable with dependency injection; $parameters override args by name |
factory(Closure $instance): Closure |
Mark a service as a factory (new instance each time) |
extend(string $id, Closure $instance): Closure |
Wrap/modify a service |
protect(Closure $instance): Closure |
Prevent closure execution |
getRegisteredServices(): array |
Get all service IDs |
isFactory(string $id): bool |
Check if a service is a factory |
isFrozen(string $id): bool |
Check if a service is frozen |
getBindings(): array |
Get all bindings |
warmUp(array $classNames): void |
Pre-resolve dependencies. Takes a list only: warming resolves, so it deliberately does not accept a ClassSource |
compile(array $classNames): array |
Warm up and return compiled constructor plans. Container also takes a ClassSource, which describes instead of warming |
writeCompiledCache(array $classNames, string $file): void |
Compile plans and write them to a PHP cache file, fingerprinted against the files they came from — see staleness. Container takes an optional third ?string $buildStamp |
writeCompiledFactories(array $classNames, string $file, ?string $buildStamp = null): array |
Generate new expressions for statically-decidable classes; returns those compiled |
withSelfReference(ContainerInterface $facade): self |
Hand $facade to service closures instead of this container — for decorators |
validate(array|ClassSource $classNames): ValidationReport |
Prove these classes resolve without resolving them |
compileReport(array $classNames): CompilationReport |
What the generator makes of these classes, and why it refuses the rest — see performance. Container also takes a ClassSource |
useCompiledFactories(array $factories): void |
Use generated factories as a fast path |
alias(string $alias, string $id): void |
Create an alias for a service |
tag(string|array $ids, string $tag): void |
Group service ids under a tag (accumulates, dedupes); a map gives entries keys |
tagged(string $tag): iterable |
Lazily resolve all services under a tag, in insertion order; keyed entries under their key |
taggedByKey(string $tag, string $key): mixed |
Resolve the one entry under $key; throws naming the known keys if there is none — see tags |
taggedKeys(string $tag): array |
The keys a tag can be asked for, in insertion order |
dependencyGraph(string $className): DependencyNode |
The dependency graph as a tree — depth, the parameter each child satisfies, and cycles marked rather than thrown — see cookbook |
createScope(): static |
A child container inheriting this one's registration without copying it — see scopes |
stats(): ContainerStats |
Container statistics as a readonly object; shape is covered by BC. Its processMemoryBytes is process memory, not this container's — see introspection |
getStats(): array |
Same numbers as an array (return shape is not covered by BC). Superseded by stats() |
getDependencyTree(string $className): array |
Every class a given class depends on, flat and deduplicated — despite the name, a list rather than a tree. Use dependencyGraph() for structure |
when(string|array $concrete): ContextualBindingBuilder |
Define contextual bindings for specific classes (needs() accepts a type or a $paramName) |
offsetGet / offsetSet / offsetExists / offsetUnset |
ArrayAccess with string or integer offsets: lookup, assignment, isset(), unset(). See array access |
// Quick instantiation without container setup
$instance = Container::create(YourClass::class);
// Clear the reflection caches shared by every container in the process.
// See performance.md#process-global-caches for what is and is not cleared
Container::resetStaticCaches();
// Load compiled constructor plans from a cache file. Entries whose class file
// changed since the cache was written are dropped and fall back to reflection
$plans = Container::loadCompiledCache(__DIR__ . '/cache/container.php');
$container = new Container($bindings, [], $plans);
// Same, for generated `new` expressions
$container->useCompiledFactories(
Container::loadCompiledFactories(__DIR__ . '/cache/factories.php'),
);Both take an optional second argument, a build stamp: pass the same value used
when the file was written and the whole file is validated in one comparison
instead of one stat per class. See staleness.
public function __construct(
array $bindings = [], // abstract => concrete map
array $instancesToExtend = [], // id => list of extension closures
array $compiledPlans = [], // from writeCompiledCache() / loadCompiledCache()
?PlanCache $planCache = null, // one plan cache shared with unrelated containers
)PlanCache shares reflection output — constructor plans — between containers
that have no parent/scope relationship, and nothing else: bindings, contextual
bindings, instances and singletons stay private to each container. See
performance.
PlanCache::fromFile($file, ?$buildStamp) and $planCache->writeTo($file, ?$buildStamp)
carry it from one process to the next using the same non-empty identifier of the loaded release. Unstamped writes persist only native internal-class plans; user-class plans remain in memory. See
performance.
All four parameters are optional and part of the stable API — their names, order, types, and defaults do not change within a major version. Named arguments are safe.
Container cannot be subclassed. Extend behaviour by composition instead of
inheritance: type-hint an interface and wrap.
Implement ContainerInterface: it declares the whole surface, so the compiler
checks the forwarders for you — a method the wrapper forgets is a compile error
rather than one that silently goes missing at runtime. A wrapper should also
call withSelfReference() on the container it
wraps, so service closures receive the wrapper rather than the inner container.
final class LoggingContainer implements ContainerInterface
{
public function __construct(
private ContainerInterface $inner,
private LoggerInterface $logger,
) {}
public function get(string $id): mixed
{
$this->logger->debug('resolving', ['id' => $id]);
return $this->inner->get($id);
}
// delegate the rest to $this->inner
}For per-service changes you usually do not need a wrapper at all — prefer
extend() to decorate a single binding, or
afterResolving() to run code after an id resolves.
| Attribute | Target | Description |
|---|---|---|
#[Inject(class-string)] |
parameter | Inject a specific implementation |
#[Singleton] |
class | Instantiate once and reuse |
#[Factory] |
class | Create a fresh instance every time |
#[Lazy] |
class | Defer construction until first use (PHP 8.4+; eager on 8.3) |
See attributes for examples.