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.
This commit is contained in:
@@ -10,6 +10,8 @@ final class CyclicDependencyException extends ContainerException
|
|||||||
{
|
{
|
||||||
private readonly array $cycle;
|
private readonly array $cycle;
|
||||||
private readonly string $cycleStart;
|
private readonly string $cycleStart;
|
||||||
|
private readonly array $fullChain;
|
||||||
|
private readonly array $chainBeforeCycle;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
array $dependencyChain,
|
array $dependencyChain,
|
||||||
@@ -17,6 +19,9 @@ final class CyclicDependencyException extends ContainerException
|
|||||||
int $code = 0,
|
int $code = 0,
|
||||||
?\Throwable $previous = null
|
?\Throwable $previous = null
|
||||||
) {
|
) {
|
||||||
|
// Speichere vollständige Kette
|
||||||
|
$this->fullChain = array_merge($dependencyChain, [$class]);
|
||||||
|
|
||||||
// Finde wo der Zyklus beginnt (erste Wiederholung in der Kette)
|
// Finde wo der Zyklus beginnt (erste Wiederholung in der Kette)
|
||||||
$cycleStartIndex = array_search($class, $dependencyChain, true);
|
$cycleStartIndex = array_search($class, $dependencyChain, true);
|
||||||
|
|
||||||
@@ -25,10 +30,13 @@ final class CyclicDependencyException extends ContainerException
|
|||||||
$cycle = array_slice($dependencyChain, $cycleStartIndex);
|
$cycle = array_slice($dependencyChain, $cycleStartIndex);
|
||||||
$cycle[] = $class; // Schließe den Zyklus
|
$cycle[] = $class; // Schließe den Zyklus
|
||||||
$cycleStart = $dependencyChain[$cycleStartIndex];
|
$cycleStart = $dependencyChain[$cycleStartIndex];
|
||||||
|
// Speichere Teil vor dem Zyklus
|
||||||
|
$this->chainBeforeCycle = array_slice($dependencyChain, 0, $cycleStartIndex);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: Verwende die gesamte Kette
|
// Fallback: Verwende die gesamte Kette
|
||||||
$cycle = array_merge($dependencyChain, [$class]);
|
$cycle = array_merge($dependencyChain, [$class]);
|
||||||
$cycleStart = $dependencyChain[0] ?? $class;
|
$cycleStart = $dependencyChain[0] ?? $class;
|
||||||
|
$this->chainBeforeCycle = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->cycle = $cycle;
|
$this->cycle = $cycle;
|
||||||
@@ -56,14 +64,45 @@ final class CyclicDependencyException extends ContainerException
|
|||||||
private function buildMessage(): string
|
private function buildMessage(): string
|
||||||
{
|
{
|
||||||
$cycleStr = implode(' → ', $this->cycle);
|
$cycleStr = implode(' → ', $this->cycle);
|
||||||
|
$requestedClass = end($this->fullChain);
|
||||||
|
|
||||||
|
// Prüfe ob Initializer-Zyklus vorliegt
|
||||||
|
$initializerInfo = $this->detectInitializerCycle();
|
||||||
|
|
||||||
$message = "🔄 Zyklische Abhängigkeit entdeckt:\n\n";
|
$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 .= " {$cycleStr}\n";
|
||||||
$message .= " ↑─────────────────────┘\n";
|
$message .= " ↑─────────────────────┘\n";
|
||||||
$message .= " Der Zyklus beginnt hier bei '{$this->cycleStart}'\n\n";
|
$message .= " Der Zyklus beginnt hier bei '{$this->cycleStart}'\n\n";
|
||||||
|
|
||||||
// Füge hilfreiche Hinweise hinzu
|
// Spezifische Hinweise für Initializer-Zyklen
|
||||||
$message .= "💡 Lösungsvorschläge:\n";
|
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 .= " • Verwende Lazy Loading für eine der Abhängigkeiten\n";
|
||||||
$message .= " • Füge eine Factory zwischen die Klassen ein\n";
|
$message .= " • Füge eine Factory zwischen die Klassen ein\n";
|
||||||
$message .= " • Refactoriere die Abhängigkeitsstruktur (Dependency Inversion)\n";
|
$message .= " • Refactoriere die Abhängigkeitsstruktur (Dependency Inversion)\n";
|
||||||
@@ -72,6 +111,44 @@ final class CyclicDependencyException extends ContainerException
|
|||||||
return $message;
|
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)
|
* Get the detected cycle (without full dependency chain)
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user