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; namespace App\Framework\Core\ValueObjects;
use InvalidArgumentException; use InvalidArgumentException;
use Stringable;
/** /**
* Immutable class name value object with namespace support * Immutable class name value object with namespace support
*/ */
final readonly class ClassName final readonly class ClassName implements Stringable
{ {
/** /**
* @var class-string * @var class-string

View File

@@ -5,11 +5,12 @@ declare(strict_types=1);
namespace App\Framework\Core\ValueObjects; namespace App\Framework\Core\ValueObjects;
use InvalidArgumentException; use InvalidArgumentException;
use Stringable;
/** /**
* Value object for PHP method names with validation and ClassName integration * Value object for PHP method names with validation and ClassName integration
*/ */
final readonly class MethodName final readonly class MethodName implements Stringable
{ {
private function __construct( private function __construct(
public string $name 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\ClassName;
use App\Framework\Core\ValueObjects\MethodName; use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\DI\Exceptions\InitializerCycleException;
use App\Framework\DI\ValueObjects\DependencyGraphNode; use App\Framework\DI\ValueObjects\DependencyGraphNode;
use App\Framework\Reflection\ReflectionProvider; use App\Framework\Reflection\ReflectionProvider;
@@ -54,9 +55,17 @@ final class InitializerDependencyGraph
/** /**
* Berechnet die optimale Ausführungsreihenfolge basierend auf Dependencies * Berechnet die optimale Ausführungsreihenfolge basierend auf Dependencies
* @return array<string> Sortierte Liste von Return-Types * @return array<string> Sortierte Liste von Return-Types
* @throws InitializerCycleException Wenn circular dependencies gefunden werden
*/ */
public function getExecutionOrder(): array 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->visited = [];
$this->inStack = []; $this->inStack = [];
$result = []; $result = [];
@@ -159,4 +168,78 @@ final class InitializerDependencyGraph
$this->inStack[$returnType] = false; $this->inStack[$returnType] = false;
$result[] = $returnType; $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\ClassName;
use App\Framework\Core\ValueObjects\MethodName; use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\DI\Container; use App\Framework\DI\Container;
use App\Framework\DI\Exceptions\InitializerCycleException;
use App\Framework\DI\Initializer; use App\Framework\DI\Initializer;
use App\Framework\DI\InitializerDependencyGraph; use App\Framework\DI\InitializerDependencyGraph;
use App\Framework\DI\ValueObjects\DependencyGraphNode; 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) { } catch (\Throwable $e) {
// Fallback: Registriere alle Services ohne spezielle Reihenfolge // Andere Fehler: Fallback mit generischer Warnung
$logger = $this->getLogger(); $logger = $this->getLogger();
$logger->warning( $logger->warning(
"Failed to register services with dependency graph, falling back to unordered registration", "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; namespace App\Framework\Http;
use App\Framework\Core\ValueObjects\ClassName; use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\DI\Container; use App\Framework\DI\Container;
use App\Framework\Logging\Logger; use App\Framework\Logging\Logger;
use App\Framework\Reflection\ReflectionProvider; use App\Framework\Reflection\ReflectionProvider;
@@ -168,7 +169,7 @@ final readonly class MiddlewareDependencyResolver
private function getMiddlewareDependencies(ClassName $middlewareClass): array private function getMiddlewareDependencies(ClassName $middlewareClass): array
{ {
try { try {
if (! $middlewareClass->exists()) { if (! $middlewareClass->exists() || MethodName::construct()->existsIn($middlewareClass)) {
return []; return [];
} }