Skip to content

Autowiring: resolve union and intersection parameter types in DiContainer #651

Open

Description

@armanist

Problem

Quantum\Di\DiContainer::resolveParameter() reads a parameter type only when it is a ReflectionNamedType. For a union (A|B) or an intersection (Reader&Writer) type the type is ignored, so the parameter falls through to the passed arguments, then the default value, then null.

Result (checked on PHP 8.1):

  • public function __construct(A|B $dep) fails with a TypeError that gives no hint about the container.
  • public function __construct(A|B|null $dep) silently receives null, even when A is registered in the container.
  • Reader&Writer is never resolved, even when an implementing class is registered.

Proposal

Resolve a union or intersection parameter only when the choice is unambiguous. Never guess from the declaration order and never pass null silently.

  1. Union: drop null and builtin members. If exactly one remaining member can be resolved (it is registered in the container, or it is an instantiable class), use it. If several can, do not pick one.
  2. Intersection: use a registered or instantiable member and check that the result satisfies every part of the type (instanceof for each). A result that does not satisfy all parts is not used.
  3. Otherwise: keep the existing order (passed argument, default value). If neither exists, throw a DiException with a clear message (new named constructor and message template in src/Di/Enums/ExceptionMessages.php) that names the class, the parameter and the type, instead of a TypeError or a silent null.
  4. The same rules apply to autowire() of a closure, which shares resolveParameter().

Why not the order of declaration

Other containers differ: one rejects union types (Marko, a clear exception), one tries members in declaration order (Tempest, which also does not check that an intersection is satisfied as a whole), one tries each branch and verifies it (Canvas). Declaration order makes A|B and B|A behave differently, so it is not used here.

Acceptance

  • Tests in tests/Unit/Di/ for: union with one registered member, union with none, union with several resolvable members, A|B|null with and without a default, intersection with a registered implementation that satisfies all parts, intersection that does not, closure autowiring.
  • CHANGELOG.md entry. A parameter that failed before with a TypeError now resolves or throws a DiException; the exception type changes, so it is noted as a behavior change.

Milestone 3.2.0 (a different exception type on a path that used to throw TypeError).

Activity

  1. added this to the 3.2.0 milestone on Nov 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions