refactor(di): enhance CyclicDependencyException with full path analysis and improved messaging

- Add support for full dependency path detection in `CyclicDependencyException` to identify complex cycles.
- Extend `InitializerDependencyAnalyzer` with recursive path analysis up to a maximum depth.
- Update error messages with detailed full path and actionable resolutions for improved debugging.
- Refactor problematic dependency detection to include full path context where applicable.
This commit is contained in:
2025-11-03 20:52:16 +01:00
parent 8919da8a5c
commit 9f0dfd131a
2 changed files with 215 additions and 9 deletions

View File

@@ -128,13 +128,24 @@ final class CyclicDependencyException extends ContainerException
); );
$problematicDependency = $problematicInfo['problematicDependency']; $problematicDependency = $problematicInfo['problematicDependency'];
$fullPath = $problematicInfo['fullPath'] ?? null;
if ($problematicDependency !== null) { if ($problematicDependency !== null) {
$message .= " ✅ Problem-Dependency identifiziert: '{$problematicDependency}'\n\n"; $message .= " ✅ Problem-Dependency identifiziert: '{$problematicDependency}'\n\n";
$message .= " Das Problem: '{$problematicDependency}' (eine Dependency des Initializers)\n";
$message .= " benötigt im Constructor '{$this->cycleStart}',\n"; // Zeige vollständigen Pfad wenn verfügbar
$message .= " wodurch der Zyklus entsteht:\n"; if ($fullPath !== null && count($fullPath) > 2) {
$message .= " Initializer → {$problematicDependency}{$this->cycleStart} → Initializer\n\n"; $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 { } else {
$message .= " Der Initializer '{$initializerInfo['initializerClass']}' benötigt\n"; $message .= " Der Initializer '{$initializerInfo['initializerClass']}' benötigt\n";
$message .= " das Interface/abstrakte Klasse '{$this->cycleStart}',\n"; $message .= " das Interface/abstrakte Klasse '{$this->cycleStart}',\n";
@@ -201,6 +212,7 @@ final class CyclicDependencyException extends ContainerException
// Finde problematische Dependency für Hervorhebung // Finde problematische Dependency für Hervorhebung
$problematicInfo = $this->findProblematicDependency($allDependencies, $interface); $problematicInfo = $this->findProblematicDependency($allDependencies, $interface);
$problematicDependency = $problematicInfo['problematicDependency']; $problematicDependency = $problematicInfo['problematicDependency'];
$fullPath = $problematicInfo['fullPath'] ?? null;
$constructorDeps = $dependencyAnalysis !== null ? $dependencyAnalysis['constructorDeps'] : []; $constructorDeps = $dependencyAnalysis !== null ? $dependencyAnalysis['constructorDeps'] : [];
$containerGetDeps = $dependencyAnalysis !== null ? $dependencyAnalysis['containerGetDeps'] : []; $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"; $message .= " ↓ '{$problematicDependency}' benötigt wieder\n";
} else { } else {
$message .= " ↓ eine dieser Dependencies benötigt wieder\n"; $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) * 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<string>|null}
*/ */
private function findProblematicDependency(array $initializerDependencies, string $interface): array 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) { foreach ($this->chainBeforeCycle as $classInChain) {
if (in_array($classInChain, $initializerDependencies, true)) { if (in_array($classInChain, $initializerDependencies, true)) {
// Bestätige durch Reflection, dass diese Klasse das Interface benötigt // Bestätige durch Reflection, dass diese Klasse das Interface benötigt
@@ -247,29 +284,36 @@ final class CyclicDependencyException extends ContainerException
return [ return [
'problematicDependency' => $classInChain, 'problematicDependency' => $classInChain,
'confirmationMethod' => 'dependency_chain', '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) { foreach ($this->cycle as $classInCycle) {
if ($classInCycle !== $interface && in_array($classInCycle, $initializerDependencies, true)) { if ($classInCycle !== $interface && in_array($classInCycle, $initializerDependencies, true)) {
if ($this->dependencyNeedsInterface($classInCycle, $interface)) { if ($this->dependencyNeedsInterface($classInCycle, $interface)) {
return [ return [
'problematicDependency' => $classInCycle, 'problematicDependency' => $classInCycle,
'confirmationMethod' => 'dependency_chain', '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) { foreach ($initializerDependencies as $dependency) {
if ($dependency === Container::class || $dependency === 'App\Framework\DI\Container') {
continue;
}
if ($this->dependencyNeedsInterface($dependency, $interface)) { if ($this->dependencyNeedsInterface($dependency, $interface)) {
return [ return [
'problematicDependency' => $dependency, 'problematicDependency' => $dependency,
'confirmationMethod' => 'reflection_analysis', 'confirmationMethod' => 'reflection_analysis',
'fullPath' => [$dependency, $interface],
]; ];
} }
} }
@@ -277,6 +321,7 @@ final class CyclicDependencyException extends ContainerException
return [ return [
'problematicDependency' => null, 'problematicDependency' => null,
'confirmationMethod' => 'none', 'confirmationMethod' => 'none',
'fullPath' => null,
]; ];
} }

View File

@@ -9,9 +9,11 @@ namespace App\Framework\DI;
* *
* Erkennt sowohl Constructor-Dependencies als auch container->get() Aufrufe * Erkennt sowohl Constructor-Dependencies als auch container->get() Aufrufe
* und prüft ob Container verfügbar ist, bevor nach container->get() gesucht wird. * 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 final readonly class InitializerDependencyAnalyzer
{ {
private const MAX_RECURSION_DEPTH = 4;
/** /**
* Analysiere Dependencies eines Initializers * Analysiere Dependencies eines Initializers
* *
@@ -222,5 +224,164 @@ final readonly class InitializerDependencyAnalyzer
return null; 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<string> $visited Bereits besuchte Klassen (für Cycle-Detection)
* @param array<string> $currentPath Aktueller Pfad
* @param int $depth Aktuelle Rekursionstiefe
* @return array<string>|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<string>
*/
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 [];
}
}
} }