orno/di
Orno\Di is a lightweight, PSR-11 compatible dependency injection container for PHP. It supports constructor and setter injection, autowiring, lazy service resolution, aliases, and shared instances, making it easy to wire dependencies cleanly in small applications and custom frameworks.
Install via Composer and pull in the container:
composer require orno/di
The entry point is Orno\Di\Container. Instantiate one at your application's composition root (front controller, bootstrap file, or a service provider) and pass it down — avoid reaching for a global.
use Orno\Di\Container;
$container = new Container();
Your first useful case is autowiring a class with typed constructor dependencies. If UserRepository needs a PDO, you register nothing but the PDO and let the container build the rest:
$container->add('PDO', function () {
return new PDO('mysql:host=localhost;dbname=app', 'user', 'pass');
});
$repo = $container->get('UserRepository'); // resolved by reflection
Where to look first in the repo: src/Orno/Di/Container.php is the whole API surface. Read the add() and get() methods plus the reflection-based resolver before anything else — the README examples cover the happy path, but the resolution logic is where the real behavior (and the surprises) live. Registering an interface-to-concrete binding is the second thing to try:
$container->bind('UserRepositoryInterface', 'UserRepository');
Interface binding at the composition root. Keep every bind() call in one bootstrap or a dedicated ServiceProvider so you can see the whole wiring map at a glance. This is the single highest-value habit with this container — autowiring is invisible, so the explicit bindings are your documentation.
Factory closures for side-effectful construction. Anything that opens a connection, reads config, or needs a runtime argument should be a closure, not a class name:
$container->add('Mailer', function (Container $c) {
return new Mailer($c->get('Swift_Mailer'), $c->get('config')['from']);
});
The closure receives the container, so you can chain resolutions without hard-coding class names.
Singletons for shared infrastructure. A database connection, an event dispatcher, a logger — register these once and reuse. Check the add() signature for the singleton/shared flag (it differs between versions of this package); if a service is accidentally rebuilt on every get(), you'll see duplicate connections or lost state first.
Service providers for modular apps. Group related bindings into classes that receive the container and register their own services. It keeps bootstrap.php from turning into a wall of add() calls and makes it easy to register only the modules a given request needs.
Constructor injection first, everything else last. This container's autowiring is strongest on constructor type-hints. Reach for setter/method injection or manual get() calls only where a circular or optional dependency forces it. Treat any $container->get() inside a domain class as a smell — it hides a dependency that should be a constructor parameter.
Framework integration. In a legacy or hand-rolled app, resolve your controllers through the container: $container->get($controllerClass) and let autowiring build the dependency tree. This gives you nearly framework-level DI without adopting the framework's container.
string $dsn or int $timeout. Either pass explicit arguments when registering, or add a factory closure. This is the #1 source of confusing "cannot resolve" exceptions.bind() is declarative; if you forget it, you won't find out until get() is called. Write a smoke test that resolves your top-level services on every build.add() returns the same instance on every get() in your installed version. If state is leaking between resolutions — or being needlessly rebuilt — that flag is the first place to look.get()/has() may not match the signatures typehinted by newer libraries. Write a thin adapter class implementing Psr\Container\ContainerInterface around it rather than passing the raw container into third-party code.Container into services is a service-locator anti-pattern and makes tests need the whole wiring graph. Inject the resolved dependencies instead; use the container only at the edges.composer.json, check it runs on your PHP version before building on it, and consider vendoring or forking if you depend on it heavily — the maintenance surface is small and unlikely to change.Container (or wrapping it in a ServiceProvider layer) is the cleanest way to add cross-cutting behavior like lazy proxies or resolution logging without patching the vendor code.How can I help you explore Laravel packages today?