Version 3.0 — PSR-11 compatible dependency injection container.
There are three classes, each adding one capability on top of the previous:
Container— plain PSR-11 storage:set()/get()/has()/delete()/lock(). A closure definition is invoked fresh on everyget()call — nothing is cached.Resolver— extendsContainerand adds singleton caching,setFactory()(opt a specific id out of caching),make()(bypass the cache for one call), andbind()(resolve one id to another).Injector— extendsResolverand adds reflection-based autowiring: an id that is neither registered nor bound is constructed automatically if it names an instantiable class.
Pick the class that matches what you need — a Resolver for singleton
services without reflection, an Injector for full autowiring.
- Get and set values, objects, and closures by string key
Resolvercaches closure results as singletonssetFactory()— register a closure so eachget()call returns a fresh instancemake()— resolve an id right now, bypassing the singleton cache for that one callbind()— resolve one id by getting another insteadlock()— prevent a key from being overwritten or deletedInjectorautomatically resolves classes and their dependencies via reflection- Implements
Psr\Container\ContainerInterface
use SugiPHP\Container\Container;
$c = new Container();$c->set('debug', true);
$c->get('debug'); // true
$c->set('dsn', 'mysql:host=localhost;dbname=app');
$c->get('dsn'); // 'mysql:host=localhost;dbname=app'
$c->has('dsn'); // true
$c->delete('dsn');$c->set('db', new PDO($c->get('dsn')));
$c->get('db'); // the PDO instanceContainer invokes a closure definition fresh on every get() call:
$c = new Container();
$c->set('db', function () use ($c) {
return new PDO($c->get('dsn'));
});
$c->get('db') === $c->get('db'); // false — a new instance every callUse Resolver when you want the result cached as a singleton instead:
use SugiPHP\Container\Resolver;
$c = new Resolver();
$c->set('db', function () use ($c) {
return new PDO($c->get('dsn'));
});
$c->get('db') === $c->get('db'); // true — same instance// Register a closure so each get() returns a new instance, even through a Resolver
$c->setFactory('request', function () {
return new Request();
});
$c->get('request') === $c->get('request'); // false — new instance each time$c->set('request', function () {
return new Request();
});
$c->get('request') === $c->get('request'); // true — cached singleton
$c->make('request'); // a fresh Request, this one call only — 'request' is still a singleton afterward
$c->make('missing'); // throws NotFoundException, same as get()set() invokes a stored closure (to produce the definition's value). To make
get() return a closure itself instead, wrap it in another closure — only the
outer one gets invoked:
$c->set('greet', function () {
return function () {
return 'hello';
};
});
$c->get('greet'); // the inner Closure$c->set('env', 'production');
$c->lock('env');
$c->set('env', 'staging'); // throws ContainerException
$c->delete('env'); // throws ContainerException
$c->isLocked('env'); // truebind() makes requesting $id resolve to get()-ing $target instead. No
reflection involved — $target just needs to be resolvable itself.
$c = new Resolver();
$c->set('file.logger', function () {
return new FileLogger();
});
$c->bind('logger', 'file.logger');
$c->get('logger'); // the FileLogger instance from 'file.logger'Use Injector to get autowiring: get() will automatically instantiate
any class that is neither registered nor bound, resolving its constructor
dependencies recursively.
use SugiPHP\Container\Injector;
$c = new Injector();
// No set() calls needed — resolved automatically
$c->get(MyService::class); // instantiated with no args
$c->get(MyController::class); // constructor deps resolved recursivelyAutowired instances are cached as singletons. Explicit set() registrations
always take precedence.
For a given id, Injector::get() resolves in this order:
- An explicit registration —
set()/setFactory(). Always wins; also clears any binding for the same id. - A
bind()-registered binding. - Autowiring the id itself as a class name.
Registering an id explicitly always overrides a binding for that same id, and vice versa — set() clears the id's binding, bind() clears the id's explicit registration.
On an Injector, bind()'s target doesn't need to be registered — an
instantiable class name is autowired automatically:
$c = new Injector();
$c->bind(LoggerInterface::class, FileLogger::class);
$c->get(LoggerInterface::class); // returns an autowired FileLogger instance
$c->get(ServiceWithLogger::class); // LoggerInterface dep resolved automaticallyAutowiring fails with NotFoundException when:
- The class does not exist
- The class is abstract or an interface
- A constructor parameter has a builtin type (scalar, array, etc.) with no default value
- A constructor parameter is variadic
- A constructor parameter has a union or intersection type
// Must still be registered explicitly — scalar arg with no default
$c->set('dsn', 'mysql:host=localhost;dbname=app');
$c->set(PDO::class, fn() => new PDO($c->get('dsn')));| Class | Thrown when |
|---|---|
ContainerException |
Overriding/deleting a locked key, a circular binding, or a circular dependency detected during autowiring |
NotFoundException |
get() called for a key that does not exist, or autowiring fails to resolve a class |
Both implement the corresponding PSR-11 interfaces.