Files
michaelschiemer/src/Framework/DI/Exceptions/CyclicDependencyException.php
Michael Schiemer 6b5aaf47a4 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.
2025-11-03 18:27:09 +01:00

168 lines
6.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\DI\Exceptions;
use App\Framework\Exception\ExceptionContext;
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,
string $class,
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);
if ($cycleStartIndex !== false) {
// Extrahiere nur den Zyklus-Teil
$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;
$this->cycleStart = $cycleStart;
$context = ExceptionContext::forOperation('dependency_resolution', 'DI')
->withData([
'dependencyChain' => $dependencyChain,
'class' => $class,
'cycle' => $this->cycle,
'cycleStart' => $this->cycleStart,
]);
$message = $this->buildMessage();
// ContainerException erwartet message als ersten Parameter und optional context
parent::__construct(
message: $message,
context: $context,
code: $code,
previous: $previous
);
}
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";
// 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";
$message .= " • Verwende Event-basierte Kommunikation statt direkter Abhängigkeit\n";
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)
*/
public function getCycle(): array
{
return $this->cycle;
}
/**
* Get the class where the cycle starts
*/
public function getCycleStart(): string
{
return $this->cycleStart;
}
}