Autowire
AutowireContainer
builds classes from their constructor types. You can use it
on its own or pass an existing PSR-11 container whose entries are checked first.
No external autowiring engine is required.
How It Works
- Already resolved entries are returned from the cache.
- If a delegate was supplied, its entries are looked up before autowiring.
- Concrete classes that still need to be resolved are built from their constructor types.
- Constructor dependencies follow this same order, including optional dependencies.
- Each standalone container has its own cache of successfully constructed services.
Autowiring without registrations
When all dependencies are concrete classes with inferable arguments, no provider or delegate is needed:
use FastForward\Container\AutowireContainer;
final readonly class MessageSender
{
public function __construct(public string $channel = 'default') {}
}
final readonly class NotificationService
{
public function __construct(public MessageSender $sender) {}
}
$container = new AutowireContainer();
$service = $container->get(NotificationService::class);
echo $service->sender->channel; // default
Passing null
explicitly has the same effect. Repeated lookups return the same
successfully constructed instance within that container.
Adding autowiring to an existing container
Pass any object implementing Psr\Container\ContainerInterface
to reuse its
registrations. In this example, a provider-backed container supplies a configured
MessageSender
:
use FastForward\Container\ServiceProvider\ArrayServiceProvider;
use FastForward\Container\ServiceProviderContainer;
$sender = new MessageSender('configured');
$existingContainer = new ServiceProviderContainer(new ArrayServiceProvider([
MessageSender::class => static fn(): MessageSender => $sender,
]));
$container = new AutowireContainer($existingContainer);
$service = $container->get(NotificationService::class);
var_dump($service->sender === $sender); // true
var_dump($container->get(MessageSender::class) === $sender); // true
The configured sender wins even though its class could be autowired. The same
priority applies to direct lookups, nested dependencies and optional object
parameters. NotificationService
is built automatically because the delegate
does not define it. Your framework's existing PSR-11 container can be passed in
the same way.
For several PSR-11 containers, pass new AggregateContainer($first, $second)
;
they are consulted in order before native autowiring. To combine providers,
configuration and containers directly, use container(...)
,
ContainerFactory::create(...)
or ContainerBuilder
; see
Container Helper Function. An existing AggregateContainer
is reused and extended
with autowiring.
Register sources before the first lookup: successful resolutions are cached.
If a delegate's get()
throws a PSR-11 exception, aggregate fallback may try
another source. If every source fails, construction errors are preserved; see
Special Patterns and Advanced Behaviors for the full fallback rules.
Optional Dependencies
Named object dependencies are looked up before their declared defaults, even when the argument is optional:
use Psr\Log\LoggerInterface;
final class Handler
{
public function __construct(public ?LoggerInterface $logger = null) {}
}
If LoggerInterface::class
is registered, its entry is injected. An optional
concrete class is recursively autowired when possible. If the dependency is absent
or its constructor graph requires arguments that cannot be inferred, the declared
default is used. This also applies when the default is an object instead of null
.
A registered null
is a real entry and overrides an object default for a nullable
argument. A nullable argument without a default is still required: `?LoggerInterface
$logger`` does not imply a fallback to ``null
`.
Constructor/factory failures, incompatible registered values and dependency cycles are errors. They do not trigger optional defaults. Cycles include the construction path in the exception; failed resolutions are not cached and may be retried.
When an optional dependency fails
An optional default permits an unavailable dependency. A failure while executing its construction is propagated instead of being replaced with that default:
| Resolution outcome | Result for an optional parameter |
|---|---|
| No entry supplies the requested interface or class | Use the declared default |
| Autowiring cannot infer a required argument in the dependency graph | Use the declared default |
| A constructor, factory or autoloader throws, or an injected value has an incompatible type | Propagate the construction error if no other source resolves the entry |
| Resolution encounters a dependency cycle | Propagate the cycle error if no other source resolves the entry |
| Another aggregate source resolves the entry after a delegated failure | Inject the successfully resolved value |
For example, ?LoggerInterface $logger = null
receives null
when no logger
implementation is available. If a registered logger factory throws while building
the logger, the lookup fails with ContainerException
and preserves the original
cause, unless another source successfully resolves that entry. A cycle also fails
with ContainerException
and includes the resolution path. The same rules apply
when the declared default is a non-null value. An error while evaluating the default
itself is also propagated.
If the application intentionally allows a resource to fail, handle the expected
failure in an explicit factory and return null
for a nullable dependency or an
appropriate fallback object. Catch only failures that the application deliberately
recovers from. This keeps that recovery policy explicit for the affected service.
Supported Parameters
- Required and optional named class/interface types resolve by their type identifier.
selfandparentresolve in the constructor's declaring-class scope, including inherited constructors.- Scalar and untyped parameters use their declared defaults. Required values need an explicit factory for the containing service; parameter names are not service IDs.
- Union, intersection and DNF types use their defaults or require an explicit factory. The resolver does not guess which member of an ambiguous type to inject.
- Variadic parameters receive no implicit entries. Register an explicit factory when a service needs a collection.
- Non-instantiable classes, interfaces, traits and enums need explicit registrations.
Migration from Internal PHP-DI
The public construction APIs stay the same. Applications using PHP-DI directly must declare their own Composer dependency; an installed PHP-DI container can still be supplied as a normal PSR-11 source.
The intentional behavior changes are optional object injection before defaults,
preservation of the first delegated construction error when aggregate fallback is
exhausted, and entry discovery through has()
without attempting construction.
Previously construction failures could appear as a fresh not-found exception.
AutowireContainer::has()
now checks whether the composition knows an entry,
without calling get()
. Native autowiring checks class metadata: a class with a
required scalar argument or a broken constructor can therefore return true
from
has()
and still throw a construction error from get()
. A false
result
means the entry is unknown and get()
throws NotFoundExceptionInterface
.
This follows PSR-11. Use get()
with exception handling when you need to attempt
construction; has()
is not a construction health check. Supplied delegates
retain their own has()
implementation.
The native resolver supports constructor injection. It does not implement PHP-DI's definition DSL, attribute/property/setter injection, proxies or compilation.