refactor(di): enhance CyclicDependencyException with detailed chain and cycle context

- Add full dependency chain and pre-cycle segmentation for enhanced error context.
- Improve error messages with clearer formatting, initializer-specific hints, and actionable resolutions.
- Introduce initializer cycle detection to provide targeted solutions for common scenarios.
This commit is contained in:
2025-11-03 18:27:09 +01:00
parent 2b4772a922
commit 6b5aaf47a4

View File

@@ -10,6 +10,8 @@ final class CyclicDependencyException extends ContainerException
{ {
private readonly array $cycle; private readonly array $cycle;
private readonly string $cycleStart; private readonly string $cycleStart;
private readonly array $fullChain;
private readonly array $chainBeforeCycle;
public function __construct( public function __construct(
array $dependencyChain, array $dependencyChain,
@@ -17,6 +19,9 @@ final class CyclicDependencyException extends ContainerException
int $code = 0, int $code = 0,
?\Throwable $previous = null ?\Throwable $previous = null
) { ) {
// Speichere vollständige Kette
$this->fullChain = array_merge($dependencyChain, [$class]);
// Finde wo der Zyklus beginnt (erste Wiederholung in der Kette) // Finde wo der Zyklus beginnt (erste Wiederholung in der Kette)
$cycleStartIndex = array_search($class, $dependencyChain, true); $cycleStartIndex = array_search($class, $dependencyChain, true);
@@ -25,10 +30,13 @@ final class CyclicDependencyException extends ContainerException
$cycle = array_slice($dependencyChain, $cycleStartIndex); $cycle = array_slice($dependencyChain, $cycleStartIndex);
$cycle[] = $class; // Schließe den Zyklus $cycle[] = $class; // Schließe den Zyklus
$cycleStart = $dependencyChain[$cycleStartIndex]; $cycleStart = $dependencyChain[$cycleStartIndex];
// Speichere Teil vor dem Zyklus
$this->chainBeforeCycle = array_slice($dependencyChain, 0, $cycleStartIndex);
} else { } else {
// Fallback: Verwende die gesamte Kette // Fallback: Verwende die gesamte Kette
$cycle = array_merge($dependencyChain, [$class]); $cycle = array_merge($dependencyChain, [$class]);
$cycleStart = $dependencyChain[0] ?? $class; $cycleStart = $dependencyChain[0] ?? $class;
$this->chainBeforeCycle = [];
} }
$this->cycle = $cycle; $this->cycle = $cycle;
@@ -56,14 +64,45 @@ final class CyclicDependencyException extends ContainerException
private function buildMessage(): string private function buildMessage(): string
{ {
$cycleStr = implode(' → ', $this->cycle); $cycleStr = implode(' → ', $this->cycle);
$requestedClass = end($this->fullChain);
// Prüfe ob Initializer-Zyklus vorliegt
$initializerInfo = $this->detectInitializerCycle();
$message = "🔄 Zyklische Abhängigkeit entdeckt:\n\n"; $message = "🔄 Zyklische Abhängigkeit entdeckt:\n\n";
// Zeige Kontext: Wer versucht was zu erstellen
$message .= "❌ Problem: Beim Versuch, '{$requestedClass}' zu erstellen,\n";
$message .= " wurde eine zyklische Abhängigkeit entdeckt.\n\n";
// Zeige vollständige Abhängigkeitskette (wenn vorhanden)
if (!empty($this->chainBeforeCycle)) {
$beforeCycleStr = implode(' → ', $this->chainBeforeCycle);
$message .= "📋 Abhängigkeitskette:\n";
$message .= " {$beforeCycleStr} → [ZYKLUS]\n\n";
}
// Zeige den Zyklus selbst
$message .= "🔄 Zyklus:\n";
$message .= " {$cycleStr}\n"; $message .= " {$cycleStr}\n";
$message .= " ↑─────────────────────┘\n"; $message .= " ↑─────────────────────┘\n";
$message .= " Der Zyklus beginnt hier bei '{$this->cycleStart}'\n\n"; $message .= " Der Zyklus beginnt hier bei '{$this->cycleStart}'\n\n";
// Füge hilfreiche Hinweise hinzu // Spezifische Hinweise für Initializer-Zyklen
$message .= "💡 Lösungsvorschläge:\n"; if ($initializerInfo['isInitializerCycle']) {
$message .= "⚠️ Initializer-Zyklus erkannt!\n\n";
$message .= " Der Initializer '{$initializerInfo['initializerClass']}' benötigt\n";
$message .= " das Interface/abstrakte Klasse '{$this->cycleStart}',\n";
$message .= " welches wiederum den Initializer benötigt.\n\n";
$message .= "🔧 Spezifische Lösung für Initializer-Zyklen:\n";
$message .= " • Initializer sollte das Interface NICHT im Constructor benötigen\n";
$message .= " • Verwende Container als Parameter und hole das Interface erst im __invoke()\n";
$message .= " • Oder: Refactoriere den Initializer, sodass er keine zirkuläre Abhängigkeit hat\n";
$message .= " • Prüfe, ob der Initializer wirklich alle Dependencies braucht\n\n";
}
// Füge allgemeine hilfreiche Hinweise hinzu
$message .= "💡 Allgemeine Lösungsvorschläge:\n";
$message .= " • Verwende Lazy Loading für eine der Abhängigkeiten\n"; $message .= " • Verwende Lazy Loading für eine der Abhängigkeiten\n";
$message .= " • Füge eine Factory zwischen die Klassen ein\n"; $message .= " • Füge eine Factory zwischen die Klassen ein\n";
$message .= " • Refactoriere die Abhängigkeitsstruktur (Dependency Inversion)\n"; $message .= " • Refactoriere die Abhängigkeitsstruktur (Dependency Inversion)\n";
@@ -72,6 +111,44 @@ final class CyclicDependencyException extends ContainerException
return $message; return $message;
} }
/**
* Erkenne ob ein Initializer-Zyklus vorliegt
*
* @return array{isInitializerCycle: bool, initializerClass: string|null}
*/
private function detectInitializerCycle(): array
{
// Prüfe ob die Klasse am Zyklus-Start ein Interface oder abstrakt ist
$isInterfaceOrAbstract = false;
try {
if (interface_exists($this->cycleStart)) {
$isInterfaceOrAbstract = true;
} elseif (class_exists($this->cycleStart)) {
$reflection = new \ReflectionClass($this->cycleStart);
$isInterfaceOrAbstract = $reflection->isAbstract();
}
} catch (\Throwable) {
// Ignoriere Reflection-Fehler
}
if (!$isInterfaceOrAbstract) {
return ['isInitializerCycle' => false, 'initializerClass' => null];
}
// Suche nach Initializer-Klassen in der Kette
// Initializer enden typischerweise auf "Initializer"
foreach ($this->fullChain as $classInChain) {
if (str_ends_with($classInChain, 'Initializer')) {
return [
'isInitializerCycle' => true,
'initializerClass' => $classInChain,
];
}
}
return ['isInitializerCycle' => false, 'initializerClass' => null];
}
/** /**
* Get the detected cycle (without full dependency chain) * Get the detected cycle (without full dependency chain)
*/ */