Enable Discovery debug logging for production troubleshooting

- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
2025-08-11 20:13:26 +02:00
parent 59fd3dd3b1
commit 55a330b223
3683 changed files with 2956207 additions and 16948 deletions

View File

@@ -1,79 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\AttributeCompiler;
use App\Framework\Core\AttributeMapper;
final class AttributeProcessorRegistry
{
/**
* @var AttributeMapper[]
*/
private array $mappers = [];
/**
* @var AttributeCompiler[]
*/
private array $compilers = [];
public function __construct(private readonly DefaultContainer $container)
{
}
/**
* Registriert einen AttributeMapper
*/
public function registerMapper(string $mapperClass): self
{
$this->mappers[] = $mapperClass;
return $this;
}
/**
* Registriert einen AttributeCompiler
*/
public function registerCompiler(string $compilerClass): self
{
$this->compilers[] = $compilerClass;
return $this;
}
/**
* Verarbeitet alle Attribute mit den registrierten Mappern und Compilern
*/
public function processAll(array $discoveryResults): array
{
$compiledResults = [];
$mappedByType = [];
// Mapper-Instanzen erstellen
$mapperInstances = [];
foreach ($this->mappers as $mapperClass) {
$mapper = $this->container->get($mapperClass);
$mapperInstances[$mapper->getAttributeClass()] = $mapper;
}
// Compiler-Instanzen erstellen
$compilerInstances = [];
foreach ($this->compilers as $compilerClass) {
$compiler = $this->container->get($compilerClass);
$compilerInstances[$compiler->getAttributeClass()] = $compiler;
}
// Attribute kompilieren, für die sowohl Mapper als auch Compiler existieren
foreach ($discoveryResults as $attributeClass => $attributeData) {
if (isset($mapperInstances[$attributeClass]) && isset($compilerInstances[$attributeClass])) {
// Wenn ein Compiler vorhanden ist, verwenden wir ihn zur Kompilierung
$compiledResults[$attributeClass] = $compilerInstances[$attributeClass]->compile($attributeData);
} elseif (isset($mapperInstances[$attributeClass])) {
// Ohne Compiler geben wir die gemappten Daten direkt zurück
$compiledResults[$attributeClass] = $attributeData;
}
}
return $compiledResults;
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
/**
@@ -29,7 +31,17 @@ final class BindingRegistry
unset($this->bindings[$abstract]);
}
public function flush(): void
{
$this->bindings = [];
}
public function getAllBindings(): array
{
return $this->bindings;
}
public function getAllKeys(): array
{
return array_keys($this->bindings);
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
interface Container
@@ -12,9 +14,14 @@ interface Container
* @return T
*/
public function get(string $class): object;
public function has(string $class): bool;
public function bind(string $abstract, callable|string|object $concrete): void;
public function singleton(string $abstract, callable|string|object $concrete): void;
public function instance(string $abstract, object $instance): void;
public function forget(string $class): void;
}

View File

@@ -1,142 +1,367 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
class ContainerCompiler
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Dependency\Dependency;
use App\Framework\DI\Dependency\DependencyType;
use App\Framework\Reflection\ReflectionProvider;
/**
* Compiles a Container into optimized PHP code for production use
*/
final readonly class ContainerCompiler
{
public function compile(DefaultContainer $container, array $services, string $outputFile): void
{
$code = "<?php\n\nnamespace App\\Framework\\DI;\n\nclass CompiledContainer extends Container\n{\n";
// get()-Methode überschreiben
$code .= " public function get(string \$class): object\n {\n";
$code .= " // Singleton- und Instanz-Prüfung vom Basiscontainer\n";
$code .= " if (isset(\$this->singletons[\$class])) {\n";
$code .= " return \$this->singletons[\$class];\n";
$code .= " }\n\n";
$code .= " if (isset(\$this->instances[\$class])) {\n";
$code .= " return \$this->instances[\$class];\n";
$code .= " }\n\n";
// Switch-Statement für bekannte Services
$code .= " // Optimierte Factory-Methoden für bekannte Services\n";
$code .= " switch (\$class) {\n";
foreach ($services as $service) {
$code .= " case \\{$service}::class:\n";
$code .= " return \$this->create" . $this->getServiceMethodName($service) . "();\n";
}
$code .= " default:\n";
$code .= " // Fallback auf Standard-Container\n";
$code .= " return parent::get(\$class);\n";
$code .= " }\n";
$code .= " }\n\n";
// Factory-Methoden für jeden bekannten Service
foreach ($services as $service) {
$code .= $this->generateFactoryMethod($service, $container);
}
$code .= "}\n";
file_put_contents($outputFile, $code);
public function __construct(
private ReflectionProvider $reflectionProvider,
private DependencyResolver $dependencyResolver
) {
}
private function getServiceMethodName(string $service): string
/**
* Compile container to PHP file with all bindings and dependencies
*/
public function compile(DefaultContainer $container, string $outputPath, array $options = []): void
{
// Erzeugt einen methodenkompatiblen Namen aus dem Klassennamen
$parts = explode('\\', $service);
return end($parts);
$code = $this->generateContainerCode($container, $options);
$this->writeToFile($outputPath, $code);
}
private function generateFactoryMethod(string $service, DefaultContainer $container): string
/**
* Generate the complete compiled container PHP code
*/
private function generateContainerCode(DefaultContainer $container, array $options): string
{
// Analysiert den Service und seine Abhängigkeiten
$reflection = new \ReflectionClass($service);
$constructor = $reflection->getConstructor();
$timestamp = date('Y-m-d H:i:s');
$hash = $this->generateContainerHash($container);
$methodName = 'create' . $this->getServiceMethodName($service);
$code = " private function {$methodName}(): \\{$service}\n {\n";
$bindings = $this->compileBindings($container);
$singletons = $this->compileSingletons($container);
$dependencies = $this->compileDependencyMethods($container);
// Dependency-Analyse und optimierter Instantiierungs-Code
if ($constructor) {
$params = [];
foreach ($constructor->getParameters() as $param) {
$paramType = $param->getType();
if ($paramType instanceof \ReflectionNamedType && !$paramType->isBuiltin()) {
$typeName = $paramType->getName();
$params[] = "\$this->get(\\{$typeName}::class)";
} elseif ($param->isDefaultValueAvailable()) {
$default = var_export($param->getDefaultValue(), true);
$params[] = $default;
} elseif ($paramType instanceof \ReflectionNamedType && $paramType->allowsNull()) {
$params[] = 'null';
} else {
// Nicht auflösbar, Fallback
$paramName = $param->getName();
$params[] = "/* Parameter '{$paramName}' konnte nicht aufgelöst werden */";
}
return <<<PHP
<?php
declare(strict_types=1);
/**
* Compiled Container
* Generated: {$timestamp}
* Hash: {$hash}
*
* WARNING: This file is auto-generated. Do not edit manually!
*/
namespace App\Framework\DI\Compiled;
use App\Framework\DI\Container;
final class CompiledContainer implements Container
{
private array \$instances = [];
private array \$singletons = [];
public function __construct()
{
\$this->initializeSingletons();
}
public function get(string \$class): object
{
// Singleton check
if (isset(\$this->singletons[\$class])) {
if (isset(\$this->instances[\$class])) {
return \$this->instances[\$class];
}
$paramsStr = implode(', ', $params);
$code .= " return new \\{$service}({$paramsStr});\n";
} else {
$code .= " return new \\{$service}();\n";
\$instance = \$this->createInstance(\$class);
\$this->instances[\$class] = \$instance;
return \$instance;
}
$code .= " }\n\n";
return $code;
return \$this->createInstance(\$class);
}
// Neue Methode zur Abhängigkeitsanalyse hinzufügen
public function analyzeDependencies(DefaultContainer $container, array $services): array
public function has(string \$class): bool
{
$dependencies = [];
$analyzedServices = [];
// Rekursive Analyse von Service-Abhängigkeiten
foreach ($services as $service) {
$this->analyzeServiceDependencies($service, $dependencies, $analyzedServices);
}
return array_unique(array_merge($services, array_keys($dependencies)));
return method_exists(\$this, 'create' . str_replace('\\\\', '_', \$class));
}
private function analyzeServiceDependencies(
string $service,
array &$dependencies,
array &$analyzedServices
): void {
// Vermeidung von Zyklen
if (isset($analyzedServices[$service])) {
return;
public function bind(string \$class, callable|string|object \$concrete): void
{
throw new \\RuntimeException('Cannot bind to compiled container. Recompile instead.');
}
public function singleton(string \$class, callable|string|object \$concrete): void
{
throw new \\RuntimeException('Cannot add singletons to compiled container. Recompile instead.');
}
public function instance(string \$class, object \$instance): void
{
\$this->instances[\$class] = \$instance;
}
private function initializeSingletons(): void
{
{$singletons}
}
private function createInstance(string \$class): object
{
return match (\$class) {
{$bindings}
default => throw new \\InvalidArgumentException("Class {\$class} is not bound in the container")
};
}
{$dependencies}
}
PHP;
}
/**
* Compile all container bindings to optimized creation calls
*/
private function compileBindings(DefaultContainer $container): string
{
$bindings = [];
$bindingRegistry = $this->getBindingRegistry($container);
foreach ($bindingRegistry->getAllBindings() as $class => $binding) {
$methodName = $this->getMethodName($class);
$bindings[] = " '{$class}' => \$this->{$methodName}()";
}
$analyzedServices[$service] = true;
return implode(",\n", $bindings);
}
/**
* Compile singleton registrations
*/
private function compileSingletons(DefaultContainer $container): string
{
$code = [];
$instanceRegistry = $this->getInstanceRegistry($container);
foreach ($instanceRegistry->getSingletons() as $class) {
$code[] = " \$this->singletons['{$class}'] = true;";
}
return implode("\n", $code);
}
/**
* Compile dependency creation methods
*/
private function compileDependencyMethods(DefaultContainer $container): string
{
$methods = [];
$bindingRegistry = $this->getBindingRegistry($container);
foreach ($bindingRegistry->getAllBindings() as $class => $binding) {
$methods[] = $this->compileDependencyMethod($class);
}
return implode("\n\n", $methods);
}
/**
* Compile a single dependency creation method
*/
private function compileDependencyMethod(string $class): string
{
$methodName = $this->getMethodName($class);
$className = ClassName::create($class);
$dependencies = $this->dependencyResolver->resolveDependencies($className);
if ($dependencies->isEmpty()) {
return " private function {$methodName}(): \\{$class}\n {\n return new \\{$class}();\n }";
}
$dependencyCode = $this->compileDependencyInjection($dependencies);
return " private function {$methodName}(): \\{$class}\n {\n return new \\{$class}({$dependencyCode});\n }";
}
/**
* Compile dependency injection parameters
*/
private function compileDependencyInjection($dependencies): string
{
$args = [];
foreach ($dependencies as $dependency) {
// Handle Dependency value objects from new DI system
if ($dependency instanceof Dependency) {
if ($dependency->getType() === DependencyType::DEPENDENCY) {
$className = $dependency->getClassName()?->getFullyQualified();
if ($className) {
$args[] = "\$this->get('{$className}')";
} else {
$args[] = 'null';
}
} elseif ($dependency->getType() === DependencyType::NULL) {
$args[] = 'null';
} else {
$args[] = var_export($dependency->getValue(), true);
}
} elseif (is_object($dependency)) {
$args[] = "\$this->get('" . $dependency::class . "')";
} elseif (is_string($dependency) && ! empty($dependency) && trim($dependency) !== '' && ClassName::create($dependency)->exists()) {
$args[] = "\$this->get('{$dependency}')";
} else {
$args[] = var_export($dependency, true);
}
}
return implode(', ', $args);
}
/**
* Generate method name from class name
*/
private function getMethodName(string $class): string
{
return 'create' . str_replace('\\', '_', $class);
}
/**
* Generate container hash for cache invalidation
*/
private function generateContainerHash(DefaultContainer $container): string
{
$bindingRegistry = $this->getBindingRegistry($container);
$data = [
'bindings' => $bindingRegistry->getAllKeys(),
'cache_stats' => $this->dependencyResolver->getCacheStats(),
'php_version' => PHP_VERSION,
'framework_version' => '1.0.0', // Should come from VersionInfo
];
return hash('sha256', serialize($data));
}
/**
* Write compiled code to file
*/
private function writeToFile(string $path, string $code): void
{
$directory = dirname($path);
if (! is_dir($directory)) {
mkdir($directory, 0755, true);
}
$result = file_put_contents($path, $code, LOCK_EX);
if ($result === false) {
throw new \RuntimeException("Failed to write compiled container to: {$path}");
}
chmod($path, 0644);
}
/**
* Get binding registry from DefaultContainer (using reflection for private access)
*/
private function getBindingRegistry(DefaultContainer $container): BindingRegistry
{
$reflection = new \ReflectionObject($container);
$property = $reflection->getProperty('bindings');
return $property->getValue($container);
}
/**
* Get instance registry from DefaultContainer (using reflection for private access)
*/
private function getInstanceRegistry(DefaultContainer $container): InstanceRegistry
{
$reflection = new \ReflectionObject($container);
$property = $reflection->getProperty('instances');
return $property->getValue($container);
}
/**
* Check if compiled container is valid and up-to-date
*/
public function isCompiledContainerValid(DefaultContainer $container, string $compiledPath): bool
{
if (! file_exists($compiledPath)) {
return false;
}
// Check if file is readable
if (! is_readable($compiledPath)) {
return false;
}
try {
$reflection = new \ReflectionClass($service);
$constructor = $reflection->getConstructor();
if ($constructor) {
foreach ($constructor->getParameters() as $param) {
$type = $param->getType();
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
$typeName = $type->getName();
$dependencies[$typeName] = true;
// Rekursive Analyse der Abhängigkeit
$this->analyzeServiceDependencies($typeName, $dependencies, $analyzedServices);
}
}
// Extract hash from compiled file
$content = file_get_contents($compiledPath);
if (! preg_match('/\* Hash: ([a-f0-9]{64})/', $content, $matches)) {
return false; // No hash found, file is invalid
}
} catch (\ReflectionException $e) {
// Fehlerbehandlung
$compiledHash = $matches[1];
$currentHash = $this->generateContainerHash($container);
// Hash mismatch = container changed, recompile needed
return $compiledHash === $currentHash;
} catch (\Exception $e) {
// Any error means invalid
return false;
}
}
/**
* Get compiled container path based on environment and configuration
*/
public static function getCompiledContainerPath(?string $cacheDir = null): string
{
$cacheDir = $cacheDir ?? sys_get_temp_dir() . '/framework-cache';
if (! is_dir($cacheDir)) {
mkdir($cacheDir, 0755, true);
}
return $cacheDir . '/compiled-container.php';
}
/**
* Load compiled container from file
*/
public static function load(string $compiledPath): Container
{
if (! file_exists($compiledPath)) {
throw new \RuntimeException("Compiled container not found: {$compiledPath}");
}
require_once $compiledPath;
$className = 'App\\Framework\\DI\\Compiled\\CompiledContainer';
if (! ClassName::create($className)->exists()) {
throw new \RuntimeException("Compiled container class not found: {$className}");
}
return new $className();
}
/**
* Compile container asynchronously (for production environments)
*/
public function compileAsync(DefaultContainer $container, string $outputPath, array $options = []): void
{
// In production, we might want to compile in background
// For now, just compile synchronously
$this->compile($container, $outputPath, $options);
}
}

View File

@@ -1,27 +1,34 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Exceptions\CyclicDependencyException;
use App\Framework\DI\Exceptions\LazyLoadingException;
use App\Framework\Reflection\CachedReflectionProvider;
use App\Framework\Reflection\ReflectionProvider;
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 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(
@@ -31,12 +38,13 @@ final class DefaultContainer implements Container
$this->invoker = new MethodInvoker($this, $this->reflectionProvider);
$this->registerSelf();
$this->instance(ReflectionProvider::class, $this->reflectionProvider);
}
public function bind(string $abstract, callable|string|object $concrete): void
{
$this->bindings->bind($abstract, $concrete);
$this->clearCaches($abstract);
$this->clearCaches(ClassName::create($abstract));
}
public function singleton(string $abstract, callable|string|object $concrete): void
@@ -51,15 +59,16 @@ final class DefaultContainer implements Container
}
/**
* @template T of object
* @param class-string<T> $class
* @return T
* @inheritDoc
*/
public function get(string $class): object
{
// Bereits instanziierte Objekte zurückgeben
if ($this->instances->hasSingleton($class)) {
return $this->instances->getSingleton($class);
$singleton = $this->instances->getSingleton($class);
if ($singleton !== null) {
return $singleton;
}
}
if ($this->instances->hasInstance($class)) {
@@ -71,7 +80,7 @@ final class DefaultContainer implements Container
try {
return $this->lazyInstantiator->createLazyInstance($class);
} catch (Throwable $e) {
if (!$e instanceof LazyLoadingException) {
if (! $e instanceof LazyLoadingException) {
throw $e;
}
}
@@ -80,6 +89,11 @@ final class DefaultContainer implements Container
return $this->createInstance($class);
}
/**
* @template T of object
* @param class-string<T> $class
* @return T
*/
private function createInstance(string $class): object
{
if (in_array($class, $this->resolving, true)) {
@@ -94,7 +108,7 @@ final class DefaultContainer implements Container
try {
$instance = $this->buildInstance($class);
if ($this->singletonDetector->isSingleton($class)) {
if ($this->singletonDetector->isSingleton(ClassName::create($class))) {
$this->instances->setSingleton($class, $instance);
} else {
$this->instances->setInstance($class, $instance);
@@ -108,14 +122,15 @@ final class DefaultContainer implements Container
private function buildInstance(string $class): object
{
$className = ClassName::create($class);
if ($this->bindings->hasBinding($class)) {
return $this->resolveBinding($class, $this->bindings->getBinding($class));
}
$reflection = $this->reflectionProvider->getClass($class);
$dependencies = $this->dependencyResolver->resolveDependencies($class);
$reflection = $this->reflectionProvider->getClass($className);
$dependencies = $this->dependencyResolver->resolveDependencies($className);
return $reflection->newInstanceArgs($dependencies);
return $reflection->newInstance(...$dependencies->toArray());
}
private function resolveBinding(string $class, callable|string|object $concrete): object
@@ -127,24 +142,27 @@ final class DefaultContainer implements Container
};
}
/** @param class-string $class */
public function has(string $class): bool
{
return $this->instances->hasSingleton($class)
|| $this->instances->hasInstance($class)
|| $this->bindings->hasBinding($class)
|| class_exists($class);
|| (! empty($class) && ClassName::create($class)->exists() && $this->reflectionProvider->getClass(ClassName::create($class))->isInstantiable());
}
/** @param class-string $class */
public function forget(string $class): void
{
$this->instances->forget($class);
$this->bindings->forget($class);
$this->clearCaches($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();
@@ -161,14 +179,23 @@ final class DefaultContainer implements Container
);
}
private function clearCaches(string $class): void
/**
* Get all registered service IDs (for debugging/admin)
* @return array<string>
*/
public function getServiceIds(): array
{
$this->reflectionProvider->forget($class);
$this->dependencyResolver->clearCache($class);
$this->lazyInstantiator->forgetFactory($class);
return array_keys($this->getRegisteredServices());
}
private function registerSelf():void
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);

View File

@@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Dependency;
use App\Framework\Core\ValueObjects\ClassName;
/**
* Type-safe cache for constructor dependency information
*/
final class ConstructorCache
{
/**
* @var array<string, ConstructorCacheEntry>
*/
private array $entries = [];
/**
* Check if class is cached
*/
public function has(ClassName $className): bool
{
return isset($this->entries[$className->getFullyQualified()]);
}
/**
* Get cache entry for class
*/
public function get(ClassName $className): ?ConstructorCacheEntry
{
return $this->entries[$className->getFullyQualified()] ?? null;
}
/**
* Store cache entry for class
*/
public function set(ConstructorCacheEntry $entry): void
{
$this->entries[$entry->className->getFullyQualified()] = $entry;
}
/**
* Remove cache entry for specific class
*/
public function remove(ClassName $className): void
{
unset($this->entries[$className->getFullyQualified()]);
}
/**
* Clear all cache entries
*/
public function clear(): void
{
$this->entries = [];
}
/**
* Get all cached class names
* @return array<ClassName>
*/
public function getCachedClasses(): array
{
return array_map(
fn (ConstructorCacheEntry $entry) => $entry->className,
array_values($this->entries)
);
}
/**
* Get cache statistics
*/
public function getStats(): array
{
$totalEntries = count($this->entries);
$emptyConstructors = 0;
$withDependencies = 0;
$totalParameters = 0;
foreach ($this->entries as $entry) {
if ($entry->hasParameters()) {
$withDependencies++;
$totalParameters += $entry->getParameterCount();
} else {
$emptyConstructors++;
}
}
return [
'total_entries' => $totalEntries,
'empty_constructors' => $emptyConstructors,
'with_dependencies' => $withDependencies,
'total_parameters' => $totalParameters,
'average_parameters' => $withDependencies > 0 ? round($totalParameters / $withDependencies, 2) : 0,
];
}
/**
* Find all entries that depend on a specific class
* @return array<ConstructorCacheEntry>
*/
public function getDependentsOf(ClassName $className): array
{
$dependents = [];
foreach ($this->entries as $entry) {
$dependencies = $entry->getClassDependencies();
foreach ($dependencies as $dependency) {
if ($dependency->equals($className)) {
$dependents[] = $entry;
break;
}
}
}
return $dependents;
}
/**
* Get entries that use specific dependency type
* @return array<ConstructorCacheEntry>
*/
public function getEntriesWithDependencyType(DependencyType $type): array
{
return array_filter(
$this->entries,
fn (ConstructorCacheEntry $entry) => $entry->usesDependencyType($type)
);
}
}

View File

@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Dependency;
use App\Framework\Core\ValueObjects\ClassName;
/**
* Represents a cached constructor parameter with its dependency information
*/
final readonly class ConstructorCacheEntry
{
private function __construct(
public ClassName $className,
public ParameterCacheCollection $parameters
) {
}
/**
* Create cache entry for a class with no constructor parameters
*/
public static function empty(ClassName $className): self
{
return new self($className, ParameterCacheCollection::empty());
}
/**
* Create cache entry from resolved dependencies
* @param array<array<string, mixed>> $cacheInfo
*/
public static function fromCacheInfo(ClassName $className, array $cacheInfo): self
{
return new self($className, ParameterCacheCollection::fromArray($cacheInfo));
}
/**
* Create cache entry from ParameterCacheInfo objects
*/
public static function fromParameters(ClassName $className, ParameterCacheInfo ...$parameters): self
{
return new self($className, ParameterCacheCollection::from(...$parameters));
}
/**
* Check if constructor has parameters
*/
public function hasParameters(): bool
{
return ! $this->parameters->isEmpty();
}
/**
* Get number of parameters
*/
public function getParameterCount(): int
{
return $this->parameters->count();
}
/**
* Get parameter cache info at specific position
*/
public function getParameter(int $position): ?ParameterCacheInfo
{
return $this->parameters->get($position);
}
/**
* Get parameter cache collection
*/
public function getParameters(): ParameterCacheCollection
{
return $this->parameters;
}
/**
* Get parameters as legacy array format (for backward compatibility)
* @return array<array<string, mixed>>
*/
public function getParametersAsArray(): array
{
return $this->parameters->toLegacyArray();
}
/**
* Get dependency types used in this constructor
* @return array<DependencyType>
*/
public function getDependencyTypes(): array
{
return $this->parameters->getDependencyTypes();
}
/**
* Check if constructor uses specific dependency type
*/
public function usesDependencyType(DependencyType $dependencyType): bool
{
return $this->parameters->usesDependencyType($dependencyType);
}
/**
* Get all class dependencies referenced in this constructor
* @return array<ClassName>
*/
public function getClassDependencies(): array
{
return $this->parameters->getClassDependencies();
}
/**
* Check if constructor depends on specific class
*/
public function dependsOn(ClassName $className): bool
{
return $this->parameters->dependsOn($className);
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Dependency;
use App\Framework\Core\ValueObjects\ClassName;
/**
* Represents a resolved dependency with caching information
*/
final readonly class Dependency
{
private function __construct(
public DependencyType $type,
public mixed $value,
public ?ClassName $className = null
) {
}
public static function default(mixed $value): self
{
return new self(DependencyType::DEFAULT, $value);
}
public static function null(): self
{
return new self(DependencyType::NULL, null);
}
public static function dependency(object $value, ClassName $className): self
{
return new self(DependencyType::DEPENDENCY, $value, $className);
}
public static function nullableDependency(object $value, ClassName $className): self
{
return new self(DependencyType::NULLABLE_DEPENDENCY, $value, $className);
}
public function getCacheInfo(): array
{
return match ($this->type) {
DependencyType::DEFAULT => [
'type' => $this->type->value,
'value' => $this->value,
],
DependencyType::NULL => [
'type' => $this->type->value,
],
DependencyType::DEPENDENCY, DependencyType::NULLABLE_DEPENDENCY => [
'type' => $this->type->value,
'class' => $this->className?->getFullyQualified(),
],
};
}
public static function fromCacheInfo(array $cacheInfo): ?ClassName
{
$type = DependencyType::from($cacheInfo['type']);
return match ($type) {
DependencyType::DEPENDENCY, DependencyType::NULLABLE_DEPENDENCY =>
isset($cacheInfo['class']) ? ClassName::create($cacheInfo['class']) : null,
default => null
};
}
}

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Dependency;
/**
* Collection of resolved dependencies with type safety and iteration support
*/
final readonly class DependencyCollection implements \IteratorAggregate, \Countable
{
/**
* @param array<mixed> $dependencies
*/
private function __construct(
private array $dependencies
) {
}
/**
* Create collection from resolved dependency values
* @param array<mixed> $dependencies
*/
public static function fromValues(array $dependencies): self
{
return new self($dependencies);
}
/**
* Create empty collection
*/
public static function empty(): self
{
return new self([]);
}
/**
* Get all dependency values as array (for backward compatibility)
* @return array<mixed>
*/
public function toArray(): array
{
return $this->dependencies;
}
/**
* Get dependency at specific index
*/
public function get(int $index): mixed
{
return $this->dependencies[$index] ?? null;
}
/**
* Check if collection has dependencies
*/
public function isEmpty(): bool
{
return empty($this->dependencies);
}
/**
* Count dependencies
*/
public function count(): int
{
return count($this->dependencies);
}
/**
* Get iterator for foreach loops
* @return \ArrayIterator<int, mixed>
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->dependencies);
}
/**
* Create collection with additional dependency
*/
public function with(mixed $dependency): self
{
return new self([...$this->dependencies, $dependency]);
}
}

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Dependency;
enum DependencyType: string
{
case DEFAULT = 'default';
case NULL = 'null';
case DEPENDENCY = 'dependency';
case NULLABLE_DEPENDENCY = 'nullable_dependency';
}

View File

@@ -0,0 +1,205 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Dependency;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Container;
/**
* Type-safe collection of parameter cache information
*/
final readonly class ParameterCacheCollection implements \IteratorAggregate, \Countable
{
/**
* @param array<ParameterCacheInfo> $parameters
*/
private function __construct(
private array $parameters
) {
}
/**
* Create collection from ParameterCacheInfo objects
* @param ParameterCacheInfo ...$parameters
*/
public static function from(ParameterCacheInfo ...$parameters): self
{
return new self($parameters);
}
/**
* Create collection from legacy cache array format
* @param array<array<string, mixed>> $cacheArray
*/
public static function fromArray(array $cacheArray): self
{
$parameters = array_map(
fn (array $info) => ParameterCacheInfo::fromArray($info),
$cacheArray
);
return new self($parameters);
}
/**
* Create empty collection
*/
public static function empty(): self
{
return new self([]);
}
/**
* Get iterator for foreach loops
* @return \ArrayIterator<int, ParameterCacheInfo>
*/
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->parameters);
}
/**
* Count parameters
*/
public function count(): int
{
return count($this->parameters);
}
/**
* Check if collection is empty
*/
public function isEmpty(): bool
{
return empty($this->parameters);
}
/**
* Get parameter at specific position
*/
public function get(int $position): ?ParameterCacheInfo
{
return $this->parameters[$position] ?? null;
}
/**
* Get all parameters as array
* @return array<ParameterCacheInfo>
*/
public function toArray(): array
{
return $this->parameters;
}
/**
* Convert to legacy array format (for backward compatibility)
* @return array<array<string, mixed>>
*/
public function toLegacyArray(): array
{
return array_map(
fn (ParameterCacheInfo $param) => $param->toArray(),
$this->parameters
);
}
/**
* Get all dependency types used
* @return array<DependencyType>
*/
public function getDependencyTypes(): array
{
$types = [];
foreach ($this->parameters as $param) {
if (! in_array($param->type, $types, true)) {
$types[] = $param->type;
}
}
return $types;
}
/**
* Check if collection uses specific dependency type
*/
public function usesDependencyType(DependencyType $type): bool
{
foreach ($this->parameters as $param) {
if ($param->type === $type) {
return true;
}
}
return false;
}
/**
* Get all class dependencies
* @return array<ClassName>
*/
public function getClassDependencies(): array
{
$dependencies = [];
foreach ($this->parameters as $param) {
if ($param->hasClassDependency()) {
$dependencies[] = $param->className;
}
}
return $dependencies;
}
/**
* Check if any parameter depends on specific class
*/
public function dependsOn(ClassName $className): bool
{
foreach ($this->parameters as $param) {
if ($param->dependsOn($className)) {
return true;
}
}
return false;
}
/**
* Filter parameters by dependency type
*/
public function filterByType(DependencyType $type): self
{
$filtered = array_filter(
$this->parameters,
fn (ParameterCacheInfo $param) => $param->type === $type
);
return new self(array_values($filtered));
}
/**
* Get parameters that have class dependencies
*/
public function getClassDependencyParameters(): self
{
$filtered = array_filter(
$this->parameters,
fn (ParameterCacheInfo $param) => $param->hasClassDependency()
);
return new self(array_values($filtered));
}
/**
* Resolve all parameter values using container
* @return array<mixed>
*/
public function resolveValues(Container $container): array
{
return array_map(
fn (ParameterCacheInfo $param) => $param->resolveValue($container),
$this->parameters
);
}
}

View File

@@ -0,0 +1,117 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Dependency;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Container;
/**
* Represents cached information for a single constructor parameter
*/
final readonly class ParameterCacheInfo
{
private function __construct(
public DependencyType $type,
public mixed $value = null,
public ?ClassName $className = null
) {
}
/**
* Create cache info for default value parameter
*/
public static function default(mixed $value): self
{
return new self(DependencyType::DEFAULT, $value);
}
/**
* Create cache info for null parameter
*/
public static function null(): self
{
return new self(DependencyType::NULL);
}
/**
* Create cache info for dependency parameter
*/
public static function dependency(ClassName $className): self
{
return new self(DependencyType::DEPENDENCY, null, $className);
}
/**
* Create cache info for nullable dependency parameter
*/
public static function nullableDependency(ClassName $className): self
{
return new self(DependencyType::NULLABLE_DEPENDENCY, null, $className);
}
/**
* Create from legacy cache array format
*/
public static function fromArray(array $cacheInfo): self
{
$type = DependencyType::from($cacheInfo['type']);
return match ($type) {
DependencyType::DEFAULT => self::default($cacheInfo['value']),
DependencyType::NULL => self::null(),
DependencyType::DEPENDENCY => self::dependency(ClassName::create($cacheInfo['class'])),
DependencyType::NULLABLE_DEPENDENCY => self::nullableDependency(ClassName::create($cacheInfo['class'])),
};
}
/**
* Convert to legacy array format (for backward compatibility)
*/
public function toArray(): array
{
return match ($this->type) {
DependencyType::DEFAULT => [
'type' => $this->type->value,
'value' => $this->value,
],
DependencyType::NULL => [
'type' => $this->type->value,
],
DependencyType::DEPENDENCY, DependencyType::NULLABLE_DEPENDENCY => [
'type' => $this->type->value,
'class' => $this->className?->getFullyQualified(),
],
};
}
/**
* Check if this parameter has a class dependency
*/
public function hasClassDependency(): bool
{
return $this->className !== null;
}
/**
* Check if this parameter has a specific class dependency
*/
public function dependsOn(ClassName $className): bool
{
return $this->className !== null && $this->className->equals($className);
}
/**
* Get the resolved value for this parameter
*/
public function resolveValue(Container $container): mixed
{
return match ($this->type) {
DependencyType::DEFAULT => $this->value,
DependencyType::NULL => null,
DependencyType::DEPENDENCY, DependencyType::NULLABLE_DEPENDENCY =>
$container->get($this->className->getFullyQualified()),
};
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\Dependency;
/**
* Represents a resolved parameter with its value and cache information
*/
final readonly class ResolvedParameter
{
public function __construct(
public mixed $value,
public Dependency $dependency
) {
}
/**
* Get cache information for this parameter
*/
public function getCacheInfo(): array
{
return $this->dependency->getCacheInfo();
}
}

View File

@@ -1,110 +1,190 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Dependency\ConstructorCache;
use App\Framework\DI\Dependency\ConstructorCacheEntry;
use App\Framework\DI\Dependency\Dependency;
use App\Framework\DI\Dependency\DependencyCollection;
use App\Framework\DI\Dependency\DependencyType;
use App\Framework\DI\Dependency\ResolvedParameter;
use App\Framework\DI\Exceptions\ParameterResolutionException;
use App\Framework\Reflection\ReflectionProvider;
use ReflectionNamedType;
use ReflectionParameter;
use App\Framework\Reflection\WrappedReflectionParameter;
use ReflectionType;
/**
* Löst Constructor-Abhängigkeiten auf
*/
final class DependencyResolver
final readonly class DependencyResolver
{
private array $constructorCache = [];
private ConstructorCache $constructorCache;
public function __construct(
private readonly ReflectionProvider $reflectionProvider,
private readonly Container $container
) {}
private ReflectionProvider $reflectionProvider,
private Container $container
) {
$this->constructorCache = new ConstructorCache();
}
public function resolveDependencies(string $class): array
public function resolveDependencies(ClassName $className): DependencyCollection
{
$reflection = $this->reflectionProvider->getClass($class);
$constructor = $reflection->getConstructor();
if ($constructor === null) {
return [];
// Fast path: Check cache first
if ($this->constructorCache->has($className)) {
return $this->resolveFromCache($className);
}
if (isset($this->constructorCache[$class])) {
return $this->resolveFromCache($class);
$reflection = $this->reflectionProvider->getClass($className);
// Check if class has constructor
if (! $reflection->hasMethod('__construct')) {
// Cache empty dependencies
$this->constructorCache->set(ConstructorCacheEntry::empty($className));
return DependencyCollection::empty();
}
$constructorParameters = $this->reflectionProvider->getMethodParameters($className, '__construct');
if ($constructorParameters->isEmpty()) {
// Cache empty dependencies
$this->constructorCache->set(ConstructorCacheEntry::empty($className));
return DependencyCollection::empty();
}
$dependencies = [];
$cacheInfo = [];
foreach ($constructor->getParameters() as $param) {
$dependency = $this->resolveParameter($param, $class);
$dependencies[] = $dependency['value'];
$cacheInfo[] = $dependency['cache'];
foreach ($constructorParameters as $param) {
$resolvedParam = $this->resolveParameter($param, $className);
$dependencies[] = $resolvedParam->value;
$cacheInfo[] = $resolvedParam->getCacheInfo();
}
// Nur cacheable Parameter cachen
if (!in_array(false, array_column($cacheInfo, 'cacheable'), true)) {
$this->constructorCache[$class] = $cacheInfo;
}
// Cache constructor info (even with non-cacheable dependencies for structure)
$this->constructorCache->set(ConstructorCacheEntry::fromCacheInfo($className, $cacheInfo));
return $dependencies;
return DependencyCollection::fromValues($dependencies);
}
private function resolveParameter(ReflectionParameter $param, string $class): array
private function resolveParameter(WrappedReflectionParameter $param, ClassName $className): ResolvedParameter
{
$type = $param->getType();
if ($param->isDefaultValueAvailable()) {
return [
'value' => $param->getDefaultValue(),
'cache' => ['type' => 'default', 'value' => $param->getDefaultValue(), 'cacheable' => true]
];
}
if ($type instanceof ReflectionNamedType && $type->allowsNull()) {
return [
'value' => null,
'cache' => ['type' => 'null', 'cacheable' => true]
];
}
if (!$type || $type->isBuiltin()) {
// Strikte Validierung für Union/Intersection Types
if ($type instanceof \ReflectionUnionType) {
throw new ParameterResolutionException(
paramName: $param->getName(),
class: $class,
class: $className->getFullyQualified(),
dependencyChain: [],
reason: 'Union types are not supported in DI'
);
}
if ($type instanceof \ReflectionIntersectionType) {
throw new ParameterResolutionException(
paramName: $param->getName(),
class: $className->getFullyQualified(),
dependencyChain: [],
reason: 'Intersection types are not supported in DI'
);
}
$dependency = $this->createDependency($param, $type, $className);
return new ResolvedParameter($dependency->value, $dependency);
}
private function createDependency(WrappedReflectionParameter $param, ?ReflectionType $type, ClassName $className): Dependency
{
if ($param->hasDefaultValue()) {
return Dependency::default($param->getDefaultValue());
}
$typeName = $param->getTypeName();
if ($param->allowsNull() && $typeName !== null) {
// Prüfe ob Container die nullable dependency bereitstellen kann
if (! $param->isBuiltin() && $this->container->has($typeName)) {
/** @var class-string<object> $typeName */
$value = $this->container->get($typeName);
return Dependency::nullableDependency($value, ClassName::create($typeName));
}
// Fallback auf null wenn nicht im Container verfügbar
return Dependency::null();
}
if ($typeName === null || $param->isBuiltin()) {
throw new ParameterResolutionException(
paramName: $param->getName(),
class: $className->getFullyQualified(),
dependencyChain: [] // Wird vom Container gesetzt
);
}
$typeName = $type->getName();
return [
'value' => $this->container->get($typeName),
'cache' => ['type' => 'dependency', 'class' => $typeName, 'cacheable' => false]
];
/** @var class-string<object> $typeName */
$value = $this->container->get($typeName);
return Dependency::dependency($value, ClassName::create($typeName));
}
private function resolveFromCache(string $class): array
private function resolveFromCache(ClassName $className): DependencyCollection
{
$cacheInfo = $this->constructorCache[$class];
$cacheEntry = $this->constructorCache->get($className);
if ($cacheEntry === null || ! $cacheEntry->hasParameters()) {
return DependencyCollection::empty();
}
$dependencies = [];
foreach ($cacheInfo as $info) {
$dependencies[] = match ($info['type']) {
'default' => $info['value'],
'null' => null,
'dependency' => $this->container->get($info['class']),
foreach ($cacheEntry->getParameters() as $paramInfo) {
$dependencies[] = match ($paramInfo->type) {
DependencyType::DEFAULT => $paramInfo->value,
DependencyType::NULL => null,
DependencyType::DEPENDENCY, DependencyType::NULLABLE_DEPENDENCY =>
$this->resolveFromContainer($paramInfo->className),
};
}
return $dependencies;
return DependencyCollection::fromValues($dependencies);
}
public function clearCache(string $class): void
private function resolveFromContainer(ClassName $className): object
{
unset($this->constructorCache[$class]);
return $this->container->get($className->getFullyQualified());
}
public function clearCache(ClassName $className): void
{
$this->constructorCache->remove($className);
}
public function flushCache(): void
{
$this->constructorCache = [];
$this->constructorCache->clear();
}
/**
* Get cache statistics for debugging/monitoring
*/
public function getCacheStats(): array
{
return $this->constructorCache->getStats();
}
/**
* Get all classes that depend on the given class
* @return array<ConstructorCacheEntry>
*/
public function getDependentsOf(ClassName $className): array
{
return $this->constructorCache->getDependentsOf($className);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\DI\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class CyclicDependencyException extends FrameworkException
@@ -14,14 +15,17 @@ final class CyclicDependencyException extends FrameworkException
int $code = 0,
?\Throwable $previous = null
) {
$context = ExceptionContext::forOperation('dependency_resolution', 'DI')
->withData([
'dependencyChain' => $dependencyChain,
'class' => $class,
]);
parent::__construct(
message: 'Zyklische Abhängigkeit entdeckt: ' . implode(' -> ', $dependencyChain) . " -> $class",
context: $context,
code: $code,
previous: $previous,
context: [
'dependencyChain' => $dependencyChain,
'class' => $class
]
previous: $previous
);
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\DI\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class LazyLoadingException extends FrameworkException
@@ -15,6 +16,7 @@ final class LazyLoadingException extends FrameworkException
) {
parent::__construct(
message: "Lazy Loading wird für die Klasse $class nicht unterstützt",
context: ExceptionContext::forOperation('lazy_loading', 'DI')->withData(['class' => $class]),
code: $code,
previous: $previous
);

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Framework\DI\Exceptions;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;
final class ParameterResolutionException extends FrameworkException
@@ -13,17 +14,29 @@ final class ParameterResolutionException extends FrameworkException
string $class,
array $dependencyChain,
int $code = 0,
?string $reason = null,
?\Throwable $previous = null
) {
if (! $reason) {
$message = "Cannot resolve parameter {$paramName} in $class. Dependency chain: " . implode(' -> ', $dependencyChain);
} else {
$message = "Cannot resolve parameter {$paramName} in $class because $reason";
}
parent::__construct(
message: "Cannot resolve parameter {$paramName} in $class. Dependency chain: " . implode(' -> ', $dependencyChain),
message: $message,
context: new ExceptionContext(
operation: 'dependency_injection',
component: 'DI Container',
data: [
'paramName' => $paramName,
'class' => $class,
'dependencyChain' => $dependencyChain,
]
),
code: $code,
previous: $previous,
context: [
'paramName' => $paramName,
'class' => $class,
'dependencyChain' => $dependencyChain
]
previous: $previous
);
}
}

View File

@@ -1,8 +1,29 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Context\ContextType;
#[\Attribute(\Attribute::TARGET_METHOD)]
final class Initializer
final readonly class Initializer
{
/** @var ContextType[]|null */
public ?array $contexts;
public function __construct(ContextType ...$contexts)
{
$this->contexts = empty($contexts) ? null : $contexts;
}
public function allowsContext(ContextType $context): bool
{
// Keine Context-Einschränkung = läuft in allen Contexts
if ($this->contexts === null) {
return true;
}
return in_array($context, $this->contexts, true);
}
}

View File

@@ -0,0 +1,186 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\DI\ValueObjects\DependencyGraphNode;
use App\Framework\Reflection\ReflectionProvider;
/**
* Dependency Graph für Initializer-Ausführung
* Analysiert Dependencies und sorgt für korrekte Ausführungsreihenfolge
*/
final class InitializerDependencyGraph
{
/** @var array<string, DependencyGraphNode> */
private array $nodes = [];
/** @var array<string, array<string>> */
private array $adjacencyList = [];
/** @var array<string, bool> */
private array $visited = [];
/** @var array<string, bool> */
private array $inStack = [];
public function __construct(
private readonly ReflectionProvider $reflectionProvider,
) {
}
public function addInitializer(string $returnType, ClassName $className, MethodName $methodName): void
{
if ($returnType === 'null' || $returnType === 'void') {
return; // Setup-Initializer werden sofort ausgeführt
}
$dependencies = $this->analyzeDependencies($className, $methodName);
$node = new DependencyGraphNode(
returnType: $returnType,
className: $className,
methodName: $methodName,
dependencies: $dependencies
);
$this->nodes[$returnType] = $node;
$this->adjacencyList[$returnType] = $dependencies;
}
/**
* Berechnet die optimale Ausführungsreihenfolge basierend auf Dependencies
* @return array<string> Sortierte Liste von Return-Types
*/
public function getExecutionOrder(): array
{
$this->visited = [];
$this->inStack = [];
$result = [];
foreach (array_keys($this->nodes) as $returnType) {
if (! isset($this->visited[$returnType])) {
$this->topologicalSort($returnType, $result);
}
}
return array_reverse($result);
}
/**
* Prüft ob ein Node für den gegebenen Return-Type existiert
*/
public function hasNode(string $returnType): bool
{
return isset($this->nodes[$returnType]);
}
/**
* Gibt einen spezifischen Node zurück
*/
public function getNode(string $returnType): ?DependencyGraphNode
{
return $this->nodes[$returnType] ?? null;
}
/**
* Gibt alle Nodes zurück
*
* @return array<string, DependencyGraphNode>
*/
public function getNodes(): array
{
return $this->nodes;
}
/**
* Gibt alle Nodes als Array zurück (backward compatibility)
*
* @deprecated Use getNodes() for DependencyGraphNode objects
* @return array<string, array{class: string, method: string, dependencies: string[]}>
*/
public function getNodesAsArray(): array
{
return array_map(fn ($node) => $node->toArray(), $this->nodes);
}
/**
* Analysiert die Dependencies eines Initializers durch Reflection
*/
private function analyzeDependencies(CLassName $className, MethodName $methodName): array
{
try {
$reflection = $this->reflectionProvider->getClass($className);
#$reflection = new \ReflectionClass($className);
$method = $reflection->getMethod($methodName->toString());
;
$dependencies = [];
foreach ($method->getParameters() as $parameter) {
$paramType = $parameter->getType();
if ($paramType instanceof \ReflectionNamedType) {
$typeName = $paramType->getName();
// Nur Klassen/Interfaces als Dependencies, keine primitiven Typen
if (! empty($typeName) && ClassName::create($typeName)->exists()) {
$dependencies[] = $typeName;
}
}
}
return $dependencies;
} catch (\Throwable $e) {
return [];
}
}
/**
* Topologische Sortierung mit Cycle-Detection
*/
private function topologicalSort(string $returnType, array &$result): void
{
$this->visited[$returnType] = true;
$this->inStack[$returnType] = true;
foreach ($this->adjacencyList[$returnType] ?? [] as $dependency) {
if (isset($this->nodes[$dependency])) {
if (isset($this->inStack[$dependency]) && $this->inStack[$dependency]) {
// Cycle detected - ignore this dependency to prevent infinite loops
continue;
}
if (! isset($this->visited[$dependency])) {
$this->topologicalSort($dependency, $result);
}
}
}
$this->inStack[$returnType] = false;
$result[] = $returnType;
}
private function hasCycle(string $returnType): bool
{
$this->visited[$returnType] = true;
$this->inStack[$returnType] = true;
foreach ($this->adjacencyList[$returnType] ?? [] as $dependency) {
if (isset($this->nodes[$dependency])) {
if (isset($this->inStack[$dependency]) && $this->inStack[$dependency]) {
return true;
}
if (! isset($this->visited[$dependency]) && $this->hasCycle($dependency)) {
return true;
}
}
}
$this->inStack[$returnType] = false;
return false;
}
}

View File

@@ -1,8 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\AttributeMapper;
use App\Framework\Reflection\WrappedReflectionClass;
use App\Framework\Reflection\WrappedReflectionMethod;
final readonly class InitializerMapper implements AttributeMapper
{
@@ -11,13 +15,34 @@ final readonly class InitializerMapper implements AttributeMapper
return Initializer::class;
}
public function map(object $reflectionTarget, object $attributeInstance): ?array
public function map(WrappedReflectionClass|WrappedReflectionMethod $reflectionTarget, object $attributeInstance): array
{
return [
'class' => $reflectionTarget->getDeclaringClass()->getName(),
'return' => $reflectionTarget->getReturnType()->getName(),
'method' => $reflectionTarget->getName(),
#'path' => $attributeInstance->path,
];
/** @var Initializer $attributeInstance */
if ($reflectionTarget instanceof WrappedReflectionMethod) {
return [
'class' => $reflectionTarget->getDeclaringClass()->getFullyQualified(),
'return' => $this->getReturnTypeName($reflectionTarget),
'method' => $reflectionTarget->getName(),
'contexts' => $attributeInstance->contexts,
];
}
// For class-level initializers (if needed in the future)
if ($reflectionTarget instanceof WrappedReflectionClass) {
return [
'class' => $reflectionTarget->getName()->getFullyQualified(),
'return' => null,
'method' => null,
'contexts' => $attributeInstance->contexts,
];
}
return [];
}
private function getReturnTypeName(WrappedReflectionMethod $method): ?string
{
return $method->getReturnType();
}
}

View File

@@ -1,5 +1,7 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
/**
@@ -8,43 +10,62 @@ namespace App\Framework\DI;
final class InstanceRegistry
{
private array $singletons = [];
private array $instances = [];
/**
* @template T of object
* @param class-string<T> $class
* @return T|null
*/
public function getSingleton(string $class): ?object
{
return $this->singletons[$class] ?? null;
}
/** @param class-string $class */
public function hasSingleton(string $class): bool
{
return isset($this->singletons[$class]) && $this->singletons[$class] !== null;
}
/** @param class-string $class */
public function setSingleton(string $class, object $instance): void
{
$this->singletons[$class] = $instance;
}
/** @param class-string $class */
public function markAsSingleton(string $class): void
{
$this->singletons[$class] = null;
}
/** @param class-string $class */
public function isMarkedAsSingleton(string $class): bool
{
return array_key_exists($class, $this->singletons);
}
/** @param class-string $class */
public function getInstance(string $class): ?object
{
return $this->instances[$class] ?? null;
}
/** @param class-string $class */
public function hasInstance(string $class): bool
{
return isset($this->instances[$class]);
}
/** @param class-string $class */
public function setInstance(string $class, object $instance): void
{
$this->instances[$class] = $instance;
}
/** @param class-string $class */
public function forget(string $class): void
{
unset($this->instances[$class], $this->singletons[$class]);
@@ -63,4 +84,9 @@ final class InstanceRegistry
array_keys($this->instances)
);
}
public function getSingletons(): array
{
return array_keys($this->singletons);
}
}

View File

@@ -1,56 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\DI\Exceptions\LazyLoadingException;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Reflection\ReflectionProvider;
use Closure;
/**
* Behandelt Lazy Loading von Objekten
*/
final class LazyInstantiator
final readonly class LazyInstantiator
{
private array $factories = [];
public function __construct(
private readonly ReflectionProvider $reflectionProvider,
private readonly Closure $instanceCreator
) {}
private ReflectionProvider $reflectionProvider,
private Closure $instanceCreator
) {
}
public function canUseLazyLoading(string $class, InstanceRegistry $registry): bool
{
return !$registry->hasSingleton($class);
return ! $registry->hasSingleton($class);
}
public function createLazyInstance(string $class): object
{
$reflection = $this->reflectionProvider->getClass($class);
// Framework ReflectionProvider verwenden für Konsistenz
$className = ClassName::create($class);
$reflection = $this->reflectionProvider->getClass($className);
if (!isset($this->factories[$class])) {
$this->factories[$class] = fn() => ($this->instanceCreator)($class);
// Prüfen ob die Klasse instanziierbar ist (nicht Interface/Abstract)
if (! $reflection->isInstantiable()) {
// Fallback: Normale Instanziierung über den Container
return ($this->instanceCreator)($class);
}
$factory = $this->factories[$class];
// Für newLazyGhost() brauchen wir die native ReflectionClass
$nativeReflection = $this->reflectionProvider->getNativeClass($className);
if (method_exists($reflection, 'getLazyGhost')) {
return $reflection->getLazyGhost($factory);
}
// Lazy Ghost Initializer - initialisiert das Ghost-Objekt "in place"
$initializer = function (object $ghost) use ($class, $className): void {
// Das echte Objekt erstellen
$realObject = ($this->instanceCreator)($class);
if (method_exists($reflection, 'getLazyProxy')) {
return $reflection->getLazyProxy($factory);
}
// Framework ReflectionProvider für Property-Zugriff verwenden
$realReflection = $this->reflectionProvider->getNativeClass($className);
foreach ($realReflection->getProperties() as $property) {
if ($property->isInitialized($realObject)) {
$property->setValue($ghost, $property->getValue($realObject));
}
}
};
throw new LazyLoadingException("Lazy loading not supported for class: {$class}");
// PHP 8.4 lazy loading verwenden
return $nativeReflection->newLazyGhost($initializer);
}
public function forgetFactory(string $class): void
{
unset($this->factories[$class]);
// Keine Factories mehr zu vergessen, da sie nicht gecacht werden
}
public function flushFactories(): void
{
$this->factories = [];
// Keine Factories mehr zu löschen, da sie nicht gecacht werden
}
}

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Reflection\ReflectionProvider;
use ReflectionMethod;
use ReflectionNamedType;
use App\Framework\Reflection\WrappedReflectionMethod;
/**
* Führt Methoden auf beliebigen Klassen mit automatischer Dependency Injection aus
@@ -13,15 +15,17 @@ final readonly class MethodInvoker
{
public function __construct(
private Container $container,
private ReflectionProvider $reflectionCache
) {}
private ReflectionProvider $reflectionProvider
) {
}
/**
* Führt eine Methode auf einer Klasse aus und löst alle Parameter automatisch auf
*/
public function invoke(string $className, string $methodName, array $overrides = []): mixed
public function invoke(ClassName $className, string $methodName, array $overrides = []): mixed
{
$instance = $this->container->get($className);
$instance = $this->container->get($className->getFullyQualified());
return $this->invokeOn($instance, $methodName, $overrides);
}
@@ -30,25 +34,28 @@ final readonly class MethodInvoker
*/
public function invokeOn(object $instance, string $methodName, array $overrides = []): mixed
{
$reflection = $this->reflectionCache->getClass($instance::class);
$className = ClassName::fromObject($instance);
$reflection = $this->reflectionProvider->getClass($className);
if (!$reflection->hasMethod($methodName)) {
if (! $reflection->hasMethod($methodName)) {
throw new \InvalidArgumentException(
"Method $methodName does not exist on class $instance::class"
"Method $methodName does not exist on class " . $instance::class
);
}
$method = $this->reflectionCache->getMethod($instance::class, $methodName);
// Framework WrappedReflectionMethod verwenden
$method = $this->reflectionProvider->getMethod($className, $methodName);
$nativeMethod = $this->reflectionProvider->getNativeMethod($className, $methodName);
if (!$method->isPublic()) {
if (! $nativeMethod->isPublic()) {
throw new \InvalidArgumentException(
"Method $methodName on class $instance::class is not public"
"Method $methodName on class " . $instance::class . " is not public"
);
}
$parameters = $this->resolveMethodParameters($method, $overrides);
$parameters = $this->resolveMethodParameters($className, $methodName, $overrides);
return $method->invokeArgs($instance, $parameters);
return $nativeMethod->invokeArgs($instance, $parameters);
}
/**
@@ -56,69 +63,77 @@ final readonly class MethodInvoker
*/
public function invokeStatic(string $className, string $methodName, array $overrides = []): mixed
{
$reflection = $this->reflectionCache->getClass($className);
$classNameObj = ClassName::create($className);
$reflection = $this->reflectionProvider->getClass($classNameObj);
if (!$reflection->hasMethod($methodName)) {
if (! $reflection->hasMethod($methodName)) {
throw new \InvalidArgumentException(
"Method '{$methodName}' does not exist on class '{$className}'"
);
}
$method = $this->reflectionCache->getMethod($className, $methodName);
// Framework WrappedReflectionMethod verwenden
$method = $this->reflectionProvider->getMethod($classNameObj, $methodName);
$nativeMethod = $this->reflectionProvider->getNativeMethod($classNameObj, $methodName);
if (!$method->isStatic()) {
if (! $nativeMethod->isStatic()) {
throw new \InvalidArgumentException(
"Method '{$methodName}' on class '{$className}' is not static"
);
}
$parameters = $this->resolveMethodParameters($method, $overrides);
$parameters = $this->resolveMethodParameters($classNameObj, $methodName, $overrides);
return $method->invokeArgs(null, $parameters);
return $nativeMethod->invokeArgs(null, $parameters);
}
/**
* Löst alle Parameter einer Methode auf
*/
private function resolveMethodParameters(ReflectionMethod $method, array $overrides): array
private function resolveMethodParameters(ClassName $className, string $methodName, array $overrides): array
{
$parameters = [];
foreach ($method->getParameters() as $param) {
// Framework ParameterCollection verwenden
$methodParameters = $this->reflectionProvider->getMethodParameters($className, $methodName);
foreach ($methodParameters as $param) {
$paramName = $param->getName();
// Override-Werte haben Priorität
if (array_key_exists($paramName, $overrides)) {
$parameters[] = $overrides[$paramName];
continue;
}
// Type-based injection
$type = $param->getType();
if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) {
$typeName = $type->getName();
// Type-based injection mit Framework API
$typeName = $param->getTypeName();
if ($typeName !== null && ! $param->isBuiltin()) {
if ($this->container->has($typeName)) {
$parameters[] = $this->container->get($typeName);
continue;
}
}
// Default-Werte verwenden
if ($param->isDefaultValueAvailable()) {
if ($param->hasDefaultValue()) {
$parameters[] = $param->getDefaultValue();
continue;
}
// Nullable Parameter
if ($type && $type->allowsNull()) {
if ($param->allowsNull()) {
$parameters[] = null;
continue;
}
throw new \InvalidArgumentException(
"Cannot resolve parameter '{$paramName}' for method '{$method->getName()}' on class '{$method->getDeclaringClass()->getName()}'"
"Cannot resolve parameter '{$paramName}' for method '{$methodName}' on class '{$className->getFullyQualified()}'"
);
}

View File

@@ -1,64 +0,0 @@
<?php
namespace App\Framework\DI;
use ReflectionClass;
use ReflectionMethod;
/**
* Cache für ReflectionClass-Instanzen
*/
final class ReflectionCache
{
private array $classCache = [];
private array $methodCache = [];
private array $parameterCache = [];
public function get(string $class): ReflectionClass
{
return $this->classCache[$class] ??= new ReflectionClass($class);
}
public function getMethod(string $class, string $method): ReflectionMethod
{
$key = "{$class}::{$method}";
return $this->methodCache[$key] ??= $this->get($class)->getMethod($method);
}
public function getMethodParameters(string $class, string $method): array
{
$key = "{$class}::{$method}::params";
return $this->parameterCache[$key] ??= $this->getMethod($class, $method)->getParameters();
}
public function forget(string $class): void
{
unset($this->classCache[$class]);
// Alle Methods dieser Klasse aus dem Cache entfernen
$this->methodCache = array_filter(
$this->methodCache,
fn($key) => !str_starts_with($key, $class . '::'),
ARRAY_FILTER_USE_KEY
);
$this->parameterCache = array_filter(
$this->parameterCache,
fn($key) => !str_starts_with($key, $class . '::'),
ARRAY_FILTER_USE_KEY
);
}
public function forgetMethod(string $class, string $method): void
{
unset($this->methodCache["{$class}::{$method}"]);
}
public function flush(): void
{
$this->classCache = [];
$this->methodCache = [];
$this->parameterCache = [];
}
}

View File

@@ -1,8 +1,11 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Attributes\Singleton;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Reflection\ReflectionProvider;
/**
@@ -13,15 +16,26 @@ final readonly class SingletonDetector
public function __construct(
private ReflectionProvider $reflectionProvider,
private InstanceRegistry $instanceRegistry
) {}
) {
}
public function isSingleton(string $class): bool
public function isSingleton(ClassName $className): bool
{
$class = $className->getFullyQualified();
// Check if already has instance
if ($this->instanceRegistry->hasSingleton($class)) {
return true;
}
$reflection = $this->reflectionProvider->getClass($class);
return count($reflection->getAttributes(Singleton::class)) > 0;
// Check if marked as singleton (even without instance yet)
if ($this->instanceRegistry->isMarkedAsSingleton($class)) {
return true;
}
$reflection = $this->reflectionProvider->getClass($className);
$hasAttribute = count($reflection->getAttributes(Singleton::class)) > 0;
return $hasAttribute;
}
}

View File

@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\ValueObjects;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
/**
* Represents a node in the dependency graph for Initializer processing
*
* Each node contains information about a service initializer including
* the class that provides it, the method to call, and dependencies.
*/
final readonly class DependencyGraphNode
{
/**
* @param string $returnType The type that this initializer returns/provides
* @param ClassName $className The class containing the initializer method
* @param MethodName $methodName The method name to invoke
* @param string[] $dependencies Array of return types this node depends on
*/
public function __construct(
public string $returnType,
public ClassName $className,
public MethodName $methodName,
public array $dependencies = []
) {
}
/**
* Create a node from legacy array format
*
* @param string $returnType
* @param array{class: string, method: string, dependencies?: string[]} $nodeData
* @return self
*/
public static function fromArray(string $returnType, array $nodeData): self
{
return new self(
returnType: $returnType,
className: ClassName::create($nodeData['class']),
methodName: MethodName::create($nodeData['method']),
dependencies: $nodeData['dependencies'] ?? []
);
}
/**
* Convert to legacy array format for backward compatibility
*
* @return array{class: string, method: string, dependencies: string[]}
*/
public function toArray(): array
{
return [
'class' => $this->className->getFullyQualified(),
'method' => $this->methodName->toString(),
'dependencies' => $this->dependencies,
];
}
/**
* Check if this node has any dependencies
*/
public function hasDependencies(): bool
{
return ! empty($this->dependencies);
}
/**
* Check if this node depends on a specific return type
*/
public function dependsOn(string $returnType): bool
{
return in_array($returnType, $this->dependencies, true);
}
/**
* Get a new node with additional dependencies
*
* @param string[] $additionalDependencies
*/
public function withDependencies(array $additionalDependencies): self
{
return new self(
returnType: $this->returnType,
className: $this->className,
methodName: $this->methodName,
dependencies: array_unique(array_merge($this->dependencies, $additionalDependencies))
);
}
/**
* Get the fully qualified class name as string
*/
public function getClassName(): string
{
return $this->className->getFullyQualified();
}
/**
* Get the method name as string
*/
public function getMethodName(): string
{
return $this->methodName->toString();
}
/**
* Get a human-readable string representation
*/
public function toString(): string
{
$deps = empty($this->dependencies)
? 'no dependencies'
: 'depends on: ' . implode(', ', $this->dependencies);
return "{$this->returnType} -> {$this->getClassName()}::{$this->getMethodName()} ({$deps})";
}
/**
* Get debug information
*
* @return array{return_type: string, class: string, method: string, dependencies: string[], has_dependencies: bool}
*/
public function getDebugInfo(): array
{
return [
'return_type' => $this->returnType,
'class' => $this->getClassName(),
'method' => $this->getMethodName(),
'dependencies' => $this->dependencies,
'has_dependencies' => $this->hasDependencies(),
];
}
}