aura/di
Aura.Di is a serializable dependency injection container for PHP 8+ supporting constructor and setter injection, interface and trait awareness, configuration inheritance, and PSR-11 compliance. It has no external implementation dependencies and installs via Composer as aura/di.
Install with Composer; the package is PSR-4 autoloadable and requires PHP 8.0+:
composer require aura/di
On 5.x, the container constructor requires a Resolver (this changed from 4.x, where you built a container with an injection factory). The smallest useful setup is:
use Aura\Di\Container;
use Aura\Di\Resolver;
$di = new Container(new Resolver());
$di->set('clock', $di->lazyNew(Clock::class));
$clock = $di->get('clock'); // PSR-11 get()
$di->has('clock'); // PSR-11 has()
Look first at docs/index.md and its linked pages, then at changelog/5.0.0.md. The 5.0 changelog is the important one: it introduces attribute-driven configuration, class scanning, and a compile step, and it changes the constructor contract. If you are coming from 4.x or earlier, read docs/migrating.md before copying old snippets.
The first real use case is not a one-off $di->get() call, but a config class. Implement Aura\Di\ContainerConfigInterface::define(Container $di) and keep all wiring for one module in that class. That scales far better than scattering set() calls, and it mirrors the Laravel service-provider habit without relying on Laravel's container.
Config classes as the unit of composition. Each module gets a ContainerConfigInterface implementation. Use ContainerBuilder or ConfigCollection to combine several configs into one container. This is the closest Aura equivalent to Laravel service providers, except there is no separate boot() phase and no deferred-provider machinery: define() is where all wiring happens.
Use params, setters, and values deliberately. Constructor arguments go in $di->params, setter injection in $di->setters, and shared scalar/config values in $di->values. Prefer named parameters ('$dsn') over positional indexes unless you are intentionally overriding. For a consumer class, define only what cannot be inferred:
$di->params[Database::class] = [
'dsn' => $di->lazyValue('db.dsn'),
];
$di->setters[LoggerAware::class] = [
'setLogger' => $di->lazyGet('logger'),
];
Bind interfaces with lazyGet, not direct instances. Keep the interface name as the service key and inject lazies into consumers. That gives you a single place to swap implementations and keeps construction deferred until the consuming service is actually resolved.
Lean on auto-resolution for leaves, be explicit for edges. Aura will auto-resolve class/interface typehints in constructor parameters, but it will not invent scalar values (or arrays and nulls since 3.x). Treat autowiring as a convenience for simple value objects and repositories, and explicitly configure anything with a DSN, key, path, or feature flag. This is the opposite of Laravel's “bind only when you need to” default; here the explicit config is the design.
Master the lazy family. lazyNew, lazyGet, lazyValue, lazyInclude, lazyRequire, lazyArray, lazyCallable, and (5.x) lazyLazy are the tools for deferring resolution. They matter for two reasons: they avoid premature instantiation, and they avoid auto-locking the container while you are still configuring it. Returning a new instance each time versus resolving a shared service is controlled by whether you use newInstance()/lazyNew or a named service via get/lazyGet.
Use the 5.x attribute + compile flow for larger apps. Mark classes and namespaces with #[Blueprint] and #[BlueprintNamespace], use #[Service], #[Instance], and #[Value] for injection, and scan with the bin/auradi executable. For production, compile the container so reflection and class-map generation are done ahead of time rather than on every request. ContainerCompileInterface lets your own config classes hook into compilation if you need custom post-processing.
Integrate with Laravel by keeping ownership clear. Aura is a standalone PSR-11 container, not a drop-in Laravel container replacement. In a Laravel app, build the Aura container inside a service provider and expose the services you need to Laravel via $this->app->instance() or thin bindings. Alternatively, use Aura only inside a framework-agnostic package and let Laravel's container handle the application layer. Do not let both containers independently construct the same stateful service.
Test configs, not just classes. Use AbstractContainerConfigTest and the container assertion traits to verify that each config class defines the expected services, params, and setters. This catches wiring regressions that normal unit tests miss because they never exercise the container.
get() and newInstance() lock the container. If you resolve a service and then try to add params or setters, you will get errors. Use lazy* definitions while configuring, and only resolve at the end of the bootstrap.MissingParam. Even with it enabled, scalars are not guessed. Define every non-class parameter explicitly.has() is your friend in tests and debug tooling. If a service behaves unexpectedly, inspect the relevant config class before digging into the container internals.new Container() is not enough on 5.x; pass a Resolver. The resolver also holds service instances in 5.x, which changes assumptions about where shared state lives.composer/class-map-generator, and classes that error during scanning are excluded from the class map (fixed across 5.0.x). If an attribute comes from an uninstalled external package, 5.0.9 changed behavior to avoid yielding broken attribute data. Keep the scan roots correct, and do not assume every class in src/ is visible to the scanner.#[Service], #[Instance], and #[Value] were changed to allow promoted constructor properties. If you write a custom attribute config, implement AttributeConfigInterface and mark it with #[AttributeConfigFor] (repeatable since 5.0.5).ContainerBuilder wants config class names, not instantiated configs. That has been true since 3.x and is a common source of “why isn't my config running?” confusion.MissingParam means configuration, not code. Then inspect $di->params, $di->setters, and $di->values for the exact class name. For compile-related failures, run the scan command manually and inspect the generated class map before blaming the runtime.AttributeConfigInterface for custom attribute behavior, ContainerCompileInterface for compile hooks, and the lazy classes/serialization machinery for caching. There is no Laravel-style extend()/rebinding callback API, so design extension through config-class composition rather than monkey-patching the container.How can I help you explore Laravel packages today?