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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
62
src/Framework/DI/Exceptions/InitializerCycleException.php
Normal file
62
src/Framework/DI/Exceptions/InitializerCycleException.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 [];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user