Skip to main content

Circular Dependency Resolution

Overview

Circular constructor dependencies — where resolving a service transitively requires resolving itself again — are not supported. The container detects the cycle while it is happening and immediately throws a ContainerException naming the full chain, instead of overflowing the call stack or silently producing a broken proxy object.


How It Works

Every call to make() tracks the identifiers currently being resolved:

  • Before building an abstract, the container checks whether that abstract is already present in $resolving. If it is, a cycle has been found.
  • While an abstract is being resolved, its identifier is pushed onto $resolutionStack (in call order) and into the $resolving lookup table.
  • Both are cleared in a finally block once resolution finishes (successfully or not), so a caught exception never leaves stale resolution state behind.

When a cycle is detected, the container looks up where the repeated identifier first appeared in $resolutionStack and throws:

Circular dependency detected: A -> B -> A.

The message always lists the exact chain from the first occurrence of the repeated identifier back to itself, so you can see precisely which classes form the cycle instead of only the two directly involved.


Example Usage

Consider two classes, A and B, that depend on each other:

class A {
public function __construct(B $b) {
$this->b = $b;
}
}

class B {
public function __construct(A $a) {
$this->a = $a;
}
}

$container = new Container();

try {
$container->make(A::class);
} catch (\DomainFlow\Container\Exception\ContainerException $e) {
echo $e->getMessage();
// Circular dependency detected: A -> B -> A.
}

Resolving A requires B, which requires A again — the container throws instead of recursing forever.


Breaking a Real Cycle

If two services genuinely need each other, the cycle has to be broken in your own code, not by the container. Common options:

  • Introduce a factory binding that defers construction of one side until it's actually used, instead of injecting the fully built object through the constructor:
$container->bind(B::class, function (Container $c) {
return new B(fn () => $c->make(A::class));
});
  • Use setter injection for one leg of the cycle instead of constructor injection, so the object graph can be built in two steps.
  • Extract a shared interface or mediator that both classes depend on, removing the direct A ⇄ B relationship entirely.

Summary

Circular constructor dependencies raise a clear ContainerException naming the full dependency chain (for example A -> B -> A) instead of being silently resolved. Use an explicit factory binding, setter injection, or a mediator/interface to break a genuine circular relationship in your own service design.