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:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
132
src/Framework/DI/Dependency/ConstructorCache.php
Normal file
132
src/Framework/DI/Dependency/ConstructorCache.php
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
119
src/Framework/DI/Dependency/ConstructorCacheEntry.php
Normal file
119
src/Framework/DI/Dependency/ConstructorCacheEntry.php
Normal 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);
|
||||
}
|
||||
}
|
||||
68
src/Framework/DI/Dependency/Dependency.php
Normal file
68
src/Framework/DI/Dependency/Dependency.php
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
86
src/Framework/DI/Dependency/DependencyCollection.php
Normal file
86
src/Framework/DI/Dependency/DependencyCollection.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
13
src/Framework/DI/Dependency/DependencyType.php
Normal file
13
src/Framework/DI/Dependency/DependencyType.php
Normal 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';
|
||||
}
|
||||
205
src/Framework/DI/Dependency/ParameterCacheCollection.php
Normal file
205
src/Framework/DI/Dependency/ParameterCacheCollection.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
117
src/Framework/DI/Dependency/ParameterCacheInfo.php
Normal file
117
src/Framework/DI/Dependency/ParameterCacheInfo.php
Normal 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()),
|
||||
};
|
||||
}
|
||||
}
|
||||
25
src/Framework/DI/Dependency/ResolvedParameter.php
Normal file
25
src/Framework/DI/Dependency/ResolvedParameter.php
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
186
src/Framework/DI/InitializerDependencyGraph.php
Normal file
186
src/Framework/DI/InitializerDependencyGraph.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()}'"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
137
src/Framework/DI/ValueObjects/DependencyGraphNode.php
Normal file
137
src/Framework/DI/ValueObjects/DependencyGraphNode.php
Normal 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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user