feat(di, cache): add proactive initializer discovery and caching mechanics

- Introduce `InitializerDependencyAnalyzer` to support dependency analysis during cyclic exceptions.
- Add proactive initializer discovery with `InitializerCacheUpdater` for improved performance.
- Integrate discovery cache updates and error handling for seamless caching of found initializers.
- Extend `CyclicDependencyException` with `InitializerDependencyAnalyzer` for enhanced diagnostics and cycle analysis.
This commit is contained in:
2025-11-03 21:08:20 +01:00
parent 1655248de5
commit 247a046f51
4 changed files with 466 additions and 8 deletions

View File

@@ -9,6 +9,7 @@ use App\Framework\DI\Exceptions\ClassNotInstantiable;
use App\Framework\DI\Exceptions\ClassNotResolvableException; use App\Framework\DI\Exceptions\ClassNotResolvableException;
use App\Framework\DI\Exceptions\ClassResolutionException; use App\Framework\DI\Exceptions\ClassResolutionException;
use App\Framework\DI\ProactiveInitializerFinder; use App\Framework\DI\ProactiveInitializerFinder;
use App\Framework\DI\ValueObjects\InitializerInfo;
use App\Framework\DI\Exceptions\ContainerException; use App\Framework\DI\Exceptions\ContainerException;
use App\Framework\DI\Exceptions\CyclicDependencyException; use App\Framework\DI\Exceptions\CyclicDependencyException;
use App\Framework\DI\Exceptions\LazyLoadingException; use App\Framework\DI\Exceptions\LazyLoadingException;
@@ -40,6 +41,8 @@ final class DefaultContainer implements Container
public readonly FrameworkMetricsCollector $metrics; public readonly FrameworkMetricsCollector $metrics;
private readonly InitializerDependencyAnalyzer $dependencyAnalyzer;
public function __construct( public function __construct(
private readonly InstanceRegistry $instances = new InstanceRegistry(), private readonly InstanceRegistry $instances = new InstanceRegistry(),
private readonly BindingRegistry $bindings = new BindingRegistry(), private readonly BindingRegistry $bindings = new BindingRegistry(),
@@ -60,6 +63,7 @@ final class DefaultContainer implements Container
fn (): array => $this->resolving fn (): array => $this->resolving
); );
$this->metrics = new FrameworkMetricsCollector(); $this->metrics = new FrameworkMetricsCollector();
$this->dependencyAnalyzer = new InitializerDependencyAnalyzer($this);
$this->registerSelf(); $this->registerSelf();
$this->instance(ReflectionProvider::class, $this->reflectionProvider); $this->instance(ReflectionProvider::class, $this->reflectionProvider);
@@ -144,7 +148,10 @@ final class DefaultContainer implements Container
if (in_array($class, $this->resolving, true)) { if (in_array($class, $this->resolving, true)) {
throw new CyclicDependencyException( throw new CyclicDependencyException(
dependencyChain: $this->resolving, dependencyChain: $this->resolving,
class: $class class: $class,
code: 0,
previous: null,
dependencyAnalyzer: $this->dependencyAnalyzer
); );
} }
@@ -522,7 +529,8 @@ final class DefaultContainer implements Container
return $container->invoker->invoke($initializerClass, $methodName); return $container->invoker->invoke($initializerClass, $methodName);
}); });
// TODO: Gefundenen Initializer in Discovery Cache nachtragen (später implementieren) // Gefundenen Initializer in Discovery Cache nachtragen
$this->updateDiscoveryCache($initializerInfo);
return true; return true;
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -530,4 +538,74 @@ final class DefaultContainer implements Container
return false; return false;
} }
} }
/**
* Aktualisiert den Discovery Cache mit einem proaktiv gefundenen Initializer
*/
private function updateDiscoveryCache(InitializerInfo $initializerInfo): void
{
try {
$updater = $this->getCacheUpdater();
if ($updater === null) {
return;
}
// Versuche zuerst Registry aus Container zu laden
$registry = $this->has(DiscoveryRegistry::class)
? $this->get(DiscoveryRegistry::class)
: null;
$updater->updateCache($initializerInfo, $registry);
} catch (\Throwable $e) {
// Silently ignore errors during cache update
// Initializer funktioniert trotzdem, nur Cache-Update schlägt fehl
}
}
/**
* Holt oder erstellt einen InitializerCacheUpdater
*/
private function getCacheUpdater(): ?InitializerCacheUpdater
{
try {
// Prüfe ob alle benötigten Abhängigkeiten verfügbar sind
if (!$this->has(\App\Framework\Discovery\Storage\DiscoveryCacheManager::class)) {
// Versuche DiscoveryCacheManager zu erstellen
if (!$this->has(\App\Framework\Cache\Cache::class) ||
!$this->has(\App\Framework\DateTime\Clock::class) ||
!$this->has(\App\Framework\Filesystem\FileSystemService::class)) {
return null;
}
$cache = $this->get(\App\Framework\Cache\Cache::class);
$clock = $this->get(\App\Framework\DateTime\Clock::class);
$fileSystemService = $this->get(\App\Framework\Filesystem\FileSystemService::class);
$cacheManager = new \App\Framework\Discovery\Storage\DiscoveryCacheManager(
cache: $cache,
clock: $clock,
fileSystemService: $fileSystemService
);
} else {
$cacheManager = $this->get(\App\Framework\Discovery\Storage\DiscoveryCacheManager::class);
}
$pathProvider = $this->has(\App\Framework\Core\PathProvider::class)
? $this->get(\App\Framework\Core\PathProvider::class)
: new \App\Framework\Core\PathProvider(getcwd() ?: '.');
$clock = $this->has(\App\Framework\DateTime\Clock::class)
? $this->get(\App\Framework\DateTime\Clock::class)
: new \App\Framework\DateTime\SystemClock();
return new InitializerCacheUpdater(
reflectionProvider: $this->reflectionProvider,
cacheManager: $cacheManager,
pathProvider: $pathProvider,
clock: $clock
);
} catch (\Throwable $e) {
return null;
}
}
} }

View File

@@ -15,12 +15,14 @@ final class CyclicDependencyException extends ContainerException
private readonly string $cycleStart; private readonly string $cycleStart;
private readonly array $fullChain; private readonly array $fullChain;
private readonly array $chainBeforeCycle; private readonly array $chainBeforeCycle;
private readonly ?InitializerDependencyAnalyzer $dependencyAnalyzer;
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,
?InitializerDependencyAnalyzer $dependencyAnalyzer = null
) { ) {
// Speichere vollständige Kette // Speichere vollständige Kette
$this->fullChain = array_merge($dependencyChain, [$class]); $this->fullChain = array_merge($dependencyChain, [$class]);
@@ -44,6 +46,7 @@ final class CyclicDependencyException extends ContainerException
$this->cycle = $cycle; $this->cycle = $cycle;
$this->cycleStart = $cycleStart; $this->cycleStart = $cycleStart;
$this->dependencyAnalyzer = $dependencyAnalyzer;
$context = ExceptionContext::forOperation('dependency_resolution', 'DI') $context = ExceptionContext::forOperation('dependency_resolution', 'DI')
->withData([ ->withData([
@@ -256,7 +259,8 @@ final class CyclicDependencyException extends ContainerException
*/ */
private function findProblematicDependency(array $initializerDependencies, string $interface): array private function findProblematicDependency(array $initializerDependencies, string $interface): array
{ {
$analyzer = new InitializerDependencyAnalyzer(); // Verwende Analyzer aus Exception (wenn verfügbar)
$analyzer = $this->dependencyAnalyzer ?? new InitializerDependencyAnalyzer();
// Methode 1: Rekursive Suche - Finde vollständigen Pfad für jede Dependency // Methode 1: Rekursive Suche - Finde vollständigen Pfad für jede Dependency
foreach ($initializerDependencies as $dependency) { foreach ($initializerDependencies as $dependency) {
@@ -439,7 +443,7 @@ final class CyclicDependencyException extends ContainerException
} }
// Analysiere Dependencies des Initializers // Analysiere Dependencies des Initializers
$analyzer = new InitializerDependencyAnalyzer(); $analyzer = $this->dependencyAnalyzer ?? new InitializerDependencyAnalyzer();
$dependencyAnalysis = $analyzer->analyze($initializerClass); $dependencyAnalysis = $analyzer->analyze($initializerClass);
// Kombiniere Constructor- und container->get() Dependencies // Kombiniere Constructor- und container->get() Dependencies

View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\PathProvider;
use App\Framework\DateTime\Clock;
use App\Framework\DI\ValueObjects\InitializerInfo;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Discovery\Storage\DiscoveryCacheManager;
use App\Framework\Discovery\ValueObjects\AttributeTarget;
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
use App\Framework\Discovery\ValueObjects\DiscoveryContext;
use App\Framework\Discovery\ValueObjects\DiscoveryOptions;
use App\Framework\Discovery\ValueObjects\ScanType;
use App\Framework\Filesystem\FileSystemService;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Reflection\ReflectionProvider;
/**
* Verantwortlich für das Update des Discovery Caches mit proaktiv gefundenen Initializern
*/
final readonly class InitializerCacheUpdater
{
public function __construct(
private ReflectionProvider $reflectionProvider,
private DiscoveryCacheManager $cacheManager,
private PathProvider $pathProvider,
private Clock $clock
) {}
/**
* Aktualisiert den Discovery Cache mit einem proaktiv gefundenen Initializer
*/
public function updateCache(InitializerInfo $initializerInfo, ?DiscoveryRegistry $registry = null): bool
{
try {
// Konvertiere InitializerInfo zu DiscoveredAttribute
$discoveredAttribute = $this->convertToDiscoveredAttribute($initializerInfo);
if ($discoveredAttribute === null) {
return false;
}
// Lade aktuelle DiscoveryRegistry falls nicht übergeben
if ($registry === null) {
$registry = $this->loadDiscoveryRegistry();
}
if ($registry === null) {
return false;
}
// Füge DiscoveredAttribute zur Registry hinzu
$registry->attributes->add(Initializer::class, $discoveredAttribute);
// Speichere aktualisierte Registry zurück in Cache
return $this->storeDiscoveryRegistry($registry);
} catch (\Throwable $e) {
// Silently ignore errors during cache update
// Initializer funktioniert trotzdem, nur Cache-Update schlägt fehl
return false;
}
}
/**
* Konvertiert InitializerInfo zu DiscoveredAttribute
*/
private function convertToDiscoveredAttribute(InitializerInfo $initializerInfo): ?DiscoveredAttribute
{
try {
$initializerClass = $initializerInfo->initializerClass;
$methodName = $initializerInfo->methodName;
// Bestimme filePath via Reflection
$reflection = $this->reflectionProvider->getClass($initializerClass);
$fileName = $reflection->getFileName();
$filePath = $fileName !== false ? FilePath::create($fileName) : null;
// Generiere additionalData via InitializerMapper
$methodReflectionWrapped = $this->reflectionProvider->getMethod(
$initializerClass,
$methodName->toString()
);
// Erstelle Initializer Attribut-Instanz für Mapper (keine Contexts = alle erlaubt)
$initializerAttr = new Initializer();
// Nutze InitializerMapper um additionalData zu generieren
$mapper = new InitializerMapper();
$additionalData = $mapper->map($methodReflectionWrapped, $initializerAttr);
return new DiscoveredAttribute(
className: $initializerClass,
attributeClass: Initializer::class,
target: AttributeTarget::METHOD,
methodName: $methodName,
propertyName: null,
arguments: [],
filePath: $filePath,
additionalData: $additionalData
);
} catch (\Throwable $e) {
// Silently ignore errors during conversion
return null;
}
}
/**
* Lädt die aktuelle DiscoveryRegistry aus Cache
*/
private function loadDiscoveryRegistry(): ?DiscoveryRegistry
{
try {
$context = $this->createDiscoveryContext();
$cached = $this->cacheManager->get($context);
if ($cached !== null) {
return $cached;
}
// Falls Cache leer, erstelle neue Registry
return DiscoveryRegistry::empty();
} catch (\Throwable $e) {
return null;
}
}
/**
* Speichert die DiscoveryRegistry zurück in den Cache
*/
private function storeDiscoveryRegistry(DiscoveryRegistry $registry): bool
{
try {
$context = $this->createDiscoveryContext();
return $this->cacheManager->store($context, $registry);
} catch (\Throwable $e) {
return false;
}
}
/**
* Erstellt einen DiscoveryContext für Cache-Zugriff
*/
private function createDiscoveryContext(): DiscoveryContext
{
$paths = [$this->pathProvider->getSourcePath()];
return new DiscoveryContext(
paths: $paths,
scanType: ScanType::FULL,
options: DiscoveryOptions::default(),
startTime: $this->clock->now()
);
}
}

View File

@@ -14,6 +14,11 @@ namespace App\Framework\DI;
final readonly class InitializerDependencyAnalyzer final readonly class InitializerDependencyAnalyzer
{ {
private const MAX_RECURSION_DEPTH = 4; private const MAX_RECURSION_DEPTH = 4;
public function __construct(
private ?Container $container = null
) {
}
/** /**
* Analysiere Dependencies eines Initializers * Analysiere Dependencies eines Initializers
* *
@@ -351,18 +356,31 @@ final readonly class InitializerDependencyAnalyzer
} }
/** /**
* Hole Dependencies einer Klasse (Constructor-Parameter) * Hole Dependencies einer Klasse (Constructor-Parameter + Array-Elemente)
* *
* @return array<string> * @return array<string>
*/ */
private function getClassDependencies(string $className): array private function getClassDependencies(string $className): array
{ {
try { try {
if (!class_exists($className)) { if (!class_exists($className) && !interface_exists($className)) {
return []; return [];
} }
$reflection = new \ReflectionClass($className); $reflection = new \ReflectionClass($className);
// Wenn es ein Interface ist, versuche die Implementierung zu finden
if ($reflection->isInterface()) {
// Suche nach bekannten Implementierungen (basierend auf Namenskonvention)
$implClass = $this->findInterfaceImplementation($className);
if ($implClass !== null && class_exists($implClass)) {
$reflection = new \ReflectionClass($implClass);
} else {
// Kann keine Dependencies für Interfaces ohne Implementierung finden
return [];
}
}
$constructor = $reflection->getConstructor(); $constructor = $reflection->getConstructor();
if ($constructor === null) { if ($constructor === null) {
@@ -378,10 +396,210 @@ final readonly class InitializerDependencyAnalyzer
} }
} }
return $dependencies; // Zusätzlich: Prüfe ob Parameter Arrays sind, die Klassen-Namen enthalten könnten
// (z.B. TemplateProcessor hat $astTransformers als Array von Klassen-Namen)
foreach ($constructor->getParameters() as $parameter) {
$type = $parameter->getType();
if ($type instanceof \ReflectionNamedType && $type->getName() === 'array') {
// Versuche Klassen-Namen aus dem Array zu extrahieren (via Code-Parsing)
$arrayDeps = $this->extractClassNamesFromArrayParameter($reflection, $parameter);
$dependencies = array_merge($dependencies, $arrayDeps);
}
}
return array_unique($dependencies);
} catch (\Throwable) { } catch (\Throwable) {
return []; return [];
} }
} }
/**
* Extrahiere Klassen-Namen aus Array-Parametern (z.B. $astTransformers = [XComponentTransformer::class])
*
* Sucht sowohl im Constructor als auch in Initializern (__invoke)
*/
private function extractClassNamesFromArrayParameter(\ReflectionClass $class, \ReflectionParameter $parameter): array
{
try {
$fileName = $class->getFileName();
if ($fileName === false || !file_exists($fileName)) {
return [];
}
$fileContent = file_get_contents($fileName);
if ($fileContent === false) {
return [];
}
$paramName = $parameter->getName();
$classes = [];
// 1. Suche im Constructor
$constructor = $class->getConstructor();
if ($constructor !== null) {
$startLine = $constructor->getStartLine();
$endLine = $constructor->getEndLine();
if ($startLine !== false && $endLine !== false) {
$lines = explode("\n", $fileContent);
$constructorLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1);
$constructorCode = implode("\n", $constructorLines);
// Finde Array-Initialisierungen für diesen Parameter
// Pattern: [ClassName::class, ...] oder ['ClassName', ...]
$pattern = '/\$' . preg_quote($paramName, '/') . '\s*=\s*\[(.*?)\]/s';
if (preg_match($pattern, $constructorCode, $matches)) {
$arrayClasses = $this->extractClassesFromArrayContent($matches[1], $fileContent, $constructor);
$classes = array_merge($classes, $arrayClasses);
}
}
}
// 2. Suche auch in __invoke() (für Initializer)
try {
$invokeMethod = $class->getMethod('__invoke');
$startLine = $invokeMethod->getStartLine();
$endLine = $invokeMethod->getEndLine();
if ($startLine !== false && $endLine !== false) {
$lines = explode("\n", $fileContent);
$invokeLines = array_slice($lines, $startLine - 1, $endLine - $startLine + 1);
$invokeCode = implode("\n", $invokeLines);
// Finde Array-Initialisierungen die an diesen Parameter übergeben werden
// Pattern: [$paramName] = [...] oder new Class([...])
// Oder: $paramName = [...]
$pattern = '/\$' . preg_quote($paramName, '/') . '\s*=\s*\[(.*?)\]/s';
if (preg_match($pattern, $invokeCode, $matches)) {
$arrayClasses = $this->extractClassesFromArrayContent($matches[1], $fileContent, $invokeMethod);
$classes = array_merge($classes, $arrayClasses);
}
// Suche auch nach direkten Array-Definitionen die an den Parameter übergeben werden
// z.B. new TemplateProcessor(astTransformers: [XComponentTransformer::class, ...])
$pattern = '/\b' . preg_quote($paramName, '/') . '\s*:\s*\[(.*?)\]/s';
if (preg_match($pattern, $invokeCode, $matches)) {
$arrayClasses = $this->extractClassesFromArrayContent($matches[1], $fileContent, $invokeMethod);
$classes = array_merge($classes, $arrayClasses);
}
}
} catch (\ReflectionException) {
// __invoke() existiert nicht - das ist okay
}
return array_unique($classes);
} catch (\Throwable) {
return [];
}
}
/**
* Extrahiere Klassen-Namen aus Array-Content-String
*/
private function extractClassesFromArrayContent(string $arrayContent, string $fileContent, \ReflectionMethod $method): array
{
// Finde alle Klassen-Namen (::class oder als String)
$classPattern = '/([A-Z][A-Za-z0-9\\\\]+)(::class|\')/';
preg_match_all($classPattern, $arrayContent, $classMatches);
$classes = [];
if (!empty($classMatches[1])) {
foreach ($classMatches[1] as $classMatch) {
// Normalisiere (füge \ am Anfang hinzu wenn nicht vorhanden)
if (!str_starts_with($classMatch, '\\') && !str_starts_with($classMatch, 'App\\')) {
// Versuche vollständigen Namespace zu finden
$fullClassName = $this->resolveClassNameFromMethod($classMatch, $fileContent, $method);
if ($fullClassName !== null) {
$classes[] = $fullClassName;
} elseif (class_exists($classMatch)) {
$classes[] = $classMatch;
}
} elseif (class_exists($classMatch)) {
$classes[] = $classMatch;
}
}
}
return $classes;
}
/**
* Versuche Implementierung eines Interfaces zu finden
*
* Prüft zuerst Container-Bindings, dann Namenskonventionen
*/
private function findInterfaceImplementation(string $interface): ?string
{
// 1. Versuche über Container-Bindings (falls Container verfügbar)
if ($this->container !== null) {
try {
// Prüfe ob Interface gebunden ist
if ($this->container->has($interface)) {
$binding = $this->getBindingForInterface($interface);
if ($binding !== null) {
// Wenn Binding ein String ist (Klassenname), verwende diesen
if (is_string($binding) && class_exists($binding)) {
return $binding;
}
// Wenn Binding ein Objekt ist, verwende dessen Klasse
if (is_object($binding)) {
return $binding::class;
}
// Wenn Binding ein Callable ist, können wir es nicht direkt auflösen
// aber wir können versuchen, es zu instanziieren (wenn kein Zyklus)
// Das ist riskant, also überspringen wir es für jetzt
}
}
} catch (\Throwable) {
// Container-Zugriff fehlgeschlagen - ignoriere und versuche Fallback
}
}
// 2. Versuche Namenskonvention: Interface -> DefaultInterfaceName
$interfaceName = basename(str_replace('\\', '/', $interface));
$namespace = substr($interface, 0, strrpos($interface, '\\'));
// Versuche "Default" + InterfaceName ohne "Interface"
$implName = str_replace('Interface', '', $interfaceName);
$defaultImpl = $namespace . '\\Default' . $implName;
if (class_exists($defaultImpl)) {
return $defaultImpl;
}
// Versuche einfach InterfaceName ohne "Interface"
$simpleImpl = $namespace . '\\' . $implName;
if (class_exists($simpleImpl)) {
return $simpleImpl;
}
return null;
}
/**
* Hole Binding für ein Interface aus dem Container
*/
private function getBindingForInterface(string $interface): callable|string|object|null
{
if ($this->container === null) {
return null;
}
try {
// Versuche über Introspector (sicherer, keine Instanziierung)
// Introspector ist eine readonly Property im Container
if (property_exists($this->container, 'introspector')) {
$introspector = $this->container->introspector;
return $introspector->getBinding($interface);
}
// Fallback: Versuche direkt über BindingRegistry (wenn verfügbar)
// Das ist nicht ideal, aber besser als nichts
return null;
} catch (\Throwable) {
return null;
}
}
} }