From 6b5aaf47a4a216aa8417b10c4d70238f4b905380 Mon Sep 17 00:00:00 2001 From: Michael Schiemer Date: Mon, 3 Nov 2025 18:27:09 +0100 Subject: [PATCH] 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. --- .../Exceptions/CyclicDependencyException.php | 81 ++++++++++++++++++- 1 file changed, 79 insertions(+), 2 deletions(-) diff --git a/src/Framework/DI/Exceptions/CyclicDependencyException.php b/src/Framework/DI/Exceptions/CyclicDependencyException.php index d89d134e..cc148af3 100644 --- a/src/Framework/DI/Exceptions/CyclicDependencyException.php +++ b/src/Framework/DI/Exceptions/CyclicDependencyException.php @@ -10,6 +10,8 @@ final class CyclicDependencyException extends ContainerException { private readonly array $cycle; private readonly string $cycleStart; + private readonly array $fullChain; + private readonly array $chainBeforeCycle; public function __construct( array $dependencyChain, @@ -17,6 +19,9 @@ final class CyclicDependencyException extends ContainerException int $code = 0, ?\Throwable $previous = null ) { + // Speichere vollständige Kette + $this->fullChain = array_merge($dependencyChain, [$class]); + // Finde wo der Zyklus beginnt (erste Wiederholung in der Kette) $cycleStartIndex = array_search($class, $dependencyChain, true); @@ -25,10 +30,13 @@ final class CyclicDependencyException extends ContainerException $cycle = array_slice($dependencyChain, $cycleStartIndex); $cycle[] = $class; // Schließe den Zyklus $cycleStart = $dependencyChain[$cycleStartIndex]; + // Speichere Teil vor dem Zyklus + $this->chainBeforeCycle = array_slice($dependencyChain, 0, $cycleStartIndex); } else { // Fallback: Verwende die gesamte Kette $cycle = array_merge($dependencyChain, [$class]); $cycleStart = $dependencyChain[0] ?? $class; + $this->chainBeforeCycle = []; } $this->cycle = $cycle; @@ -56,14 +64,45 @@ final class CyclicDependencyException extends ContainerException private function buildMessage(): string { $cycleStr = implode(' → ', $this->cycle); + $requestedClass = end($this->fullChain); + + // Prüfe ob Initializer-Zyklus vorliegt + $initializerInfo = $this->detectInitializerCycle(); $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 .= " ↑─────────────────────┘\n"; $message .= " Der Zyklus beginnt hier bei '{$this->cycleStart}'\n\n"; - // Füge hilfreiche Hinweise hinzu - $message .= "💡 Lösungsvorschläge:\n"; + // Spezifische Hinweise für Initializer-Zyklen + 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 .= " • Füge eine Factory zwischen die Klassen ein\n"; $message .= " • Refactoriere die Abhängigkeitsstruktur (Dependency Inversion)\n"; @@ -72,6 +111,44 @@ final class CyclicDependencyException extends ContainerException 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) */