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:
@@ -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<string>|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,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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 [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user