Files
michaelschiemer/src/Framework/Discovery/InitializerProcessor.php
Michael Schiemer 0ca382f80b 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
2025-11-03 15:37:40 +01:00

237 lines
9.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Discovery;
use App\Framework\Context\ContextType;
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;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Reflection\ReflectionProvider;
/**
* Processes and executes Initializers discovered by the Discovery system
*
* Handles context filtering, dependency graph construction, and lazy service registration.
* Separated from DiscoveryServiceBootstrapper for Single Responsibility Principle.
*/
final readonly class InitializerProcessor
{
public function __construct(
private Container $container,
private ReflectionProvider $reflectionProvider,
private ExecutionContext $executionContext,
) {}
/**
* Intelligente Initializer-Verarbeitung mit Dependency-Graph:
* - Context-Filter: Nur für passende Execution-Contexts
* - void/null Return: Sofort ausführen (Setup)
* - Konkreter Return-Type: Dependency-Graph basierte Registrierung
*/
public function processInitializers(DiscoveryRegistry $results): void
{
$logger = $this->getLogger();
$initializerResults = $results->attributes->get(Initializer::class);
$logger->debug("InitializerProcessor: Processing " . count($initializerResults) . " initializers");
$dependencyGraph = new InitializerDependencyGraph($this->reflectionProvider);
// Phase 1: Setup-Initializer sofort ausführen & Service-Initializer zum Graph hinzufügen
foreach ($initializerResults as $discoveredAttribute) {
/** @var Initializer $initializer */
$initializer = $discoveredAttribute->createAttributeInstance();
// The actual initializer data is in the additionalData from the InitializerMapper
$initializerData = $discoveredAttribute->additionalData;
if (! $initializerData) {
continue;
}
// Context-Filter: Prüfe ob Initializer für aktuellen Context erlaubt ist
// The contexts data is directly in the additionalData
// Wenn contexts null ist, ist der Initializer für alle Contexts verfügbar
if (
isset($initializerData['contexts'])
&& $initializerData['contexts'] !== null
&& ! $this->isContextAllowed(...$initializerData['contexts'])
) {
continue;
}
$methodName = $discoveredAttribute->methodName ?? MethodName::invoke();
// The return type is directly in the additionalData from the InitializerMapper
$returnType = $initializerData['return'] ?? null;
try {
// Setup-Initializer: void/null Return → Sofort ausführen
if ($returnType === null || $returnType === 'void') {
$this->container->invoker->invoke($discoveredAttribute->className, $methodName->toString());
}
// Service-Initializer: Konkreter Return-Type → Zum Dependency-Graph hinzufügen
else {
$dependencyGraph->addInitializer($returnType, $discoveredAttribute->className, $methodName);
}
} catch (\Throwable $e) {
$logger->error(
"Failed to register initializer: {$discoveredAttribute->className->toString()}::{$methodName->toString()}",
LogContext::withExceptionAndData($e, [
'class' => $discoveredAttribute->className->toString(),
'method' => $methodName->toString(),
'return_type' => $returnType,
])
);
// Skip failed initializers to prevent breaking the entire application
}
}
// Phase 2: Service-Initializer in optimaler Reihenfolge registrieren
$this->registerServicesWithDependencyGraph($dependencyGraph);
}
/**
* Registriert Services basierend auf Dependency-Graph in optimaler Reihenfolge
*/
private function registerServicesWithDependencyGraph(InitializerDependencyGraph $graph): void
{
try {
$executionOrder = $graph->getExecutionOrder();
foreach ($executionOrder as $returnType) {
if ($graph->hasNode($returnType)) {
/** @var DependencyGraphNode $node */
$node = $graph->getNode($returnType);
$this->registerLazyService(
$returnType,
$node->getClassName(),
$node->getMethodName()
);
}
}
} 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) {
// Andere Fehler: Fallback mit generischer Warnung
$logger = $this->getLogger();
$logger->warning(
"Failed to register services with dependency graph, falling back to unordered registration",
LogContext::withException($e)
);
/** @var string $returnType */
foreach ($graph->getNodes() as $returnType => $node) {
$this->registerLazyService(
$returnType,
$node->getClassName(),
$node->getMethodName()
);
}
}
}
/**
* Prüft ob ein Initializer im aktuellen Context ausgeführt werden darf
*/
private function isContextAllowed(mixed ...$contexts): bool
{
$currentContext = $this->executionContext->getType();
foreach ($contexts as $context) {
// Handle both ContextType objects and string values (from cache deserialization)
if ($context instanceof ContextType) {
if ($currentContext === $context) {
return true;
}
} elseif (is_string($context)) {
if ($currentContext->value === $context) {
return true;
}
}
}
return false;
}
/**
* Registriert einen Service lazy im Container mit Dual-Registration für Interfaces
*/
private function registerLazyService(string $returnType, string $className, string $methodName): void
{
$factory = function ($container) use ($className, $methodName, $returnType) {
try {
$instance = $container->invoker->invoke(ClassName::create($className), $methodName);
} catch (\Throwable $e) {
$container->get(Logger::class)->error("Failed to invoke initializer method {$methodName} for class {$className}: {$e->getMessage()}");
throw $e;
}
// Wenn das ein Interface ist, registriere auch die konkrete Klasse automatisch
if (interface_exists($returnType)) {
$concreteClass = get_class($instance);
if (! $container->has($concreteClass)) {
$container->instance($concreteClass, $instance);
}
}
return $instance;
};
try {
// Registriere den Return-Type (Interface oder konkrete Klasse)
$this->container->singleton($returnType, $factory);
} catch (\Throwable $e) {
$logger = $this->getLogger();
$logger->error(
"Failed to register lazy service for return type: {$returnType}",
LogContext::withExceptionAndData($e, [
'return_type' => $returnType,
'class' => $className,
'method' => $methodName,
])
);
// Service registration failed - continue to prevent breaking the entire application
}
}
/**
* Gibt den Logger zurück (ist im Framework immer verfügbar)
*/
private function getLogger(): Logger
{
return $this->container->get(Logger::class);
}
}