refactor: add circular dependency detection and error handling in DI container

- Introduce `InitializerCycleException` for detailed cycle reporting
- Enhance `InitializerProcessor` fallback with explicit discovery order handling and logging
- Implement proactive cycle detection in `InitializerDependencyGraph`
- Improve `ClassName` and `MethodName` with `Stringable` support
This commit is contained in:
2025-11-03 15:37:40 +01:00
parent 376fcd5fc1
commit 0ca382f80b
6 changed files with 173 additions and 4 deletions

View File

@@ -5,11 +5,12 @@ declare(strict_types=1);
namespace App\Framework\Core\ValueObjects;
use InvalidArgumentException;
use Stringable;
/**
* Immutable class name value object with namespace support
*/
final readonly class ClassName
final readonly class ClassName implements Stringable
{
/**
* @var class-string

View File

@@ -5,11 +5,12 @@ declare(strict_types=1);
namespace App\Framework\Core\ValueObjects;
use InvalidArgumentException;
use Stringable;
/**
* Value object for PHP method names with validation and ClassName integration
*/
final readonly class MethodName
final readonly class MethodName implements Stringable
{
private function __construct(
public string $name

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
/**
* Exception thrown when circular dependencies are detected in initializer dependency graph
*/
final class InitializerCycleException extends FrameworkException
{
/**
* @param array<array<string>> $cycles Array von Cycles, jeder Cycle ist ein Array von Return-Types
*/
public function __construct(
array $cycles,
int $code = 0,
?\Throwable $previous = null
) {
$messages = array_map(
fn(array $cycle): string => implode(' → ', $cycle) . ' → ' . $cycle[0],
$cycles
);
$message = 'Circular dependencies detected in initializers:' . PHP_EOL
. implode(PHP_EOL, array_map(fn(string $m): string => ' - ' . $m, $messages));
$context = ExceptionContext::forOperation('initializer_dependency_resolution', 'DI')
->withData([
'cycles' => $cycles,
'cycle_count' => count($cycles),
]);
parent::__construct(
message: $message,
context: $context,
code: $code,
previous: $previous
);
}
/**
* Gibt alle gefundenen Cycles zurück
* @return array<array<string>>
*/
public function getCycles(): array
{
return $this->getData()['cycles'] ?? [];
}
/**
* Gibt die Anzahl der gefundenen Cycles zurück
*/
public function getCycleCount(): int
{
return $this->getData()['cycle_count'] ?? 0;
}
}

View File

@@ -6,6 +6,7 @@ namespace App\Framework\DI;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\DI\Exceptions\InitializerCycleException;
use App\Framework\DI\ValueObjects\DependencyGraphNode;
use App\Framework\Reflection\ReflectionProvider;
@@ -54,9 +55,17 @@ final class InitializerDependencyGraph
/**
* Berechnet die optimale Ausführungsreihenfolge basierend auf Dependencies
* @return array<string> Sortierte Liste von Return-Types
* @throws InitializerCycleException Wenn circular dependencies gefunden werden
*/
public function getExecutionOrder(): array
{
// Proaktive Cycle-Detection: Prüfe alle Nodes bevor wir sortieren
$cycles = $this->detectAllCycles();
if (! empty($cycles)) {
throw new InitializerCycleException($cycles);
}
// Topologische Sortierung (ursprüngliche Logik)
$this->visited = [];
$this->inStack = [];
$result = [];
@@ -159,4 +168,78 @@ final class InitializerDependencyGraph
$this->inStack[$returnType] = false;
$result[] = $returnType;
}
/**
* Findet alle Cycles im Dependency Graph
* @return array<array<string>> Array von Cycles, jeder Cycle ist ein Array von Return-Types
*/
private function detectAllCycles(): array
{
$cycles = [];
$visited = [];
$inStack = [];
foreach (array_keys($this->nodes) as $returnType) {
if (! isset($visited[$returnType])) {
$cycle = $this->detectCycle($returnType, $visited, $inStack);
if (! empty($cycle)) {
// Prüfe ob dieser Cycle bereits gefunden wurde (als Teil eines anderen Cycles)
$isDuplicate = false;
foreach ($cycles as $existingCycle) {
if (count(array_intersect($cycle, $existingCycle)) === count($cycle)) {
$isDuplicate = true;
break;
}
}
if (! $isDuplicate) {
$cycles[] = $cycle;
}
}
}
}
return $cycles;
}
/**
* Prüft ob ein Cycle für einen gegebenen Return-Type existiert
* @param array<string, bool> $visited Referenz auf visited Array
* @param array<string, bool> $inStack Referenz auf inStack Array
* @param array<string> $path Aktueller Pfad während der Traversierung
* @return array<string> Leer wenn kein Cycle, sonst Array mit Cycle-Pfad
*/
private function detectCycle(string $returnType, array &$visited, array &$inStack, array $path = []): array
{
if (isset($inStack[$returnType]) && $inStack[$returnType]) {
// Cycle gefunden: Finde Start des Cycles
$cycleStart = array_search($returnType, $path, true);
if ($cycleStart !== false) {
$cycle = array_slice($path, $cycleStart);
$cycle[] = $returnType; // Schließe den Cycle
return $cycle;
}
}
if (isset($visited[$returnType])) {
return [];
}
$visited[$returnType] = true;
$inStack[$returnType] = true;
$path[] = $returnType;
foreach ($this->adjacencyList[$returnType] ?? [] as $dependency) {
if (isset($this->nodes[$dependency])) {
$cycle = $this->detectCycle($dependency, $visited, $inStack, $path);
if (! empty($cycle)) {
return $cycle;
}
}
}
unset($inStack[$returnType]);
array_pop($path);
return [];
}
}

View File

@@ -9,6 +9,7 @@ use App\Framework\Context\ExecutionContext;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\DI\Container;
use App\Framework\DI\Exceptions\InitializerCycleException;
use App\Framework\DI\Initializer;
use App\Framework\DI\InitializerDependencyGraph;
use App\Framework\DI\ValueObjects\DependencyGraphNode;
@@ -121,8 +122,28 @@ final readonly class InitializerProcessor
);
}
}
} catch (InitializerCycleException $e) {
// Spezielle Behandlung für Cycles: Expliziter Fallback mit detailliertem Logging
$logger = $this->getLogger();
$logger->error(
"Circular dependencies detected in initializers, registering in discovery order",
LogContext::withExceptionAndData($e, [
'cycles' => $e->getCycles(),
'cycle_count' => $e->getCycleCount(),
])
);
// Fallback: Registriere alle Services in Discovery-Reihenfolge
/** @var string $returnType */
foreach ($graph->getNodes() as $returnType => $node) {
$this->registerLazyService(
$returnType,
$node->getClassName(),
$node->getMethodName()
);
}
} catch (\Throwable $e) {
// Fallback: Registriere alle Services ohne spezielle Reihenfolge
// Andere Fehler: Fallback mit generischer Warnung
$logger = $this->getLogger();
$logger->warning(
"Failed to register services with dependency graph, falling back to unordered registration",

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Framework\Http;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\DI\Container;
use App\Framework\Logging\Logger;
use App\Framework\Reflection\ReflectionProvider;
@@ -168,7 +169,7 @@ final readonly class MiddlewareDependencyResolver
private function getMiddlewareDependencies(ClassName $middlewareClass): array
{
try {
if (! $middlewareClass->exists()) {
if (! $middlewareClass->exists() || MethodName::construct()->existsIn($middlewareClass)) {
return [];
}