refactor(di): improve cyclic dependency handling and error messages
- Replace `FrameworkException` with `ContainerException` in `CyclicDependencyException` - Add detection logic for cycle start and improved message formatting - Provide actionable resolution tips in error messages - Enhance `ClassResolutionException` to reuse `CyclicDependencyException` messages - Add context for available bindings when a cycle is detected
This commit is contained in:
@@ -32,8 +32,12 @@ final class ClassResolutionException extends ContainerException
|
|||||||
?FailedInitializer $failedInitializer = null,
|
?FailedInitializer $failedInitializer = null,
|
||||||
?string $matchingInitializer = null
|
?string $matchingInitializer = null
|
||||||
): self {
|
): self {
|
||||||
|
// Spezialbehandlung für zyklische Abhängigkeiten
|
||||||
|
if ($previous instanceof CyclicDependencyException) {
|
||||||
|
// Nutze die verbesserte Meldung der CyclicDependencyException direkt
|
||||||
|
$reason = $previous->getMessage();
|
||||||
|
} elseif ($bindingType === 'callable' && ($failedInitializer !== null || $matchingInitializer !== null)) {
|
||||||
// Für callable bindings: Spezielle Fehlermeldung wenn Initializer beteiligt ist
|
// Für callable bindings: Spezielle Fehlermeldung wenn Initializer beteiligt ist
|
||||||
if ($bindingType === 'callable' && ($failedInitializer !== null || $matchingInitializer !== null)) {
|
|
||||||
$reason = "Initializer failed during execution.";
|
$reason = "Initializer failed during execution.";
|
||||||
} else {
|
} else {
|
||||||
$reason = "Binding resolution failed (binding type: {$bindingType}): " . $previous->getMessage();
|
$reason = "Binding resolution failed (binding type: {$bindingType}): " . $previous->getMessage();
|
||||||
@@ -65,9 +69,16 @@ final class ClassResolutionException extends ContainerException
|
|||||||
array $availableBindings,
|
array $availableBindings,
|
||||||
array $dependencyChain
|
array $dependencyChain
|
||||||
): self {
|
): self {
|
||||||
|
// Spezialbehandlung für zyklische Abhängigkeiten
|
||||||
|
// CyclicDependencyException extends FrameworkException extends RuntimeException,
|
||||||
|
// daher wird es hier auch erkannt
|
||||||
|
$reason = $exception instanceof CyclicDependencyException
|
||||||
|
? $exception->getMessage()
|
||||||
|
: $exception->getMessage();
|
||||||
|
|
||||||
return new self(
|
return new self(
|
||||||
class: $class,
|
class: $class,
|
||||||
reason: $exception->getMessage(),
|
reason: $reason,
|
||||||
availableBindings: $availableBindings,
|
availableBindings: $availableBindings,
|
||||||
dependencyChain: $dependencyChain,
|
dependencyChain: $dependencyChain,
|
||||||
previous: $exception
|
previous: $exception
|
||||||
@@ -91,8 +102,22 @@ final class ClassResolutionException extends ContainerException
|
|||||||
) {
|
) {
|
||||||
$dependencyChainStr = implode(' -> ', $dependencyChain);
|
$dependencyChainStr = implode(' -> ', $dependencyChain);
|
||||||
|
|
||||||
|
// Spezialbehandlung für zyklische Abhängigkeiten: Nutze die verbesserte Meldung direkt
|
||||||
|
if ($previous instanceof CyclicDependencyException) {
|
||||||
|
// Die CyclicDependencyException hat bereits eine übersichtliche Formatierung
|
||||||
|
// Füge nur zusätzlichen Kontext hinzu, wenn relevant
|
||||||
|
$message = $reason; // Nutze die bereits formatierte Meldung aus CyclicDependencyException
|
||||||
|
|
||||||
|
// Füge zusätzliche Informationen nur hinzu, wenn sie hilfreich sind
|
||||||
|
if (!empty($availableBindings)) {
|
||||||
|
$message .= "\n\n";
|
||||||
|
$message .= "📋 Verfügbare Bindings: " . implode(', ', array_slice($availableBindings, 0, 10));
|
||||||
|
if (count($availableBindings) > 10) {
|
||||||
|
$message .= " (und " . (count($availableBindings) - 10) . " weitere)";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} elseif ($bindingType === 'callable' && ($this->failedInitializer !== null || $this->matchingInitializer !== null)) {
|
||||||
// Für callable bindings mit fehlgeschlagenem Initializer: Detaillierte Fehlermeldung
|
// Für callable bindings mit fehlgeschlagenem Initializer: Detaillierte Fehlermeldung
|
||||||
if ($bindingType === 'callable' && ($this->failedInitializer !== null || $this->matchingInitializer !== null)) {
|
|
||||||
$message = "Cannot resolve class '{$class}': {$reason}\n\n";
|
$message = "Cannot resolve class '{$class}': {$reason}\n\n";
|
||||||
$message .= "⚠️ Initializer failed:\n";
|
$message .= "⚠️ Initializer failed:\n";
|
||||||
|
|
||||||
|
|||||||
@@ -5,27 +5,83 @@ declare(strict_types=1);
|
|||||||
namespace App\Framework\DI\Exceptions;
|
namespace App\Framework\DI\Exceptions;
|
||||||
|
|
||||||
use App\Framework\Exception\ExceptionContext;
|
use App\Framework\Exception\ExceptionContext;
|
||||||
use App\Framework\Exception\FrameworkException;
|
|
||||||
|
|
||||||
final class CyclicDependencyException extends FrameworkException
|
final class CyclicDependencyException extends ContainerException
|
||||||
{
|
{
|
||||||
|
private readonly array $cycle;
|
||||||
|
private readonly string $cycleStart;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
array $dependencyChain,
|
array $dependencyChain,
|
||||||
string $class,
|
string $class,
|
||||||
int $code = 0,
|
int $code = 0,
|
||||||
?\Throwable $previous = null
|
?\Throwable $previous = null
|
||||||
) {
|
) {
|
||||||
|
// Finde wo der Zyklus beginnt (erste Wiederholung in der Kette)
|
||||||
|
$cycleStartIndex = array_search($class, $dependencyChain, true);
|
||||||
|
|
||||||
|
if ($cycleStartIndex !== false) {
|
||||||
|
// Extrahiere nur den Zyklus-Teil
|
||||||
|
$this->cycle = array_slice($dependencyChain, $cycleStartIndex);
|
||||||
|
$this->cycle[] = $class; // Schließe den Zyklus
|
||||||
|
$this->cycleStart = $dependencyChain[$cycleStartIndex];
|
||||||
|
} else {
|
||||||
|
// Fallback: Verwende die gesamte Kette
|
||||||
|
$this->cycle = array_merge($dependencyChain, [$class]);
|
||||||
|
$this->cycleStart = $dependencyChain[0] ?? $class;
|
||||||
|
}
|
||||||
|
|
||||||
$context = ExceptionContext::forOperation('dependency_resolution', 'DI')
|
$context = ExceptionContext::forOperation('dependency_resolution', 'DI')
|
||||||
->withData([
|
->withData([
|
||||||
'dependencyChain' => $dependencyChain,
|
'dependencyChain' => $dependencyChain,
|
||||||
'class' => $class,
|
'class' => $class,
|
||||||
|
'cycle' => $this->cycle,
|
||||||
|
'cycleStart' => $this->cycleStart,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$message = $this->buildMessage();
|
||||||
|
|
||||||
|
// ContainerException erwartet message als ersten Parameter und optional context
|
||||||
parent::__construct(
|
parent::__construct(
|
||||||
message: 'Zyklische Abhängigkeit entdeckt: ' . implode(' -> ', $dependencyChain) . " -> $class",
|
message: $message,
|
||||||
context: $context,
|
context: $context,
|
||||||
code: $code,
|
code: $code,
|
||||||
previous: $previous
|
previous: $previous
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function buildMessage(): string
|
||||||
|
{
|
||||||
|
$cycleStr = implode(' → ', $this->cycle);
|
||||||
|
|
||||||
|
$message = "🔄 Zyklische Abhängigkeit entdeckt:\n\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";
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user