- 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.
168 lines
6.2 KiB
PHP
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;
|
|
}
|
|
}
|