wilkques / container
Requires
- php: >=5.3
- psr/container: >=1.0 <1.1
Requires (Dev)
Suggests
None
Provides
Conflicts
None
Replaces
None
README
English | 繁體中文
A small, dependency-free, PSR-11 compatible dependency injection container,
aligned with Illuminate\Container\Container semantics where practical, kept
compatible back to PHP 5.3.
Upgrading from 4.x? Several methods changed behavior in 5.0, most notably
bind()andget(). Read UPGRADING.md before you upgrade.
How to use
composer require wilkques/container
That's it — no manual require of any file is needed. Composer's files
autoload mechanism (declared in this package's composer.json) loads
src/helpers.php automatically, which defines the global container()
helper used throughout this document and registers the bundled PSR-11
interfaces if the real psr/container package isn't already installed (see
PSR-11 below).
Method
-
registerRegisters an already-built object into the container under the given name. It is a thin wrapper around
instance()— the object you pass is stored as-is and returned as-is on every subsequent resolution (it is effectively always "shared"). Both the single-pair form and the batch array-of-pairs form are supported:container()->register( '<your class name>', new \Your\Class\Name ); // or, a batch of [abstract, concrete] pairs container()->register([ [ '<your class name1>', new \Your\Class\Name1 ], [ '<your class name2>', new \Your\Class\Name2 ], // ... ]);
-
bind($abstract, $concrete = null, $shared = false)Registers a lazy, non-shared factory. The
$concreteclosure (or class name) is not invoked when you callbind()— it is only invoked when the abstract is resolved, and it is invoked fresh on every singlemake()/get()call, returning a new instance each time.The closure is always called as
$concrete($container, $parameters)— its first parameter receives the container itself, no matter what (if anything) it's type-hinted as. It is not autowired by type hint; resolve any dependencies you need explicitly inside the closure body. This applies identically tosingleton()andscoped()below, since both are built on top ofbind():container()->bind(\Your\Class\Config::class, function ($container) { $filesystem = $container->make(\Your\Class\Filesystem::class); return new \Your\Class\Config($filesystem); });
Behavior change from v4: in 4.x, a binding closure's own parameters were autowired by type hint (the same mechanism
call()uses). See UPGRADING.md.Behavior change from v4: in 4.x,
bind()behaved like a singleton — the factory ran once and the same instance was cached and returned forever after. In 5.0,bind()is a true transient/factory binding. If you want the old caching behavior, usesingleton()instead. See UPGRADING.md.container()->bind('<your class name>', function ($container) { return new \Your\Class\Name; }); container()->make('<your class name>'); // new instance container()->make('<your class name>'); // a different, new instance
-
singleton($abstract, $concrete = null)Binds a class or interface into the container that should only be resolved one time. Once a singleton binding is resolved, the same object instance is returned on every subsequent call into the container:
container()->singleton('<your class name>', function ($container) { return new \Your\Class\Name; }); container()->make('<your class name>') === container()->make('<your class name>'); // true
-
scoped($abstract, $concrete = null)Behaves exactly like
singleton(), but the binding is additionally tracked so it can be cleared in bulk withforgetScopedInstances()— handy for per-request state in long-running processes.container()->scoped('<your class name>', function ($container) { return new \Your\Class\Name; });
-
instance($abstract, $instance)The direct, explicit way to register an already-built object as a shared instance — this is what
register()andsingleton()'s resolved cache both end up calling under the hood. Returns the instance you passed in.$object = new \Your\Class\Name; container()->instance('<your class name>', $object); container()->make('<your class name>') === $object; // true
-
alias($abstract, $alias)Registers an alternate name for an existing abstract. Aliases chain (an alias may itself be aliased) and are resolved transparently by
make()/get()/has(). Aliasing an abstract to itself throws aLogicException.container()->instance('<your class name>', new \Your\Class\Name); container()->alias('<your class name>', '<your class alias>'); container()->make('<your class alias>'); // same object as '<your class name>'
-
tag($abstracts, $tags)/tagged($tag)Groups one or more abstracts under one or more tags, and later resolves every abstract registered under a tag in one call.
$tagsmay be a single tag string, an array of tag strings, or extra variadic string arguments.tagged()eagerly resolves every tagged abstract and returns a plain PHP array — never a generator, since this library targets PHP 5.3, which has no generator support.container()->bind('report.csv', function () { return new \Reports\Csv; }); container()->bind('report.pdf', function () { return new \Reports\Pdf; }); container()->tag(['report.csv', 'report.pdf'], 'reports'); foreach (container()->tagged('reports') as $report) { // $report is a resolved \Reports\Csv / \Reports\Pdf instance }
-
extend($abstract, \Closure $closure)Registers a decorator that wraps/modifies an abstract's resolved instance. If the abstract has already been resolved (e.g. it's a singleton that was already built), the closure is applied immediately to the cached instance; otherwise, it's applied the moment the abstract is next built. Multiple extenders on the same abstract are applied in registration order.
container()->bind('<your class name>', function () { return new \Your\Class\Name; }); container()->extend('<your class name>', function ($instance, $container) { return new \Your\Class\Decorator($instance); }); container()->make('<your class name>'); // a \Your\Class\Decorator wrapping \Your\Class\Name
-
resolving($abstract, $callback = null)/afterResolving($abstract, $callback = null)Register lifecycle hooks that run whenever the container resolves an entry —
resolving()runs right after the object is built (before it's cached/returned),afterResolving()runs right after that. Both support two forms:-
Global form — pass only a
\Closure, no abstract, and it fires for every resolution:container()->resolving(function ($object, $container) { // runs for every resolved object });
-
Per-abstract form — pass an abstract name plus a callback, and it fires only when that abstract (or an instance of that type) is resolved:
container()->resolving('<your class name>', function ($object, $container) { // runs only when '<your class name>' is resolved });
Both
resolving()andafterResolving()return$thisfor chaining. For a single resolution, callbacks fire in this order: globalresolving()callbacks, then per-abstractresolving()callbacks, then globalafterResolving()callbacks, then per-abstractafterResolving()callbacks. -
-
when($concrete)->needs($abstractOrParamName)->give($implementation)Contextual binding: override what a specific concrete class receives for one of its constructor dependencies, without changing the global binding for that dependency.
$implementationmay be a\Closure(invoked with the container), a class/interface name (resolved through the container), or any other value (returned as-is) — includingnull,0, or'', all of which are honored as explicit values.Two forms of
needs()are supported, checked in this order whenever the dependency is about to be resolved:- By parameter name —
needs('$paramName')(note the leading$). This is checked first. - By type name —
needs(SomeInterface::class), matching the dependency's declared class/interface type hint. This is checked if no parameter-name binding matched.
// By parameter name: container()->when(\Your\Class\Name::class) ->needs('$connection') ->give('mysql'); // By type name: container()->when(\Your\Class\Name::class) ->needs(\Your\Contract\LoggerInterface::class) ->give(\Your\Class\FileLogger::class); container()->make(\Your\Class\Name::class);
- By parameter name —
-
has($abstract)PSR-11
ContainerInterface::has(). Returnstrueif the container has a binding, a registered instance, or a resolvable alias for$abstract. Does not guaranteeget()/make()will succeed without throwing (a bound factory can still fail at build time).container()->has('<your class name>');
-
get($abstract)PSR-11
ContainerInterface::get(). Resolves and returns the entry.- Throws
Psr\Container\NotFoundExceptionInterface(aWilkques\Container\Exceptions\NotFoundException) if$abstractis not bound/registered and cannot be autowired (e.g. an unknown class name, or an interface/abstract class with no binding). - Throws
Psr\Container\ContainerExceptionInterface(aWilkques\Container\Exceptions\ContainerException, typically aBindingResolutionExceptionorCircularDependencyException) if$abstractis bound but building it fails for some other reason (e.g. its factory throws, or a dependency can't be resolved).
Behavior change from v4: in 4.x,
get()returnednullfor anything it couldn't resolve, instead of throwing. See UPGRADING.md.container()->get('<your class name>');
- Throws
-
make($abstract, $arguments = array())Resolves
$abstract, building it (with constructor autowiring) if needed.$argumentslets you override specific constructor parameters, by name or by position — any keys in$argumentsthat don't correspond to an actual constructor parameter are simply ignored (not passed through as stray positional arguments).container('<your class name>'); // or container()->make('<your class name>'); // override the 2nd constructor parameter by position container()->make('<your class name>', [1 => 'value']); // override a constructor parameter by name container()->make('<your class name>', ['paramName' => 'value']);
-
call($callable, $arguments = array())Calls the given callable, autowiring any parameters not present in
$arguments. Supported forms:[$object, 'method']— callsmethodon exactly the$objectinstance you passed in (it is never rebuilt/re-resolved through the container, even if a singleton for that class already exists).['ClassName', 'method']— ifmethodisstatic, it's invoked directly with no instance constructed at all; otherwise, an instance ofClassNameis built viamake()(so an existing singleton binding is honored) andmethodis invoked on it.'ClassName@method'and'ClassName::method'— both parsed into the['ClassName', 'method']form above and behave identically to it (including the static-vs-instance distinction —'::'does not imply the method must be static).- A
\Closure. - An invokable object (any object with
__invoke(), that isn't itself a\Closure) — its__invoke()method is called.
container()->call(['<your class name>', '<your class method name>'], ['<your class method vars name>' => '<your class method vars value>']); // or, on a specific already-built instance container()->call([new \Your\Class\Name, '<your class method name>'], ['<your class method vars name>' => '<your class method vars value>']); // or container()->call('\Your\Class\Name@<your class method name>'); // or container()->call('\Your\Class\Name::<your class method name>'); // or container()->call(function (\Your\Class\Name $abstract) { // do something });
-
forgetInstance($abstract),forgetInstances(),forgetScopedInstances(),flush()-
forgetInstance($abstract)— removes a single resolved instance from the instance cache.container()->forgetInstance('<your class name>');
-
forgetInstances()— clears every resolved instance from the container. -
forgetScopedInstances()— clears only the instances registered viascoped(). -
flush()— resets the container to a fresh state: every binding, alias, tag, extender, resolving callback, and resolved instance is cleared, and the container then re-registers itself (socontainer()->make(Container::class)keeps working immediately after a flush).flush()returns$thisfor chaining.Behavior change from v4:
flush()used to returnvoid. It now returns$this, and it re-registers the container's own self-binding (v4 lost it after a flush).
-
Exceptions
All container-specific exceptions live under Wilkques\Container\Exceptions:
ContainerException extends \Exception implements Psr\Container\ContainerExceptionInterface— the base exception for any container error.BindingResolutionException extends ContainerException— thrown when the container cannot build/resolve a binding: the target class doesn't exist, isn't instantiable (interface/abstract class with no binding), or one of its dependencies can't be resolved.CircularDependencyException extends BindingResolutionException— thrown when resolving an abstract would require resolving itself again further down the build stack (directly or through a chain of dependencies), instead of exhausting memory.NotFoundException extends ContainerException implements Psr\Container\NotFoundExceptionInterface— thrown only byget(), only when$abstractis not bound/registered and cannot be autowired (the PSR-11 "no entry for this identifier" case).
PSR-11
Wilkques\Container\Container implements Psr\Container\ContainerInterface.
Because this library must keep working on PHP 5.3, it cannot depend on a
psr/container release that requires PHP >= 7.2. composer.json therefore
pins psr/container: >=1.0 <1.1: PSR-11 1.1+ adds a : bool return type to
ContainerInterface::has(), which a PHP-5.3-compatible class cannot declare.
If your application already requires a real psr/container package, this
library uses it as-is (provide: psr/container-implementation: 1.0); if not,
src/helpers.php defines the Psr\Container\ContainerInterface,
ContainerExceptionInterface, and NotFoundExceptionInterface interfaces
itself (only if they don't already exist), so the container is always
PSR-11-typed regardless of what else is installed.
PHP compatibility
Targets php >= 5.3 (see composer.json). Verified:
- PHP 5.3.29 — every file under
src/parses withphp -l(no short arrays,finally,??,::class, or type declarations are used). - PHP 7.0.33 — this is the version most likely to break, since
ReflectionNamedType::getName()did not exist until PHP 7.1 andReflectionTypehad only__toString(). The container guards this withmethod_exists($type, 'getName'). Verified directly: autowiring a class-typed parameter, aself-typed parameter, and aparent-typed parameter (the exact cases that exercise this code path) all resolve correctly, in addition to the general behavior below. - PHP 7.4.33 and PHP 8.2.33 — full behavioral smoke test: interface-to-
implementation binding, circular-dependency detection (throws
CircularDependencyExceptioninstead of exhausting memory), PSR-11has()/get()(includingNotFoundExceptionInterfaceon a missing binding), contextual binding by type name,tag()/tagged(),extend(), and variadic parameter injection via contextualgive()— all pass identically on both versions (and on 7.0).
The full PHPUnit suite (78 tests) runs on PHP 8.2 with PHPUnit 9.6, since no
version of PHPUnit supports both PHP 5.3 and modern PHP simultaneously; PHP
5.3 compatibility of the library source itself is instead guaranteed by the
php -l check above, which is a stronger guarantee than a compatibility
linter for syntax (though it does not, by itself, catch runtime-only API
differences the way the 7.0-specific check above does).