A dependency’s type often does not determine which instance it needs. Several implementations or instances can
share one interface: every database connection is a Connection. A parameter typed against that interface says
what it needs, but not which one, so the choice has to be expressed somewhere else.
What is known about the instances varies. Sometimes every concrete, and what maps to it, is known when the panel boots. Sometimes the concrete is only decided at runtime, from configuration or other state, by the subsystem that owns the instances.
The panel is dynamic and built for one purpose, and third-party modules consume what its subsystems provide. A module cannot be expected to know the exact final concrete it needs, especially when configuration can change it.
A dependency says which instance it needs with an attribute on its own parameter, and what acts on that attribute
depends on what is known. Where every concrete and its mapping are known at boot, a Named attribute maps a string,
or a qualifier attribute maps its class, to a binding registered for the type, and a factory on that binding
produces the instance where construction needs to be dynamic. Where the concrete depends on configuration or other
runtime state, a resolvable attribute is paired with a resolver belonging to the subsystem that owns the instances,
and the resolver decides the instance entirely, with nothing about it known to the bindings. The consuming class
plays no part in the choice, and the correct bindings and resolvers are trusted to exist.
Contextual binding by consuming class, as in Laravel’s container, where registration states which concrete a given consuming class receives for a dependency. It requires knowing the exact final concrete for each consumer, which a third-party module cannot be expected to know, particularly when configuration can change it. In a system this dynamic and built for one purpose, it would add a great deal of complication, where the correct bindings and resolution can instead be trusted to exist.
Contextual attributes, as in Laravel’s container. Their shape comes from being added to an existing system under
a strict backwards compatibility policy. A contextual attribute is either paired with a resolver closure registered
for it, or carries a resolve() method, which receives the attribute instance as a redundant argument. The panel’s
author built the original implementation of the idea and co-authored the one Laravel merged, which shipped both
forms together. A resolvable attribute paired with a resolver class is a more structured implementation of the same
idea, without the constraint of fitting an existing system.
No other alternatives were weighed.
Easier:
A dependency asks for what it needs on its own parameter, and neither its consuming class nor whatever registers it has to know the concrete:
public function __construct(
#[Named('secondary')] private Contract $contract,
#[Database('replica')] private Connection $connection,
) {}
Harder:
Constrained:
created.Resolvable, Resolver,
ResolverRegistry and ResolverCatalogue.Ghost
and GhostResolver as the first resolvable pair: decided. When the decision was taken is not recorded.Database attribute and
DatabaseResolver, taken from what followed rather than anticipated.#[Manifest] and #[Registrar], #[Provide] with ProviderResolver, and
#[Channel] composed with module scope, taken from later designs rather than anticipated.