diff --git a/src/Framework/DI/DefaultContainer.php b/src/Framework/DI/DefaultContainer.php index 5f5f095b..6c101a2e 100644 --- a/src/Framework/DI/DefaultContainer.php +++ b/src/Framework/DI/DefaultContainer.php @@ -9,6 +9,7 @@ use App\Framework\DI\Exceptions\ClassNotInstantiable; use App\Framework\DI\Exceptions\ClassNotResolvableException; use App\Framework\DI\Exceptions\ClassResolutionException; use App\Framework\DI\ProactiveInitializerFinder; +use App\Framework\DI\ValueObjects\InitializerInfo; use App\Framework\DI\Exceptions\ContainerException; use App\Framework\DI\Exceptions\CyclicDependencyException; use App\Framework\DI\Exceptions\LazyLoadingException; @@ -40,6 +41,8 @@ final class DefaultContainer implements Container public readonly FrameworkMetricsCollector $metrics; + private readonly InitializerDependencyAnalyzer $dependencyAnalyzer; + public function __construct( private readonly InstanceRegistry $instances = new InstanceRegistry(), private readonly BindingRegistry $bindings = new BindingRegistry(), @@ -60,6 +63,7 @@ final class DefaultContainer implements Container fn (): array => $this->resolving ); $this->metrics = new FrameworkMetricsCollector(); + $this->dependencyAnalyzer = new InitializerDependencyAnalyzer($this); $this->registerSelf(); $this->instance(ReflectionProvider::class, $this->reflectionProvider); @@ -144,7 +148,10 @@ final class DefaultContainer implements Container if (in_array($class, $this->resolving, true)) { throw new CyclicDependencyException( 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); }); - // TODO: Gefundenen Initializer in Discovery Cache nachtragen (später implementieren) + // Gefundenen Initializer in Discovery Cache nachtragen + $this->updateDiscoveryCache($initializerInfo); return true; } catch (\Throwable $e) { @@ -530,4 +538,74 @@ final class DefaultContainer implements Container 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; + } + } } diff --git a/src/Framework/DI/Exceptions/CyclicDependencyException.php b/src/Framework/DI/Exceptions/CyclicDependencyException.php index f38be190..61a1195b 100644 --- a/src/Framework/DI/Exceptions/CyclicDependencyException.php +++ b/src/Framework/DI/Exceptions/CyclicDependencyException.php @@ -15,12 +15,14 @@ final class CyclicDependencyException extends ContainerException private readonly string $cycleStart; private readonly array $fullChain; private readonly array $chainBeforeCycle; + private readonly ?InitializerDependencyAnalyzer $dependencyAnalyzer; public function __construct( array $dependencyChain, string $class, int $code = 0, - ?\Throwable $previous = null + ?\Throwable $previous = null, + ?InitializerDependencyAnalyzer $dependencyAnalyzer = null ) { // Speichere vollständige Kette $this->fullChain = array_merge($dependencyChain, [$class]); @@ -44,6 +46,7 @@ final class CyclicDependencyException extends ContainerException $this->cycle = $cycle; $this->cycleStart = $cycleStart; + $this->dependencyAnalyzer = $dependencyAnalyzer; $context = ExceptionContext::forOperation('dependency_resolution', 'DI') ->withData([ @@ -256,7 +259,8 @@ final class CyclicDependencyException extends ContainerException */ 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 foreach ($initializerDependencies as $dependency) { @@ -439,7 +443,7 @@ final class CyclicDependencyException extends ContainerException } // Analysiere Dependencies des Initializers - $analyzer = new InitializerDependencyAnalyzer(); + $analyzer = $this->dependencyAnalyzer ?? new InitializerDependencyAnalyzer(); $dependencyAnalysis = $analyzer->analyze($initializerClass); // Kombiniere Constructor- und container->get() Dependencies diff --git a/src/Framework/DI/InitializerCacheUpdater.php b/src/Framework/DI/InitializerCacheUpdater.php new file mode 100644 index 00000000..e9b6453a --- /dev/null +++ b/src/Framework/DI/InitializerCacheUpdater.php @@ -0,0 +1,158 @@ +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() + ); + } +} + diff --git a/src/Framework/DI/InitializerDependencyAnalyzer.php b/src/Framework/DI/InitializerDependencyAnalyzer.php index 4e81b9a9..e54eb45b 100644 --- a/src/Framework/DI/InitializerDependencyAnalyzer.php +++ b/src/Framework/DI/InitializerDependencyAnalyzer.php @@ -14,6 +14,11 @@ namespace App\Framework\DI; final readonly class InitializerDependencyAnalyzer { private const MAX_RECURSION_DEPTH = 4; + + public function __construct( + private ?Container $container = null + ) { + } /** * 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 */ private function getClassDependencies(string $className): array { try { - if (!class_exists($className)) { + if (!class_exists($className) && !interface_exists($className)) { return []; } $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(); 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) { 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; + } + } }