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