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:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
158
src/Framework/DI/InitializerCacheUpdater.php
Normal file
158
src/Framework/DI/InitializerCacheUpdater.php
Normal 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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user