diff --git a/src/Framework/DI/Exceptions/CyclicDependencyException.php b/src/Framework/DI/Exceptions/CyclicDependencyException.php index 952a6105..7bc9553f 100644 --- a/src/Framework/DI/Exceptions/CyclicDependencyException.php +++ b/src/Framework/DI/Exceptions/CyclicDependencyException.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace App\Framework\DI\Exceptions; +use App\Framework\DI\Initializer; +use App\Framework\Discovery\Results\DiscoveryRegistry; use App\Framework\Exception\ExceptionContext; final class CyclicDependencyException extends ContainerException @@ -64,8 +66,8 @@ final class CyclicDependencyException extends ContainerException private function buildMessage(): string { $cycleStr = implode(' → ', $this->cycle); - - $requestedClass = array_last($this->fullChain); + $requestedClass = $this->fullChain[count($this->fullChain) - 1] ?? $this->cycleStart; + $firstClass = $this->fullChain[0] ?? $this->cycleStart; // Prüfe ob Initializer-Zyklus vorliegt $initializerInfo = $this->detectInitializerCycle(); @@ -73,33 +75,66 @@ final class CyclicDependencyException extends ContainerException $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"; + if ($firstClass !== $requestedClass) { + $message .= "❌ Problem: '{$firstClass}' benötigt '{$requestedClass}',\n"; + $message .= " aber eine zyklische Abhängigkeit wurde entdeckt.\n\n"; + } else { + $message .= "❌ Problem: Beim Versuch, '{$requestedClass}' zu erstellen,\n"; + $message .= " wurde eine zyklische Abhängigkeit entdeckt.\n\n"; } - // Zeige den Zyklus selbst - $message .= "🔄 Zyklus:\n"; - $message .= " {$cycleStr}\n"; - $message .= " ↑─────────────────────┘\n"; - $message .= " Der Zyklus beginnt hier bei '{$this->cycleStart}'\n\n"; + // Zeige vollständige Abhängigkeitskette mit Zwischenschritten für Initializer-Zyklen + if ($initializerInfo['isInitializerCycle'] && $initializerInfo['initializerClass'] !== null) { + $message .= $this->buildInitializerChainMessage($firstClass, $initializerInfo); + } else { + // Standard-Darstellung ohne Initializer-Details + 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"; + } // 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"; + + // Finde welche Dependency das Interface benötigt + $problematicInfo = $this->findProblematicDependency( + $initializerInfo['initializerDependencies'] ?? [], + $this->cycleStart + ); + + $problematicDependency = $problematicInfo['problematicDependency']; + + if ($problematicDependency !== null) { + $message .= " ✅ Problem-Dependency identifiziert: '{$problematicDependency}'\n\n"; + $message .= " Das Problem: '{$problematicDependency}' (eine Dependency des Initializers)\n"; + $message .= " benötigt im Constructor '{$this->cycleStart}',\n"; + $message .= " wodurch der Zyklus entsteht:\n"; + $message .= " Initializer → {$problematicDependency} → {$this->cycleStart} → Initializer\n\n"; + } else { + $message .= " Der Initializer '{$initializerInfo['initializerClass']}' benötigt\n"; + $message .= " das Interface/abstrakte Klasse '{$this->cycleStart}',\n"; + $message .= " welches wiederum den Initializer benötigt.\n"; + $message .= " (Konnte die problematische Dependency nicht eindeutig identifizieren)\n\n"; + } + $message .= "🔧 Spezifische Lösung für Initializer-Zyklen:\n"; $message .= " • Initializer sollte das Interface NICHT im Constructor benötigen\n"; + if ($problematicDependency !== null) { + $message .= " • '{$problematicDependency}' sollte '{$this->cycleStart}' NICHT im Constructor benötigen\n"; + $message .= " • Verwende Lazy Loading für '{$problematicDependency}' → '{$this->cycleStart}'\n"; + $message .= " (z.B. über Container->get() statt Constructor-Injection)\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"; + $message .= " • Oder: Refactoriere den Initializer, sodass er keine zirkuläre Abhängigkeit hat\n\n"; } // Füge allgemeine hilfreiche Hinweise hinzu @@ -112,10 +147,159 @@ final class CyclicDependencyException extends ContainerException return $message; } + /** + * Erstelle detaillierte Kette für Initializer-Zyklen + */ + private function buildInitializerChainMessage(string $firstClass, array $initializerInfo): string + { + $initializerClass = $initializerInfo['initializerClass']; + $dependencies = $initializerInfo['initializerDependencies'] ?? []; + $interface = $this->cycleStart; + + $message = "📋 Vollständige Abhängigkeitskette:\n\n"; + $message .= " 1. '{$firstClass}' benötigt\n"; + $message .= " 2. '{$interface}' (Interface)\n"; + $message .= " ↓ wird erstellt durch Initializer\n"; + $message .= " 3. '{$initializerClass}' (Initializer)\n"; + + if (!empty($dependencies)) { + $message .= " ↓ benötigt folgende Dependencies:\n"; + + // Finde problematische Dependency für Hervorhebung + $problematicInfo = $this->findProblematicDependency($dependencies, $interface); + $problematicDependency = $problematicInfo['problematicDependency']; + + foreach ($dependencies as $index => $dependency) { + $isLast = $index === count($dependencies) - 1; + $arrow = $isLast ? '└─→' : '├─→'; + $highlight = ($dependency === $problematicDependency) ? ' ⚠️ (benötigt Interface!)' : ''; + $message .= " {$arrow} '{$dependency}'{$highlight}\n"; + } + } + + if ($problematicDependency !== null) { + $message .= " ↓ '{$problematicDependency}' benötigt wieder\n"; + } else { + $message .= " ↓ eine dieser Dependencies benötigt wieder\n"; + } + $message .= " 4. '{$interface}' (ZYKLUS!)\n\n"; + + return $message; + } + + /** + * Finde welche Dependency das Interface benötigt (durch Analyse der dependency chain und Reflection) + * + * @return array{problematicDependency: string|null, confirmationMethod: string} + */ + private function findProblematicDependency(array $initializerDependencies, string $interface): array + { + // Methode 1: Prüfe welche Dependency in der dependency chain vor dem Interface vorkommt + foreach ($this->chainBeforeCycle as $classInChain) { + if (in_array($classInChain, $initializerDependencies, true)) { + // Bestätige durch Reflection, dass diese Klasse das Interface benötigt + if ($this->dependencyNeedsInterface($classInChain, $interface)) { + return [ + 'problematicDependency' => $classInChain, + 'confirmationMethod' => 'dependency_chain', + ]; + } + } + } + + // Methode 2: Prüfe auch im Zyklus selbst (vor dem Interface) + foreach ($this->cycle as $classInCycle) { + if ($classInCycle !== $interface && in_array($classInCycle, $initializerDependencies, true)) { + if ($this->dependencyNeedsInterface($classInCycle, $interface)) { + return [ + 'problematicDependency' => $classInCycle, + 'confirmationMethod' => 'dependency_chain', + ]; + } + } + } + + // Methode 3: Analysiere alle Dependencies des Initializers über Reflection + foreach ($initializerDependencies as $dependency) { + if ($this->dependencyNeedsInterface($dependency, $interface)) { + return [ + 'problematicDependency' => $dependency, + 'confirmationMethod' => 'reflection_analysis', + ]; + } + } + + return [ + 'problematicDependency' => null, + 'confirmationMethod' => 'none', + ]; + } + + /** + * Prüfe ob eine Dependency das Interface benötigt (über Reflection) + */ + private function dependencyNeedsInterface(string $dependencyClass, string $interface): bool + { + try { + if (!class_exists($dependencyClass)) { + return false; + } + + $reflection = new \ReflectionClass($dependencyClass); + $constructor = $reflection->getConstructor(); + + if ($constructor === null) { + return false; + } + + // Prüfe alle Constructor-Parameter + foreach ($constructor->getParameters() as $parameter) { + $type = $parameter->getType(); + + if ($type === null) { + continue; + } + + // Direkter NamedType + if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { + if ($type->getName() === $interface) { + return true; + } + } + + // Union Types (PHP 8.0+) + if ($type instanceof \ReflectionUnionType) { + foreach ($type->getTypes() as $subType) { + if ($subType instanceof \ReflectionNamedType && !$subType->isBuiltin()) { + if ($subType->getName() === $interface) { + return true; + } + } + } + } + + // Intersection Types (PHP 8.1+) + if ($type instanceof \ReflectionIntersectionType) { + foreach ($type->getTypes() as $subType) { + if ($subType instanceof \ReflectionNamedType && !$subType->isBuiltin()) { + if ($subType->getName() === $interface) { + return true; + } + } + } + } + } + + return false; + } catch (\Throwable) { + return false; + } + } + /** * Erkenne ob ein Initializer-Zyklus vorliegt * - * @return array{isInitializerCycle: bool, initializerClass: string|null} + * @return array{isInitializerCycle: bool, initializerClass: string|null, initializerDependencies: array|null} */ private function detectInitializerCycle(): array { @@ -133,21 +317,106 @@ final class CyclicDependencyException extends ContainerException } if (!$isInterfaceOrAbstract) { - return ['isInitializerCycle' => false, 'initializerClass' => null]; + return ['isInitializerCycle' => false, 'initializerClass' => null, 'initializerDependencies' => null]; } // Suche nach Initializer-Klassen in der Kette - // Initializer enden typischerweise auf "Initializer" + $initializerClass = null; foreach ($this->fullChain as $classInChain) { if (str_ends_with($classInChain, 'Initializer')) { - return [ - 'isInitializerCycle' => true, - 'initializerClass' => $classInChain, - ]; + $initializerClass = $classInChain; + break; } } - return ['isInitializerCycle' => false, 'initializerClass' => null]; + // Wenn nicht in der Kette, versuche Initializer über DiscoveryRegistry oder Namenskonvention zu finden + if ($initializerClass === null) { + $initializerClass = $this->findInitializerForInterface($this->cycleStart); + } + + if ($initializerClass === null) { + return ['isInitializerCycle' => false, 'initializerClass' => null, 'initializerDependencies' => null]; + } + + // Analysiere Dependencies des Initializers + $dependencies = $this->analyzeInitializerDependencies($initializerClass); + + return [ + 'isInitializerCycle' => true, + 'initializerClass' => $initializerClass, + 'initializerDependencies' => $dependencies, + ]; + } + + /** + * Finde Initializer für ein Interface + * + * @return string|null + */ + private function findInitializerForInterface(string $interface): ?string + { + // Versuche über DiscoveryRegistry + try { + // Prüfe ob DiscoveryRegistry global verfügbar ist (z.B. über Container) + // Da wir keinen direkten Zugriff haben, versuchen wir eine Namenskonvention + // oder schauen ob wir DiscoveryRegistry über einen Singleton-Pattern erreichen können + } catch (\Throwable) { + // Ignoriere Fehler + } + + // Fallback: Namenskonvention + // ComponentRegistryInterface -> ComponentRegistryInitializer + $interfaceName = basename(str_replace('\\', '/', $interface)); + $suggestedName = str_replace('Interface', '', $interfaceName) . 'Initializer'; + + // Suche in gleichem Namespace + $namespace = substr($interface, 0, strrpos($interface, '\\')); + $suggestedClass = $namespace . '\\' . $suggestedName; + + if (class_exists($suggestedClass)) { + return $suggestedClass; + } + + // Versuche auch ohne Namespace-Präfix + $suggestedClassShort = 'App\\Framework\\' . str_replace('Interface', '', $interfaceName) . 'Initializer'; + if (class_exists($suggestedClassShort)) { + return $suggestedClassShort; + } + + return null; + } + + /** + * Analysiere Dependencies eines Initializers über Reflection + * + * @return array + */ + private function analyzeInitializerDependencies(string $initializerClass): array + { + try { + if (!class_exists($initializerClass)) { + return []; + } + + $reflection = new \ReflectionClass($initializerClass); + $constructor = $reflection->getConstructor(); + + if ($constructor === null) { + return []; + } + + $dependencies = []; + foreach ($constructor->getParameters() as $parameter) { + $type = $parameter->getType(); + if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { + $dependencies[] = $type->getName(); + } + } + + return $dependencies; + } catch (\Throwable) { + return []; + } } /**