Files
michaelschiemer/src/Framework/DI/DefaultContainer.php
Michael Schiemer 247a046f51 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.
2025-11-03 21:08:20 +01:00

612 lines
24 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\ValueObjects\ClassName;
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;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\DI\FailedInitializerRegistry;
use App\Framework\DI\ValueObjects\FailedInitializer;
use App\Framework\Logging\Logger;
use App\Framework\Logging\ValueObjects\LogContext;
use App\Framework\Metrics\FrameworkMetricsCollector;
use App\Framework\Reflection\CachedReflectionProvider;
use App\Framework\Reflection\ReflectionProvider;
use Stringable;
use Throwable;
final class DefaultContainer implements Container
{
/** @var class-string[] */
private array $resolving = [];
private readonly DependencyResolver $dependencyResolver;
private readonly SingletonDetector $singletonDetector;
private readonly LazyInstantiator $lazyInstantiator;
public readonly MethodInvoker $invoker;
public readonly ContainerIntrospector $introspector;
public readonly FrameworkMetricsCollector $metrics;
private readonly InitializerDependencyAnalyzer $dependencyAnalyzer;
public function __construct(
private readonly InstanceRegistry $instances = new InstanceRegistry(),
private readonly BindingRegistry $bindings = new BindingRegistry(),
private readonly ReflectionProvider $reflectionProvider = new CachedReflectionProvider(),
) {
$this->dependencyResolver = new DependencyResolver($this->reflectionProvider, $this);
$this->singletonDetector = new SingletonDetector($this->reflectionProvider, $this->instances);
$this->lazyInstantiator = new LazyInstantiator(
$this->reflectionProvider,
$this->createInstance(...)
);
$this->invoker = new MethodInvoker($this, $this->reflectionProvider);
$this->introspector = new ContainerIntrospector(
$this,
$this->instances,
$this->bindings,
$this->reflectionProvider,
fn (): array => $this->resolving
);
$this->metrics = new FrameworkMetricsCollector();
$this->dependencyAnalyzer = new InitializerDependencyAnalyzer($this);
$this->registerSelf();
$this->instance(ReflectionProvider::class, $this->reflectionProvider);
}
public function bind(ClassName|Stringable|string $abstract, callable|string|object $concrete): void
{
$abstract = (string) $abstract;
$this->bindings->bind($abstract, $concrete);
// Only clear caches for valid class names, skip string keys like 'filesystem.storage.local'
if (class_exists($abstract) || interface_exists($abstract)) {
$this->clearCaches(ClassName::create($abstract));
}
}
public function singleton(ClassName|Stringable|string $abstract, callable|string|object $concrete): void
{
$abstract = (string) $abstract;
$this->bind($abstract, $concrete);
$this->instances->markAsSingleton($abstract);
}
public function instance(ClassName|Stringable|string $abstract, object $instance): void
{
$this->instances->setInstance($abstract, $instance);
}
/**
* @template T of object
* @param class-string<T>|ClassName|Stringable $class
* @return T
*/
public function get(ClassName|Stringable|string $class): object
{
$className = (string) $class;
// Bereits instanziierte Objekte zurückgeben
$singleton = $this->instances->getSingleton($className);
if ($singleton !== null) {
return $singleton;
}
if ($this->instances->hasInstance($className)) {
return $this->instances->getInstance($className);
}
// Lazy Loading versuchen
if ($this->lazyInstantiator->canUseLazyLoading($className, $this->instances)) {
try {
return $this->lazyInstantiator->createLazyInstance($className);
} catch (LazyLoadingException $e) {
// Log LazyLoading-Fehler und versuche Fallback (Logger ist immer verfügbar)
$this->get(Logger::class)->warning(
"Lazy loading failed for {$className}, falling back to normal instantiation",
LogContext::withException($e)
);
// Fallback: Versuche normale Instanziierung
try {
return $this->createInstance($className);
} catch (\Throwable $fallbackException) {
// Wenn Fallback auch fehlschlägt, ursprüngliche LazyLoadingException werfen
throw $e;
}
} catch (\Throwable $e) {
// Andere Exceptions direkt weiterwerfen
throw $e;
}
}
return $this->createInstance($className);
}
/**
* @template T of object
* @param class-string<T> $class
* @return T
*/
private function createInstance(string $class): object
{
if (in_array($class, $this->resolving, true)) {
throw new CyclicDependencyException(
dependencyChain: $this->resolving,
class: $class,
code: 0,
previous: null,
dependencyAnalyzer: $this->dependencyAnalyzer
);
}
$this->resolving[] = $class;
try {
$instance = $this->buildInstance($class);
// Only check singleton attribute for actual classes
if ((class_exists($class) || interface_exists($class)) &&
$this->singletonDetector->isSingleton(ClassName::create($class))) {
$this->instances->setSingleton($class, $instance);
} else {
$this->instances->setInstance($class, $instance);
}
return $instance;
} catch (Throwable $e) {
// Track resolution failures
$this->metrics->increment('container.resolve.failures');
throw $e;
} finally {
array_pop($this->resolving);
}
}
private function buildInstance(string $class): object
{
if ($this->bindings->hasBinding($class)) {
return $this->resolveBinding($class, $this->bindings->getBinding($class));
}
// For string keys without bindings, throw immediately
if (!class_exists($class) && !interface_exists($class)) {
throw new ClassNotResolvableException(
class: $class,
availableBindings: array_keys($this->bindings->getAllBindings())
);
}
$className = ClassName::create($class);
// Enhanced diagnostics for missing bindings
try {
$reflection = $this->reflectionProvider->getClass($className);
// Check if class is instantiable using the framework's method
if (! $reflection->isInstantiable()) {
// Proaktive Suche nach Initializern für Interfaces
if ($reflection->isInterface() && $this->tryFindAndRegisterInitializer($class)) {
// Initializer gefunden und registriert - versuche erneut zu resolven
return $this->get($class);
}
$this->throwDetailedBindingException($class);
}
$dependencies = $this->dependencyResolver->resolveDependencies($className);
return $reflection->newInstance(...$dependencies->toArray());
} catch (ContainerException $e) {
// If it's already a ContainerException, just re-throw
throw $e;
} catch (\RuntimeException|\ReflectionException $e) {
// Wrap with binding information
throw ClassResolutionException::fromRuntimeException(
class: $class,
exception: $e,
availableBindings: array_keys($this->bindings->getAllBindings()),
dependencyChain: $this->resolving
);
}
}
private function throwDetailedBindingException(string $class): never
{
$availableBindings = array_keys($this->bindings->getAllBindings());
// Bestimme den Typ der Klasse mit Reflection
$className = ClassName::create($class);
$isInterface = false;
$isAbstract = false;
$isTrait = false;
try {
$reflection = $this->reflectionProvider->getClass($className);
$isInterface = $reflection->isInterface();
$isAbstract = $reflection->isAbstract();
$isTrait = $reflection->isTrait();
} catch (\Throwable $e) {
// Fallback zu einfachen Prüfungen wenn Reflection fehlschlägt
$isInterface = interface_exists($class);
// Für Abstract und Trait können wir nicht so einfach prüfen, aber wir haben bereits
// festgestellt dass die Klasse nicht instanziierbar ist
}
// Try to get DiscoveryRegistry from container and include discovered initializers
$discoveredInitializers = [];
$matchingInitializers = [];
$suggestedInitializer = null;
$failedInitializer = null;
if ($this->has(DiscoveryRegistry::class)) {
try {
$discoveryRegistry = $this->get(DiscoveryRegistry::class);
$initializerResults = $discoveryRegistry->attributes->get(Initializer::class);
if (! empty($initializerResults)) {
$discoveredInitializers = array_map(
fn($attr) => $attr->className->getFullyQualified(),
$initializerResults
);
// Spezielle Behandlung für Interfaces: Suche nach Initializern die dieses Interface zurückgeben
if ($isInterface) {
foreach ($initializerResults as $initializer) {
$returnType = $initializer->additionalData['return'] ?? null;
if ($returnType === $class) {
$matchingInitializers[] = $initializer->className->getFullyQualified();
}
}
// Vorschlag basierend auf Interface-Name (z.B. ComponentRegistryInterface -> ComponentRegistryInitializer)
$interfaceName = basename(str_replace('\\', '/', $class));
$suggestedName = str_replace('Interface', '', $interfaceName) . 'Initializer';
foreach ($discoveredInitializers as $initializer) {
if (str_contains($initializer, $suggestedName)) {
$suggestedInitializer = $initializer;
break;
}
}
}
}
} catch (\Throwable $e) {
// Silently ignore errors when trying to access DiscoveryRegistry to avoid masking the original error
}
}
// Prüfe ob ein Initializer für dieses Interface fehlgeschlagen ist
if ($this->has(FailedInitializerRegistry::class)) {
try {
$failedRegistry = $this->get(FailedInitializerRegistry::class);
if ($failedRegistry->hasFailedInitializer($class)) {
$failedInitializer = $failedRegistry->getFailedInitializer($class);
}
} catch (\Throwable $e) {
// Silently ignore errors when trying to access FailedInitializerRegistry
}
}
throw ClassNotInstantiable::fromContainerContext(
class: $class,
dependencyChain: $this->resolving,
availableBindings: $availableBindings,
discoveredInitializers: $discoveredInitializers,
matchingInitializers: $matchingInitializers,
suggestedInitializer: $suggestedInitializer,
failedInitializer: $failedInitializer,
isInterface: $isInterface,
isAbstract: $isAbstract,
isTrait: $isTrait
);
}
private function resolveBinding(string $class, callable|string|object $concrete): object
{
try {
return match (true) {
is_callable($concrete) => $concrete($this),
is_string($concrete) => $this->get($concrete),
/* @var object $concrete */
default => $concrete
};
} catch (ContainerException $e) {
// Re-throw ContainerExceptions as-is (they already have proper context)
throw $e;
} catch (\Throwable $e) {
// Determine binding type for better error messages
$bindingType = match (true) {
is_callable($concrete) => 'callable',
is_string($concrete) => 'string',
default => 'object'
};
// Für callable bindings: Prüfe ob ein Initializer fehlgeschlagen ist
$failedInitializer = null;
$matchingInitializer = null;
if ($bindingType === 'callable') {
// Prüfe ob ein fehlgeschlagener Initializer für diese Klasse existiert
if ($this->has(FailedInitializerRegistry::class)) {
try {
$failedRegistry = $this->get(FailedInitializerRegistry::class);
if ($failedRegistry->hasFailedInitializer($class)) {
$failedInitializer = $failedRegistry->getFailedInitializer($class);
}
} catch (\Throwable $registryError) {
// Silently ignore errors when trying to access FailedInitializerRegistry
}
}
// Suche auch in DiscoveryRegistry nach Initializern die diese Klasse zurückgeben
if ($failedInitializer === null && $this->has(DiscoveryRegistry::class)) {
try {
$discoveryRegistry = $this->get(DiscoveryRegistry::class);
$initializerResults = $discoveryRegistry->attributes->get(Initializer::class);
if (! empty($initializerResults)) {
foreach ($initializerResults as $initializer) {
$returnType = $initializer->additionalData['return'] ?? null;
if ($returnType === $class) {
$matchingInitializer = $initializer->className->getFullyQualified();
break;
}
}
}
} catch (\Throwable $registryError) {
// Silently ignore errors when trying to access DiscoveryRegistry
}
}
}
throw ClassResolutionException::fromBindingResolution(
class: $class,
previous: $e,
availableBindings: array_keys($this->bindings->getAllBindings()),
dependencyChain: $this->resolving,
bindingType: $bindingType,
failedInitializer: $failedInitializer,
matchingInitializer: $matchingInitializer
);
}
}
/** @param class-string $class */
public function has(ClassName|Stringable|string $class): bool
{
$class = (string) $class;
return $this->instances->hasSingleton($class)
|| $this->instances->hasInstance($class)
|| $this->bindings->hasBinding($class)
|| $this->canAutoWire($class);
}
/**
* Prüft ob eine Klasse automatisch instanziiert werden kann (auto-wiring)
* @param string $class Klassenname
*/
private function canAutoWire(string $class): bool
{
if (empty($class)) {
return false;
}
$className = ClassName::create($class);
if (! $className->exists()) {
return false;
}
try {
return $this->reflectionProvider->getClass($className)->isInstantiable();
} catch (\Throwable $e) {
return false;
}
}
/** @param class-string $class */
public function forget(ClassName|Stringable|string $class): void
{
$class = (string) $class;
$this->instances->forget($class);
$this->bindings->forget($class);
$this->clearCaches(ClassName::create($class));
}
public function flush(): void
{
$this->instances->flush();
$this->bindings->flush();
$this->reflectionProvider->flush();
$this->dependencyResolver->flushCache();
$this->lazyInstantiator->flushFactories();
// Container selbst wieder registrieren
$this->registerSelf();
}
public function getRegisteredServices(): array
{
return array_merge(
$this->instances->getAllRegistered(),
$this->bindings->getAllBindings()
);
}
/**
* Get all registered service IDs (for debugging/admin)
* @return array<string>
*/
public function getServiceIds(): array
{
return array_keys($this->getRegisteredServices());
}
private function clearCaches(ClassName $className): void
{
$this->reflectionProvider->forget($className);
$this->dependencyResolver->clearCache($className);
$this->lazyInstantiator->forgetFactory($className->getFullyQualified());
}
private function registerSelf(): void
{
$this->instances->setSingleton(self::class, $this);
$this->instances->setSingleton(DefaultContainer::class, $this);
$this->instances->setSingleton(Container::class, $this);
}
/**
* Lazy-getter für ProactiveInitializerFinder
*/
private function getProactiveFinder(): ProactiveInitializerFinder
{
// Erstelle Finder nur wenn benötigt
$discoveryRegistry = $this->has(DiscoveryRegistry::class) ? $this->get(DiscoveryRegistry::class) : null;
if ($discoveryRegistry === null) {
// Fallback: Leere Registry erstellen
$discoveryRegistry = \App\Framework\Discovery\Results\DiscoveryRegistry::empty();
}
$fileScanner = $this->has(\App\Framework\Filesystem\FileScanner::class)
? $this->get(\App\Framework\Filesystem\FileScanner::class)
: new \App\Framework\Filesystem\FileScanner(null, null, new \App\Framework\Filesystem\FileSystemService());
$fileSystemService = new \App\Framework\Filesystem\FileSystemService();
$classExtractor = new \App\Framework\Discovery\Processing\ClassExtractor($fileSystemService);
$pathProvider = $this->has(\App\Framework\Core\PathProvider::class)
? $this->get(\App\Framework\Core\PathProvider::class)
: new \App\Framework\Core\PathProvider(getcwd() ?: '.');
return new ProactiveInitializerFinder(
reflectionProvider: $this->reflectionProvider,
discoveryRegistry: $discoveryRegistry,
fileScanner: $fileScanner,
classExtractor: $classExtractor,
pathProvider: $pathProvider
);
}
/**
* Versucht proaktiv einen Initializer für ein Interface zu finden und zu registrieren
*
* @param string $interface Interface-Klasse
* @return bool True wenn Initializer gefunden und registriert wurde, false sonst
*/
private function tryFindAndRegisterInitializer(string $interface): bool
{
try {
$finder = $this->getProactiveFinder();
$initializerInfo = $finder->findInitializerForInterface($interface);
if ($initializerInfo === null) {
return false;
}
// Registriere Initializer als lazy binding
$initializerClass = $initializerInfo->initializerClass->getFullyQualified();
$methodName = $initializerInfo->methodName->toString();
$this->singleton($interface, function (Container $container) use ($initializerClass, $methodName) {
$instance = $container->get($initializerClass);
return $container->invoker->invoke($initializerClass, $methodName);
});
// Gefundenen Initializer in Discovery Cache nachtragen
$this->updateDiscoveryCache($initializerInfo);
return true;
} catch (\Throwable $e) {
// Silently ignore errors during proactive search
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;
}
}
}