feat(di): add proactive initializer finder for interface resolution

- Add ProactiveInitializerFinder to search for initializers when not found in registry
- Add InitializerInfo value object to store initializer metadata
- Implement multi-step search strategy: DefaultImplementation, naming convention, directory, subdirectories, module
- Integrate proactive finder into DefaultContainer for better interface resolution
- Simplify AppBootstrapper by moving initialization logic to DefaultContainer
- Improve error messages in ClassNotInstantiable with proactive finder context
This commit is contained in:
2025-11-03 17:45:47 +01:00
parent 8c264f3781
commit 9cad445aaf
7 changed files with 476 additions and 113 deletions

View File

@@ -11,16 +11,6 @@ use App\Framework\Performance\MemoryMonitor;
require __DIR__ . '/../vendor/autoload.php'; require __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/../src/Framework/Debug/helpers.php'; require __DIR__ . '/../src/Framework/Debug/helpers.php';
error_log('Starting application...');
error_log("------ ENVIRONMENT VARIABLES ------");
error_log(print_r($_ENV, true));
error_log("------ SERVER VARIABLES ------");
error_log(print_r($_SERVER, true));
// Anwendung initialisieren und ausführen // Anwendung initialisieren und ausführen
$basePath = dirname(__DIR__); $basePath = dirname(__DIR__);
// Create dependencies for enhanced performance collector // Create dependencies for enhanced performance collector

View File

@@ -59,62 +59,6 @@ final readonly class AppBootstrapper
$env = $env->withVariable('REDIS_PASSWORD_FILE', $expectedRedisPasswordFile); $env = $env->withVariable('REDIS_PASSWORD_FILE', $expectedRedisPasswordFile);
} }
error_log("------ ENVIRONMENT VARIABLES ------");
error_log(print_r($env->all(true), true));
error_log("------------------------------------");
// Debug: Check for specific Docker Secrets
error_log("------ DOCKER SECRETS DEBUG ------");
error_log("REDIS_PASSWORD_FILE exists: " . ($env->has('REDIS_PASSWORD_FILE') ? 'YES' : 'NO'));
error_log("REDIS_PASSWORD_FILE value: " . ($env->get('REDIS_PASSWORD_FILE') ?? 'NOT SET'));
error_log("REDIS_PASSWORD resolved: " . ($env->has('REDIS_PASSWORD') ? 'YES' : 'NO'));
error_log("REDIS_PASSWORD value: " . (empty($env->get('REDIS_PASSWORD')) ? 'EMPTY' : 'SET'));
// Check system environment directly
error_log("------ SYSTEM ENVIRONMENT CHECK ------");
error_log("getenv('REDIS_PASSWORD_FILE'): " . (getenv('REDIS_PASSWORD_FILE') !== false ? getenv('REDIS_PASSWORD_FILE') : 'NOT SET'));
error_log("isset(\$_ENV['REDIS_PASSWORD_FILE']): " . (isset($_ENV['REDIS_PASSWORD_FILE']) ? 'YES' : 'NO'));
if (isset($_ENV['REDIS_PASSWORD_FILE'])) {
error_log("\$_ENV['REDIS_PASSWORD_FILE']: " . $_ENV['REDIS_PASSWORD_FILE']);
}
error_log("isset(\$_SERVER['REDIS_PASSWORD_FILE']): " . (isset($_SERVER['REDIS_PASSWORD_FILE']) ? 'YES' : 'NO'));
if (isset($_SERVER['REDIS_PASSWORD_FILE'])) {
error_log("\$_SERVER['REDIS_PASSWORD_FILE']: " . $_SERVER['REDIS_PASSWORD_FILE']);
}
// Check all *_FILE variables
error_log("------ ALL *_FILE VARIABLES ------");
$allEnvVars = getenv();
if ($allEnvVars !== false) {
foreach ($allEnvVars as $key => $value) {
if (str_ends_with($key, '_FILE')) {
error_log("$key: $value");
}
}
}
error_log("------ \$_ENV *_FILE VARIABLES ------");
foreach ($_ENV as $key => $value) {
if (is_string($key) && str_ends_with($key, '_FILE')) {
error_log("$key: $value");
}
}
// Check if file exists at expected location
$expectedFile = '/run/secrets/redis_password';
error_log("Expected file path: $expectedFile");
error_log("File exists: " . (file_exists($expectedFile) ? 'YES' : 'NO'));
error_log("File readable: " . (is_readable($expectedFile) ? 'YES' : 'NO'));
if (file_exists($expectedFile)) {
error_log("File permissions: " . substr(sprintf('%o', fileperms($expectedFile)), -4));
error_log("File owner: " . fileowner($expectedFile));
error_log("Current process user: " . getmyuid());
if (is_readable($expectedFile)) {
$content = file_get_contents($expectedFile);
error_log("File content length: " . strlen($content ?? ''));
}
}
error_log("------------------------------------");
// Make Environment available throughout the application // Make Environment available throughout the application
$this->container->instance(Environment::class, $env); $this->container->instance(Environment::class, $env);
@@ -129,30 +73,6 @@ final readonly class AppBootstrapper
// Register MemoryMonitor as singleton // Register MemoryMonitor as singleton
$this->container->singleton(MemoryMonitor::class, $this->memoryMonitor); $this->container->singleton(MemoryMonitor::class, $this->memoryMonitor);
// Only log context in development - production doesn't need this noise
//$envType = EnvironmentType::fromEnvironment($env);
//if ($envType->isDevelopment()) {
/*if($typedConfig->app->type->isDevelopment()) {
// Fehleranzeige für die Entwicklung aktivieren
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
spl_autoload_register(function ($class) {
if (empty($class)) {
error_log('Empty class name detected in autoloader. Stack trace: ' .
json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 10)));
return false; // Don't throw, just log and continue
}
return false;
}, true, true);
}*/
} }
public function bootstrapWeb(): ApplicationInterface public function bootstrapWeb(): ApplicationInterface

View File

@@ -8,6 +8,7 @@ use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\DI\Exceptions\ClassNotInstantiable; use App\Framework\DI\Exceptions\ClassNotInstantiable;
use App\Framework\DI\Exceptions\ClassNotResolvableException; use App\Framework\DI\Exceptions\ClassNotResolvableException;
use App\Framework\DI\Exceptions\ClassResolutionException; use App\Framework\DI\Exceptions\ClassResolutionException;
use App\Framework\DI\ProactiveInitializerFinder;
use App\Framework\DI\Exceptions\ContainerException; use App\Framework\DI\Exceptions\ContainerException;
use App\Framework\DI\Exceptions\CyclicDependencyException; use App\Framework\DI\Exceptions\CyclicDependencyException;
use App\Framework\DI\Exceptions\LazyLoadingException; use App\Framework\DI\Exceptions\LazyLoadingException;
@@ -193,6 +194,12 @@ final class DefaultContainer implements Container
// Check if class is instantiable using the framework's method // Check if class is instantiable using the framework's method
if (! $reflection->isInstantiable()) { if (! $reflection->isInstantiable()) {
// Proaktive Suche nach Initializern für Interfaces
if ($reflection->isInterface() && $this->tryFindAndRegisterInitializer($class)) {
// Initializer gefunden und registriert - versuche erneut zu resolven
return $this->get($class);
}
$this->throwDetailedBindingException($class); $this->throwDetailedBindingException($class);
} }
@@ -217,6 +224,24 @@ final class DefaultContainer implements Container
{ {
$availableBindings = array_keys($this->bindings->getAllBindings()); $availableBindings = array_keys($this->bindings->getAllBindings());
// Bestimme den Typ der Klasse mit Reflection
$className = ClassName::create($class);
$isInterface = false;
$isAbstract = false;
$isTrait = false;
try {
$reflection = $this->reflectionProvider->getClass($className);
$isInterface = $reflection->isInterface();
$isAbstract = $reflection->isAbstract();
$isTrait = $reflection->isTrait();
} catch (\Throwable $e) {
// Fallback zu einfachen Prüfungen wenn Reflection fehlschlägt
$isInterface = interface_exists($class);
// Für Abstract und Trait können wir nicht so einfach prüfen, aber wir haben bereits
// festgestellt dass die Klasse nicht instanziierbar ist
}
// Try to get DiscoveryRegistry from container and include discovered initializers // Try to get DiscoveryRegistry from container and include discovered initializers
$discoveredInitializers = []; $discoveredInitializers = [];
$matchingInitializers = []; $matchingInitializers = [];
@@ -235,7 +260,7 @@ final class DefaultContainer implements Container
); );
// Spezielle Behandlung für Interfaces: Suche nach Initializern die dieses Interface zurückgeben // Spezielle Behandlung für Interfaces: Suche nach Initializern die dieses Interface zurückgeben
if (interface_exists($class)) { if ($isInterface) {
foreach ($initializerResults as $initializer) { foreach ($initializerResults as $initializer) {
$returnType = $initializer->additionalData['return'] ?? null; $returnType = $initializer->additionalData['return'] ?? null;
if ($returnType === $class) { if ($returnType === $class) {
@@ -279,7 +304,10 @@ final class DefaultContainer implements Container
discoveredInitializers: $discoveredInitializers, discoveredInitializers: $discoveredInitializers,
matchingInitializers: $matchingInitializers, matchingInitializers: $matchingInitializers,
suggestedInitializer: $suggestedInitializer, suggestedInitializer: $suggestedInitializer,
failedInitializer: $failedInitializer failedInitializer: $failedInitializer,
isInterface: $isInterface,
isAbstract: $isAbstract,
isTrait: $isTrait
); );
} }
@@ -436,4 +464,70 @@ final class DefaultContainer implements Container
$this->instances->setSingleton(DefaultContainer::class, $this); $this->instances->setSingleton(DefaultContainer::class, $this);
$this->instances->setSingleton(Container::class, $this); $this->instances->setSingleton(Container::class, $this);
} }
/**
* Lazy-getter für ProactiveInitializerFinder
*/
private function getProactiveFinder(): ProactiveInitializerFinder
{
// Erstelle Finder nur wenn benötigt
$discoveryRegistry = $this->has(DiscoveryRegistry::class) ? $this->get(DiscoveryRegistry::class) : null;
if ($discoveryRegistry === null) {
// Fallback: Leere Registry erstellen
$discoveryRegistry = \App\Framework\Discovery\Results\DiscoveryRegistry::empty();
}
$fileScanner = $this->has(\App\Framework\Filesystem\FileScanner::class)
? $this->get(\App\Framework\Filesystem\FileScanner::class)
: new \App\Framework\Filesystem\FileScanner(null, null, new \App\Framework\Filesystem\FileSystemService());
$fileSystemService = new \App\Framework\Filesystem\FileSystemService();
$classExtractor = new \App\Framework\Discovery\Processing\ClassExtractor($fileSystemService);
$pathProvider = $this->has(\App\Framework\Core\PathProvider::class)
? $this->get(\App\Framework\Core\PathProvider::class)
: new \App\Framework\Core\PathProvider(getcwd() ?: '.');
return new ProactiveInitializerFinder(
reflectionProvider: $this->reflectionProvider,
discoveryRegistry: $discoveryRegistry,
fileScanner: $fileScanner,
classExtractor: $classExtractor,
pathProvider: $pathProvider
);
}
/**
* Versucht proaktiv einen Initializer für ein Interface zu finden und zu registrieren
*
* @param string $interface Interface-Klasse
* @return bool True wenn Initializer gefunden und registriert wurde, false sonst
*/
private function tryFindAndRegisterInitializer(string $interface): bool
{
try {
$finder = $this->getProactiveFinder();
$initializerInfo = $finder->findInitializerForInterface($interface);
if ($initializerInfo === null) {
return false;
}
// Registriere Initializer als lazy binding
$initializerClass = $initializerInfo->initializerClass->getFullyQualified();
$methodName = $initializerInfo->methodName->toString();
$this->singleton($interface, function (Container $container) use ($initializerClass, $methodName) {
$instance = $container->get($initializerClass);
return $container->invoker->invoke($initializerClass, $methodName);
});
// TODO: Gefundenen Initializer in Discovery Cache nachtragen (später implementieren)
return true;
} catch (\Throwable $e) {
// Silently ignore errors during proactive search
return false;
}
}
} }

View File

@@ -22,6 +22,9 @@ final class ClassNotInstantiable extends ContainerException
* @param string[] $matchingInitializers Initializer-Klassen die dieses Interface zurückgeben * @param string[] $matchingInitializers Initializer-Klassen die dieses Interface zurückgeben
* @param string|null $suggestedInitializer Vorgeschlagener Initializer basierend auf Interface-Name * @param string|null $suggestedInitializer Vorgeschlagener Initializer basierend auf Interface-Name
* @param FailedInitializer|null $failedInitializer Fehlgeschlagener Initializer für dieses Interface * @param FailedInitializer|null $failedInitializer Fehlgeschlagener Initializer für dieses Interface
* @param bool $isInterface Ob die Klasse ein Interface ist
* @param bool $isAbstract Ob die Klasse abstract ist
* @param bool $isTrait Ob die Klasse ein Trait ist
*/ */
public static function fromContainerContext( public static function fromContainerContext(
string $class, string $class,
@@ -30,7 +33,10 @@ final class ClassNotInstantiable extends ContainerException
array $discoveredInitializers = [], array $discoveredInitializers = [],
array $matchingInitializers = [], array $matchingInitializers = [],
?string $suggestedInitializer = null, ?string $suggestedInitializer = null,
?FailedInitializer $failedInitializer = null ?FailedInitializer $failedInitializer = null,
bool $isInterface = false,
bool $isAbstract = false,
bool $isTrait = false
): self { ): self {
// Calculate similar bindings based on class name // Calculate similar bindings based on class name
$similarBindings = array_filter($availableBindings, function ($binding) use ($class) { $similarBindings = array_filter($availableBindings, function ($binding) use ($class) {
@@ -45,7 +51,10 @@ final class ClassNotInstantiable extends ContainerException
discoveredInitializers: $discoveredInitializers, discoveredInitializers: $discoveredInitializers,
matchingInitializers: $matchingInitializers, matchingInitializers: $matchingInitializers,
suggestedInitializer: $suggestedInitializer, suggestedInitializer: $suggestedInitializer,
failedInitializer: $failedInitializer failedInitializer: $failedInitializer,
isInterface: $isInterface,
isAbstract: $isAbstract,
isTrait: $isTrait
); );
} }
@@ -66,52 +75,65 @@ final class ClassNotInstantiable extends ContainerException
public readonly array $matchingInitializers = [], public readonly array $matchingInitializers = [],
public readonly ?string $suggestedInitializer = null, public readonly ?string $suggestedInitializer = null,
public readonly ?FailedInitializer $failedInitializer = null, public readonly ?FailedInitializer $failedInitializer = null,
public readonly bool $isInterface = false,
public readonly bool $isAbstract = false,
public readonly bool $isTrait = false,
) )
{ {
$dependencyChainStr = implode(' -> ', $this->dependencyChain); $dependencyChainStr = implode(' -> ', $this->dependencyChain);
$isInterface = interface_exists($this->class);
$message = $isInterface // Spezifische Fehlermeldungen je nach Typ
? "Cannot instantiate interface '{$this->class}': interface cannot be instantiated directly.\n" if ($this->isInterface) {
: "Cannot instantiate class '{$this->class}': class is not instantiable (interface, abstract class, or trait).\n"; $message = "Cannot instantiate interface '{$this->class}': interfaces cannot be instantiated directly.\n";
$message .= "An initializer must provide an implementation.\n\n";
$message .= "Dependency resolution chain: {$dependencyChainStr}\n"; // IMMER Initializer-Informationen anzeigen
// Spezielle Behandlung für Interfaces
if ($isInterface) {
// Matching Initializer
if (! empty($this->matchingInitializers)) { if (! empty($this->matchingInitializers)) {
$message .= "\nFound " . count($this->matchingInitializers) . " initializer(s) that return this interface:\n"; $message .= "Found " . count($this->matchingInitializers) . " initializer(s) that return this interface:\n";
foreach ($this->matchingInitializers as $matching) { foreach ($this->matchingInitializers as $matching) {
$message .= " - {$matching}\n"; $message .= " - {$matching}\n";
} }
$message .= "\n";
} else {
$message .= "No initializers found that return this interface.\n\n";
} }
// Fehlgeschlagener Initializer // Fehlgeschlagener Initializer
if ($this->failedInitializer !== null) { if ($this->failedInitializer !== null) {
$message .= "\n⚠️ Initializer found but failed during execution:\n"; $message .= "⚠️ Initializer found but failed during execution:\n";
$message .= " - Initializer: {$this->failedInitializer->initializerClass->getFullyQualified()}\n"; $message .= " - Initializer: {$this->failedInitializer->initializerClass->getFullyQualified()}\n";
$message .= " - Return Type: {$this->failedInitializer->returnType->getFullyQualified()}\n"; $message .= " - Return Type: {$this->failedInitializer->returnType->getFullyQualified()}\n";
$message .= " - Error: {$this->failedInitializer->errorMessage}\n"; $message .= " - Error: {$this->failedInitializer->errorMessage}\n";
$message .= " - Exception: {$this->failedInitializer->exceptionClass->getFullyQualified()}\n"; $message .= " - Exception: {$this->failedInitializer->exceptionClass->getFullyQualified()}\n";
$message .= " - Check logs for full stack trace\n"; $message .= " - Check logs for full stack trace\n\n";
} else { } else {
// Vorgeschlagener Initializer // Vorgeschlagener Initializer
if ($this->suggestedInitializer !== null) { if ($this->suggestedInitializer !== null) {
$message .= "\nSuggested initializer (based on interface name):\n"; $message .= "Suggested initializer (based on interface name):\n";
$message .= " - {$this->suggestedInitializer}\n"; $message .= " - {$this->suggestedInitializer}\n\n";
} }
// Hinweise wenn kein Initializer gefunden wurde // Hinweise wenn kein Initializer gefunden wurde
if (empty($this->matchingInitializers) && $this->suggestedInitializer === null) { if (empty($this->matchingInitializers)) {
$message .= "\nIf initializer exists but wasn't found in discovery:\n"; $message .= "If initializer exists but wasn't found in discovery:\n";
$message .= " - Try clearing discovery cache: php artisan discovery:clear\n"; $message .= " - Try clearing discovery cache: php artisan discovery:clear\n";
$message .= " - Check if it has a ContextType filter that excludes the current context\n"; $message .= " - Check if it has a ContextType filter that excludes the current context\n";
$message .= " - Ensure the initializer is registered with #[Initializer] attribute\n"; $message .= " - Ensure the initializer is registered with #[Initializer] attribute\n\n";
} }
} }
} elseif ($this->isAbstract) {
$message = "Cannot instantiate abstract class '{$this->class}': abstract classes should not be used in the framework.\n";
$message .= "Use concrete classes or interfaces instead.\n\n";
} elseif ($this->isTrait) {
$message = "Cannot instantiate trait '{$this->class}': traits cannot be instantiated directly.\n";
$message .= "Traits must be used by classes.\n\n";
} else {
// Fallback für unbekannte Fälle
$message = "Cannot instantiate class '{$this->class}': class is not instantiable (interface, abstract class, or trait).\n\n";
} }
$message .= "Dependency resolution chain: {$dependencyChainStr}\n";
$message .= "\nTotal available bindings: " . count($this->availableBindings) . "\n"; $message .= "\nTotal available bindings: " . count($this->availableBindings) . "\n";
if (!empty($this->similarBindings)) { if (!empty($this->similarBindings)) {

View File

@@ -0,0 +1,316 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI;
use App\Framework\Core\PathProvider;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\Core\ValueObjects\PhpNamespace;
use App\Framework\DI\Attributes\DefaultImplementation;
use App\Framework\DI\ValueObjects\InitializerInfo;
use App\Framework\Discovery\Processing\ClassExtractor;
use App\Framework\Discovery\Results\DiscoveryRegistry;
use App\Framework\Filesystem\File;
use App\Framework\Filesystem\FileScanner;
use App\Framework\Filesystem\ValueObjects\FilePath;
use App\Framework\Filesystem\ValueObjects\FilePattern;
use App\Framework\Reflection\ReflectionProvider;
/**
* Proaktive Suche nach Initializern für Interfaces
*
* Wenn kein Initializer in der Discovery Registry gefunden wird, sucht diese Klasse
* proaktiv nach Initializern basierend auf Namenskonventionen, Verzeichnisstruktur,
* Modulen und DefaultImplementation Attributen.
*/
final readonly class ProactiveInitializerFinder
{
public function __construct(
private ReflectionProvider $reflectionProvider,
private DiscoveryRegistry $discoveryRegistry,
private FileScanner $fileScanner,
private ClassExtractor $classExtractor,
private PathProvider $pathProvider
) {}
/**
* Findet einen Initializer für ein Interface
*
* Suchreihenfolge:
* 1. DefaultImplementation Attribut
* 2. Namenskonvention
* 3. Selber Ordner
* 4. Unterordner
* 5. Ganzes Modul
*/
public function findInitializerForInterface(string $interface): ?InitializerInfo
{
// 1. DefaultImplementation Attribut (ERSTE Suche)
$result = $this->findByDefaultImplementation($interface);
if ($result !== null) {
return $result;
}
// 2. Namenskonvention
$result = $this->findByName($interface);
if ($result !== null) {
return $result;
}
// 3. Selber Ordner
$result = $this->findInDirectory($interface);
if ($result !== null) {
return $result;
}
// 4. Unterordner
$result = $this->findInSubdirectories($interface);
if ($result !== null) {
return $result;
}
// 5. Ganzes Modul
return $this->findInModule($interface);
}
/**
* Sucht nach Initializer basierend auf DefaultImplementation Attribut
*/
private function findByDefaultImplementation(string $interface): ?InitializerInfo
{
try {
// Suche in DiscoveryRegistry nach DefaultImplementation Attributen
$defaultImplResults = $this->discoveryRegistry->attributes->get(DefaultImplementation::class);
if (empty($defaultImplResults)) {
return null;
}
foreach ($defaultImplResults as $defaultImpl) {
$implClassName = $defaultImpl->className;
// Prüfe ob das Attribut das richtige Interface spezifiziert
$defaultImplAttr = $defaultImpl->createAttributeInstance();
if ($defaultImplAttr instanceof DefaultImplementation) {
// Wenn Interface explizit angegeben, muss es übereinstimmen
if ($defaultImplAttr->interface !== null && $defaultImplAttr->interface !== $interface) {
continue;
}
}
// Prüfe ob die Klasse das Interface implementiert
$reflection = $this->reflectionProvider->getClass($implClassName);
if (!$reflection->implementsInterface($interface)) {
continue;
}
// Prüfe ob die Klasse auch Initializer Attribut hat
$initializerInfo = $this->checkInitializerAttribute($implClassName, $interface);
if ($initializerInfo !== null) {
return $initializerInfo;
}
}
} catch (\Throwable $e) {
// Silently ignore errors
}
return null;
}
/**
* Sucht nach Initializer basierend auf Namenskonvention
* SomeClassInterface -> SomeClassInitializer
*/
private function findByName(string $interface): ?InitializerInfo
{
try {
$interfaceName = ClassName::create($interface);
$shortName = $interfaceName->getShortName();
// Entferne "Interface" Suffix
if (!str_ends_with($shortName, 'Interface')) {
return null;
}
$baseName = substr($shortName, 0, -9); // "Interface" = 9 Zeichen
$initializerName = $baseName . 'Initializer';
// Konstruiere vollständigen Klassennamen
$namespace = $interfaceName->getNamespaceObject();
$initializerClassName = ClassName::fromNamespace($namespace, $initializerName);
// Prüfe ob Klasse existiert
if (!$initializerClassName->exists()) {
return null;
}
// Prüfe Initializer Attribut
return $this->checkInitializerAttribute($initializerClassName, $interface);
} catch (\Throwable $e) {
// Silently ignore errors
}
return null;
}
/**
* Sucht nach Initializer im selben Ordner wie das Interface
*/
private function findInDirectory(string $interface): ?InitializerInfo
{
try {
$interfaceName = ClassName::create($interface);
$namespace = $interfaceName->getNamespaceObject();
$directoryPath = $this->namespaceToDirectoryPath($namespace);
if ($directoryPath === null) {
return null;
}
return $this->searchInPath(FilePath::create($directoryPath), $interface, false);
} catch (\Throwable $e) {
// Silently ignore errors
}
return null;
}
/**
* Sucht nach Initializer in Unterordnern des Interface-Ordners
*/
private function findInSubdirectories(string $interface): ?InitializerInfo
{
try {
$interfaceName = ClassName::create($interface);
$namespace = $interfaceName->getNamespaceObject();
$directoryPath = $this->namespaceToDirectoryPath($namespace);
if ($directoryPath === null) {
return null;
}
return $this->searchInPath(FilePath::create($directoryPath), $interface, true);
} catch (\Throwable $e) {
// Silently ignore errors
}
return null;
}
/**
* Sucht nach Initializer im ganzen Modul
*/
private function findInModule(string $interface): ?InitializerInfo
{
try {
$interfaceName = ClassName::create($interface);
$namespace = $interfaceName->getNamespaceObject();
// Bestimme Modul-Namespace (Parent-Namespace)
$moduleNamespace = $namespace->parent();
if ($moduleNamespace === null) {
return null;
}
$directoryPath = $this->namespaceToDirectoryPath($moduleNamespace);
if ($directoryPath === null) {
return null;
}
return $this->searchInPath(FilePath::create($directoryPath), $interface, true);
} catch (\Throwable $e) {
// Silently ignore errors
}
return null;
}
/**
* Durchsucht einen Pfad nach Initializern
*/
private function searchInPath(FilePath $path, string $interface, bool $recursive): ?InitializerInfo
{
try {
foreach ($this->fileScanner->streamFiles($path, FilePattern::php()) as $file) {
$classes = $this->classExtractor->extractFromFile($file);
foreach ($classes as $className) {
if (!$className->exists()) {
continue;
}
$initializerInfo = $this->checkInitializerAttribute($className, $interface);
if ($initializerInfo !== null) {
return $initializerInfo;
}
}
}
} catch (\Throwable $e) {
// Silently ignore errors
}
return null;
}
/**
* Prüft ob eine Klasse ein Initializer Attribut hat und das Interface zurückgibt
*/
private function checkInitializerAttribute(ClassName $className, string $interface): ?InitializerInfo
{
try {
$reflection = $this->reflectionProvider->getClass($className);
$methods = $reflection->getMethods();
foreach ($methods as $method) {
// Prüfe ob Methode Initializer Attribut hat
if (!$method->hasAttribute(Initializer::class)) {
continue;
}
// Prüfe Return-Type
$returnType = $method->getReturnType();
if ($returnType === null) {
continue;
}
// Normalisiere Return-Type (entferne leading backslash, handle nullable)
$returnType = ltrim($returnType, '\\?');
// Prüfe ob Return-Type das Interface ist
if ($returnType !== $interface && $returnType !== '\\' . $interface) {
continue;
}
// Gefunden!
return new InitializerInfo(
initializerClass: $className,
methodName: MethodName::create($method->getName()),
returnType: ClassName::create($interface)
);
}
} catch (\Throwable $e) {
// Silently ignore errors
}
return null;
}
/**
* Konvertiert einen PhpNamespace in ein Verzeichnis
*/
private function namespaceToDirectoryPath(PhpNamespace $namespace): ?string
{
// Nutze PathProvider um Namespace zu Pfad zu konvertieren
$filePath = $this->pathProvider->namespaceToPath($namespace);
if ($filePath === null) {
return null;
}
// Entferne .php Extension und Dateiname - wir wollen das Verzeichnis
return dirname($filePath);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Framework\DI\ValueObjects;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
/**
* Value Object für gefundene Initializer-Informationen
*/
final readonly class InitializerInfo
{
public function __construct(
public ClassName $initializerClass,
public MethodName $methodName,
public ClassName $returnType
) {}
}

View File

@@ -48,7 +48,7 @@ final readonly class InitializerProcessor
$logger->debug("InitializerProcessor: Processing " . count($initializerResults) . " initializers"); $logger->debug("InitializerProcessor: Processing " . count($initializerResults) . " initializers");
$dependencyGraph = new InitializerDependencyGraph($this->reflectionProvider); $dependencyGraph = new InitializerDependencyGraph($this->reflectionProvider);
/** @var FailedInitializer[] */ /** @var FailedInitializer[] $failedInitializers */
$failedInitializers = []; $failedInitializers = [];
// Phase 1: Setup-Initializer sofort ausführen & Service-Initializer zum Graph hinzufügen // Phase 1: Setup-Initializer sofort ausführen & Service-Initializer zum Graph hinzufügen