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:
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\DI\Exceptions;
|
||||
|
||||
use App\Framework\DI\Initializer;
|
||||
use App\Framework\Discovery\Results\DiscoveryRegistry;
|
||||
use App\Framework\Exception\ExceptionContext;
|
||||
|
||||
final class CyclicDependencyException extends ContainerException
|
||||
@@ -64,8 +66,8 @@ final class CyclicDependencyException extends ContainerException
|
||||
private function buildMessage(): string
|
||||
{
|
||||
$cycleStr = implode(' → ', $this->cycle);
|
||||
|
||||
$requestedClass = array_last($this->fullChain);
|
||||
$requestedClass = $this->fullChain[count($this->fullChain) - 1] ?? $this->cycleStart;
|
||||
$firstClass = $this->fullChain[0] ?? $this->cycleStart;
|
||||
|
||||
// Prüfe ob Initializer-Zyklus vorliegt
|
||||
$initializerInfo = $this->detectInitializerCycle();
|
||||
@@ -73,10 +75,19 @@ final class CyclicDependencyException extends ContainerException
|
||||
$message = "🔄 Zyklische Abhängigkeit entdeckt:\n\n";
|
||||
|
||||
// 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 .= " 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)) {
|
||||
$beforeCycleStr = implode(' → ', $this->chainBeforeCycle);
|
||||
$message .= "📋 Abhängigkeitskette:\n";
|
||||
@@ -88,18 +99,42 @@ final class CyclicDependencyException extends ContainerException
|
||||
$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";
|
||||
|
||||
// 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 .= " 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 .= " • 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 .= " • 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";
|
||||
$message .= " • Oder: Refactoriere den Initializer, sodass er keine zirkuläre Abhängigkeit hat\n\n";
|
||||
}
|
||||
|
||||
// Füge allgemeine hilfreiche Hinweise hinzu
|
||||
@@ -112,10 +147,159 @@ final class CyclicDependencyException extends ContainerException
|
||||
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
|
||||
*
|
||||
* @return array{isInitializerCycle: bool, initializerClass: string|null}
|
||||
* @return array{isInitializerCycle: bool, initializerClass: string|null, initializerDependencies: array<string>|null}
|
||||
*/
|
||||
private function detectInitializerCycle(): array
|
||||
{
|
||||
@@ -133,21 +317,106 @@ final class CyclicDependencyException extends ContainerException
|
||||
}
|
||||
|
||||
if (!$isInterfaceOrAbstract) {
|
||||
return ['isInitializerCycle' => false, 'initializerClass' => null];
|
||||
return ['isInitializerCycle' => false, 'initializerClass' => null, 'initializerDependencies' => null];
|
||||
}
|
||||
|
||||
// Suche nach Initializer-Klassen in der Kette
|
||||
// Initializer enden typischerweise auf "Initializer"
|
||||
$initializerClass = null;
|
||||
foreach ($this->fullChain as $classInChain) {
|
||||
if (str_ends_with($classInChain, 'Initializer')) {
|
||||
return [
|
||||
'isInitializerCycle' => true,
|
||||
'initializerClass' => $classInChain,
|
||||
];
|
||||
$initializerClass = $classInChain;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user