refactor(di): enhance CyclicDependencyException with initializer detection and improved messaging

- Extend `CyclicDependencyException` to include detailed initializer cycle analysis.
- Refactor chain message generation with initializer-specific context.
- Add methods for problematic dependency detection and initializer dependency analysis.
- Improve error messages with clearer actionable tips for initializer-related cycles.
This commit is contained in:
2025-11-03 19:02:28 +01:00
parent da1dee7a01
commit 522e76e86a

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace App\Framework\DI\Exceptions; namespace App\Framework\DI\Exceptions;
use App\Framework\DI\Initializer;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Exception\ExceptionContext; use App\Framework\Exception\ExceptionContext;
final class CyclicDependencyException extends ContainerException final class CyclicDependencyException extends ContainerException
@@ -64,8 +66,8 @@ final class CyclicDependencyException extends ContainerException
private function buildMessage(): string private function buildMessage(): string
{ {
$cycleStr = implode(' → ', $this->cycle); $cycleStr = implode(' → ', $this->cycle);
$requestedClass = $this->fullChain[count($this->fullChain) - 1] ?? $this->cycleStart;
$requestedClass = array_last($this->fullChain); $firstClass = $this->fullChain[0] ?? $this->cycleStart;
// Prüfe ob Initializer-Zyklus vorliegt // Prüfe ob Initializer-Zyklus vorliegt
$initializerInfo = $this->detectInitializerCycle(); $initializerInfo = $this->detectInitializerCycle();
@@ -73,10 +75,19 @@ final class CyclicDependencyException extends ContainerException
$message = "🔄 Zyklische Abhängigkeit entdeckt:\n\n"; $message = "🔄 Zyklische Abhängigkeit entdeckt:\n\n";
// Zeige Kontext: Wer versucht was zu erstellen // Zeige Kontext: Wer versucht was zu erstellen
if ($firstClass !== $requestedClass) {
$message .= "❌ Problem: '{$firstClass}' benötigt '{$requestedClass}',\n";
$message .= " aber eine zyklische Abhängigkeit wurde entdeckt.\n\n";
} else {
$message .= "❌ Problem: Beim Versuch, '{$requestedClass}' zu erstellen,\n"; $message .= "❌ Problem: Beim Versuch, '{$requestedClass}' zu erstellen,\n";
$message .= " wurde eine zyklische Abhängigkeit entdeckt.\n\n"; $message .= " wurde eine zyklische Abhängigkeit entdeckt.\n\n";
}
// Zeige vollständige Abhängigkeitskette (wenn vorhanden) // Zeige vollständige Abhängigkeitskette mit Zwischenschritten für Initializer-Zyklen
if ($initializerInfo['isInitializerCycle'] && $initializerInfo['initializerClass'] !== null) {
$message .= $this->buildInitializerChainMessage($firstClass, $initializerInfo);
} else {
// Standard-Darstellung ohne Initializer-Details
if (!empty($this->chainBeforeCycle)) { if (!empty($this->chainBeforeCycle)) {
$beforeCycleStr = implode(' → ', $this->chainBeforeCycle); $beforeCycleStr = implode(' → ', $this->chainBeforeCycle);
$message .= "📋 Abhängigkeitskette:\n"; $message .= "📋 Abhängigkeitskette:\n";
@@ -88,18 +99,42 @@ final class CyclicDependencyException extends ContainerException
$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";
}
// Spezifische Hinweise für Initializer-Zyklen // Spezifische Hinweise für Initializer-Zyklen
if ($initializerInfo['isInitializerCycle']) { if ($initializerInfo['isInitializerCycle']) {
$message .= "⚠️ Initializer-Zyklus erkannt!\n\n"; $message .= "⚠️ Initializer-Zyklus erkannt!\n\n";
// Finde welche Dependency das Interface benötigt
$problematicInfo = $this->findProblematicDependency(
$initializerInfo['initializerDependencies'] ?? [],
$this->cycleStart
);
$problematicDependency = $problematicInfo['problematicDependency'];
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";
} else {
$message .= " Der Initializer '{$initializerInfo['initializerClass']}' benötigt\n"; $message .= " Der Initializer '{$initializerInfo['initializerClass']}' benötigt\n";
$message .= " das Interface/abstrakte Klasse '{$this->cycleStart}',\n"; $message .= " das Interface/abstrakte Klasse '{$this->cycleStart}',\n";
$message .= " welches wiederum den Initializer benötigt.\n\n"; $message .= " welches wiederum den Initializer benötigt.\n";
$message .= " (Konnte die problematische Dependency nicht eindeutig identifizieren)\n\n";
}
$message .= "🔧 Spezifische Lösung für Initializer-Zyklen:\n"; $message .= "🔧 Spezifische Lösung für Initializer-Zyklen:\n";
$message .= " • Initializer sollte das Interface NICHT im Constructor benötigen\n"; $message .= " • Initializer sollte das Interface NICHT im Constructor benötigen\n";
if ($problematicDependency !== null) {
$message .= " • '{$problematicDependency}' sollte '{$this->cycleStart}' NICHT im Constructor benötigen\n";
$message .= " • Verwende Lazy Loading für '{$problematicDependency}' → '{$this->cycleStart}'\n";
$message .= " (z.B. über Container->get() statt Constructor-Injection)\n";
}
$message .= " • Verwende Container als Parameter und hole das Interface erst im __invoke()\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 .= " • Oder: Refactoriere den Initializer, sodass er keine zirkuläre Abhängigkeit hat\n\n";
$message .= " • Prüfe, ob der Initializer wirklich alle Dependencies braucht\n\n";
} }
// Füge allgemeine hilfreiche Hinweise hinzu // Füge allgemeine hilfreiche Hinweise hinzu
@@ -112,10 +147,159 @@ final class CyclicDependencyException extends ContainerException
return $message; return $message;
} }
/**
* Erstelle detaillierte Kette für Initializer-Zyklen
*/
private function buildInitializerChainMessage(string $firstClass, array $initializerInfo): string
{
$initializerClass = $initializerInfo['initializerClass'];
$dependencies = $initializerInfo['initializerDependencies'] ?? [];
$interface = $this->cycleStart;
$message = "📋 Vollständige Abhängigkeitskette:\n\n";
$message .= " 1. '{$firstClass}' benötigt\n";
$message .= " 2. '{$interface}' (Interface)\n";
$message .= " ↓ wird erstellt durch Initializer\n";
$message .= " 3. '{$initializerClass}' (Initializer)\n";
if (!empty($dependencies)) {
$message .= " ↓ benötigt folgende Dependencies:\n";
// Finde problematische Dependency für Hervorhebung
$problematicInfo = $this->findProblematicDependency($dependencies, $interface);
$problematicDependency = $problematicInfo['problematicDependency'];
foreach ($dependencies as $index => $dependency) {
$isLast = $index === count($dependencies) - 1;
$arrow = $isLast ? '└─→' : '├─→';
$highlight = ($dependency === $problematicDependency) ? ' ⚠️ (benötigt Interface!)' : '';
$message .= " {$arrow} '{$dependency}'{$highlight}\n";
}
}
if ($problematicDependency !== null) {
$message .= " ↓ '{$problematicDependency}' benötigt wieder\n";
} else {
$message .= " ↓ eine dieser Dependencies benötigt wieder\n";
}
$message .= " 4. '{$interface}' (ZYKLUS!)\n\n";
return $message;
}
/**
* Finde welche Dependency das Interface benötigt (durch Analyse der dependency chain und Reflection)
*
* @return array{problematicDependency: string|null, confirmationMethod: string}
*/
private function findProblematicDependency(array $initializerDependencies, string $interface): array
{
// Methode 1: 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
if ($this->dependencyNeedsInterface($classInChain, $interface)) {
return [
'problematicDependency' => $classInChain,
'confirmationMethod' => 'dependency_chain',
];
}
}
}
// Methode 2: 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',
];
}
}
}
// Methode 3: Analysiere alle Dependencies des Initializers über Reflection
foreach ($initializerDependencies as $dependency) {
if ($this->dependencyNeedsInterface($dependency, $interface)) {
return [
'problematicDependency' => $dependency,
'confirmationMethod' => 'reflection_analysis',
];
}
}
return [
'problematicDependency' => null,
'confirmationMethod' => 'none',
];
}
/**
* Prüfe ob eine Dependency das 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;
}
}
/** /**
* Erkenne ob ein Initializer-Zyklus vorliegt * Erkenne ob ein Initializer-Zyklus vorliegt
* *
* @return array{isInitializerCycle: bool, initializerClass: string|null} * @return array{isInitializerCycle: bool, initializerClass: string|null, initializerDependencies: array<string>|null}
*/ */
private function detectInitializerCycle(): array private function detectInitializerCycle(): array
{ {
@@ -133,21 +317,106 @@ final class CyclicDependencyException extends ContainerException
} }
if (!$isInterfaceOrAbstract) { if (!$isInterfaceOrAbstract) {
return ['isInitializerCycle' => false, 'initializerClass' => null]; return ['isInitializerCycle' => false, 'initializerClass' => null, 'initializerDependencies' => null];
} }
// Suche nach Initializer-Klassen in der Kette // Suche nach Initializer-Klassen in der Kette
// Initializer enden typischerweise auf "Initializer" $initializerClass = null;
foreach ($this->fullChain as $classInChain) { foreach ($this->fullChain as $classInChain) {
if (str_ends_with($classInChain, 'Initializer')) { if (str_ends_with($classInChain, 'Initializer')) {
return [ $initializerClass = $classInChain;
'isInitializerCycle' => true, break;
'initializerClass' => $classInChain,
];
} }
} }
return ['isInitializerCycle' => false, 'initializerClass' => null]; // Wenn nicht in der Kette, versuche Initializer über DiscoveryRegistry oder Namenskonvention zu finden
if ($initializerClass === null) {
$initializerClass = $this->findInitializerForInterface($this->cycleStart);
}
if ($initializerClass === null) {
return ['isInitializerCycle' => false, 'initializerClass' => null, 'initializerDependencies' => null];
}
// Analysiere Dependencies des Initializers
$dependencies = $this->analyzeInitializerDependencies($initializerClass);
return [
'isInitializerCycle' => true,
'initializerClass' => $initializerClass,
'initializerDependencies' => $dependencies,
];
}
/**
* Finde Initializer für ein Interface
*
* @return string|null
*/
private function findInitializerForInterface(string $interface): ?string
{
// Versuche über DiscoveryRegistry
try {
// Prüfe ob DiscoveryRegistry global verfügbar ist (z.B. über Container)
// Da wir keinen direkten Zugriff haben, versuchen wir eine Namenskonvention
// oder schauen ob wir DiscoveryRegistry über einen Singleton-Pattern erreichen können
} catch (\Throwable) {
// Ignoriere Fehler
}
// Fallback: Namenskonvention
// ComponentRegistryInterface -> ComponentRegistryInitializer
$interfaceName = basename(str_replace('\\', '/', $interface));
$suggestedName = str_replace('Interface', '', $interfaceName) . 'Initializer';
// Suche in gleichem Namespace
$namespace = substr($interface, 0, strrpos($interface, '\\'));
$suggestedClass = $namespace . '\\' . $suggestedName;
if (class_exists($suggestedClass)) {
return $suggestedClass;
}
// Versuche auch ohne Namespace-Präfix
$suggestedClassShort = 'App\\Framework\\' . str_replace('Interface', '', $interfaceName) . 'Initializer';
if (class_exists($suggestedClassShort)) {
return $suggestedClassShort;
}
return null;
}
/**
* Analysiere Dependencies eines Initializers über Reflection
*
* @return array<string>
*/
private function analyzeInitializerDependencies(string $initializerClass): array
{
try {
if (!class_exists($initializerClass)) {
return [];
}
$reflection = new \ReflectionClass($initializerClass);
$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 [];
}
} }
/** /**