diff --git a/src/Framework/DI/Exceptions/CyclicDependencyException.php b/src/Framework/DI/Exceptions/CyclicDependencyException.php index da826792..f38be190 100644 --- a/src/Framework/DI/Exceptions/CyclicDependencyException.php +++ b/src/Framework/DI/Exceptions/CyclicDependencyException.php @@ -128,13 +128,24 @@ final class CyclicDependencyException extends ContainerException ); $problematicDependency = $problematicInfo['problematicDependency']; + $fullPath = $problematicInfo['fullPath'] ?? null; 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"; + + // Zeige vollständigen Pfad wenn verfügbar + if ($fullPath !== null && count($fullPath) > 2) { + $pathStr = implode(' → ', $fullPath); + $message .= " Vollständiger Dependency-Pfad:\n"; + $message .= " {$pathStr}\n\n"; + $message .= " Der Zyklus entsteht:\n"; + $message .= " Initializer → {$pathStr} → Initializer\n\n"; + } else { + $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"; @@ -201,6 +212,7 @@ final class CyclicDependencyException extends ContainerException // Finde problematische Dependency für Hervorhebung $problematicInfo = $this->findProblematicDependency($allDependencies, $interface); $problematicDependency = $problematicInfo['problematicDependency']; + $fullPath = $problematicInfo['fullPath'] ?? null; $constructorDeps = $dependencyAnalysis !== null ? $dependencyAnalysis['constructorDeps'] : []; $containerGetDeps = $dependencyAnalysis !== null ? $dependencyAnalysis['containerGetDeps'] : []; @@ -222,7 +234,12 @@ final class CyclicDependencyException extends ContainerException } } - if ($problematicDependency !== null) { + if ($problematicDependency !== null && $fullPath !== null && count($fullPath) > 2) { + // Zeige vollständigen rekursiven Pfad + $pathStr = implode(' → ', $fullPath); + $message .= " ↓ '{$problematicDependency}' benötigt über folgenden Pfad:\n"; + $message .= " {$pathStr}\n"; + } elseif ($problematicDependency !== null) { $message .= " ↓ '{$problematicDependency}' benötigt wieder\n"; } else { $message .= " ↓ eine dieser Dependencies benötigt wieder\n"; @@ -235,11 +252,31 @@ final class CyclicDependencyException extends ContainerException /** * Finde welche Dependency das Interface benötigt (durch Analyse der dependency chain und Reflection) * - * @return array{problematicDependency: string|null, confirmationMethod: string} + * @return array{problematicDependency: string|null, confirmationMethod: string, fullPath: array|null} */ private function findProblematicDependency(array $initializerDependencies, string $interface): array { - // Methode 1: Prüfe welche Dependency in der dependency chain vor dem Interface vorkommt + $analyzer = new InitializerDependencyAnalyzer(); + + // Methode 1: Rekursive Suche - Finde vollständigen Pfad für jede Dependency + foreach ($initializerDependencies as $dependency) { + // Überspringe Container selbst + if ($dependency === Container::class || $dependency === 'App\Framework\DI\Container') { + continue; + } + + $path = $analyzer->findDependencyPathToInterface($dependency, $interface); + if ($path !== null && !empty($path)) { + // Der erste Eintrag im Pfad ist die Dependency, die das Interface benötigt + return [ + 'problematicDependency' => $dependency, + 'confirmationMethod' => 'recursive_analysis', + 'fullPath' => $path, + ]; + } + } + + // Methode 2: 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 @@ -247,29 +284,36 @@ final class CyclicDependencyException extends ContainerException return [ 'problematicDependency' => $classInChain, 'confirmationMethod' => 'dependency_chain', + 'fullPath' => [$classInChain, $interface], ]; } } } - // Methode 2: Prüfe auch im Zyklus selbst (vor dem Interface) + // Methode 3: 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', + 'fullPath' => [$classInCycle, $interface], ]; } } } - // Methode 3: Analysiere alle Dependencies des Initializers über Reflection + // Methode 4: Analysiere alle Dependencies des Initializers über Reflection (direkt) foreach ($initializerDependencies as $dependency) { + if ($dependency === Container::class || $dependency === 'App\Framework\DI\Container') { + continue; + } + if ($this->dependencyNeedsInterface($dependency, $interface)) { return [ 'problematicDependency' => $dependency, 'confirmationMethod' => 'reflection_analysis', + 'fullPath' => [$dependency, $interface], ]; } } @@ -277,6 +321,7 @@ final class CyclicDependencyException extends ContainerException return [ 'problematicDependency' => null, 'confirmationMethod' => 'none', + 'fullPath' => null, ]; } diff --git a/src/Framework/DI/InitializerDependencyAnalyzer.php b/src/Framework/DI/InitializerDependencyAnalyzer.php index aa4706a1..4e81b9a9 100644 --- a/src/Framework/DI/InitializerDependencyAnalyzer.php +++ b/src/Framework/DI/InitializerDependencyAnalyzer.php @@ -9,9 +9,11 @@ namespace App\Framework\DI; * * Erkennt sowohl Constructor-Dependencies als auch container->get() Aufrufe * und prüft ob Container verfügbar ist, bevor nach container->get() gesucht wird. + * Unterstützt rekursive Analyse um vollständige Dependency-Pfade zu finden. */ final readonly class InitializerDependencyAnalyzer { + private const MAX_RECURSION_DEPTH = 4; /** * Analysiere Dependencies eines Initializers * @@ -222,5 +224,164 @@ final readonly class InitializerDependencyAnalyzer return null; } } + + /** + * Finde rekursiv den Pfad von einer Dependency bis zum Interface + * + * @param string $dependencyClass Die Dependency-Klasse + * @param string $targetInterface Das gesuchte Interface + * @param array $visited Bereits besuchte Klassen (für Cycle-Detection) + * @param array $currentPath Aktueller Pfad + * @param int $depth Aktuelle Rekursionstiefe + * @return array|null Vollständiger Pfad [Dep1, Dep2, ..., Interface] oder null + */ + public function findDependencyPathToInterface( + string $dependencyClass, + string $targetInterface, + array $visited = [], + array $currentPath = [], + int $depth = 0 + ): ?array { + // Max. Rekursionstiefe erreicht + if ($depth >= self::MAX_RECURSION_DEPTH) { + return null; + } + + // Cycle-Detection: Vermeide Endlosschleifen + if (in_array($dependencyClass, $visited, true)) { + return null; + } + + // Prüfe ob diese Klasse direkt das Interface benötigt + if ($this->dependencyNeedsInterface($dependencyClass, $targetInterface)) { + return array_merge($currentPath, [$dependencyClass, $targetInterface]); + } + + // Rekursiv: Analysiere Dependencies dieser Klasse + $dependencies = $this->getClassDependencies($dependencyClass); + + if (empty($dependencies)) { + return null; + } + + $newVisited = array_merge($visited, [$dependencyClass]); + $newPath = array_merge($currentPath, [$dependencyClass]); + + foreach ($dependencies as $subDependency) { + // Überspringe Container selbst (würde alle Dependencies auflisten) + if ($subDependency === Container::class || $subDependency === 'App\Framework\DI\Container') { + continue; + } + + $path = $this->findDependencyPathToInterface( + $subDependency, + $targetInterface, + $newVisited, + $newPath, + $depth + 1 + ); + + if ($path !== null) { + return $path; + } + } + + return null; + } + + /** + * Prüfe ob eine Klasse ein 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; + } + } + + /** + * Hole Dependencies einer Klasse (Constructor-Parameter) + * + * @return array + */ + private function getClassDependencies(string $className): array + { + try { + if (!class_exists($className)) { + return []; + } + + $reflection = new \ReflectionClass($className); + $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 []; + } + } }