diff --git a/public/index.php b/public/index.php index cd809ae5..02eccc90 100644 --- a/public/index.php +++ b/public/index.php @@ -11,16 +11,6 @@ use App\Framework\Performance\MemoryMonitor; require __DIR__ . '/../vendor/autoload.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 $basePath = dirname(__DIR__); // Create dependencies for enhanced performance collector diff --git a/src/Framework/Core/AppBootstrapper.php b/src/Framework/Core/AppBootstrapper.php index 36ea1243..d7ca42b3 100644 --- a/src/Framework/Core/AppBootstrapper.php +++ b/src/Framework/Core/AppBootstrapper.php @@ -50,7 +50,7 @@ final readonly class AppBootstrapper // Initialize environment with encryption support $env = $this->initializeEnvironment(); - + // Workaround: If REDIS_PASSWORD_FILE is not set but expected file exists, set it manually // This handles cases where Docker Compose doesn't set the variable correctly $expectedRedisPasswordFile = '/run/secrets/redis_password'; @@ -59,62 +59,6 @@ final readonly class AppBootstrapper $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 $this->container->instance(Environment::class, $env); @@ -129,30 +73,6 @@ final readonly class AppBootstrapper // Register MemoryMonitor as singleton $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 diff --git a/src/Framework/DI/DefaultContainer.php b/src/Framework/DI/DefaultContainer.php index ee36542b..5f5f095b 100644 --- a/src/Framework/DI/DefaultContainer.php +++ b/src/Framework/DI/DefaultContainer.php @@ -8,6 +8,7 @@ use App\Framework\Core\ValueObjects\ClassName; use App\Framework\DI\Exceptions\ClassNotInstantiable; use App\Framework\DI\Exceptions\ClassNotResolvableException; use App\Framework\DI\Exceptions\ClassResolutionException; +use App\Framework\DI\ProactiveInitializerFinder; use App\Framework\DI\Exceptions\ContainerException; use App\Framework\DI\Exceptions\CyclicDependencyException; 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 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); } @@ -217,6 +224,24 @@ final class DefaultContainer implements Container { $availableBindings = array_keys($this->bindings->getAllBindings()); + // Bestimme den Typ der Klasse mit Reflection + $className = ClassName::create($class); + $isInterface = false; + $isAbstract = false; + $isTrait = false; + + try { + $reflection = $this->reflectionProvider->getClass($className); + $isInterface = $reflection->isInterface(); + $isAbstract = $reflection->isAbstract(); + $isTrait = $reflection->isTrait(); + } catch (\Throwable $e) { + // Fallback zu einfachen Prüfungen wenn Reflection fehlschlägt + $isInterface = interface_exists($class); + // Für Abstract und Trait können wir nicht so einfach prüfen, aber wir haben bereits + // festgestellt dass die Klasse nicht instanziierbar ist + } + // Try to get DiscoveryRegistry from container and include discovered initializers $discoveredInitializers = []; $matchingInitializers = []; @@ -235,7 +260,7 @@ final class DefaultContainer implements Container ); // Spezielle Behandlung für Interfaces: Suche nach Initializern die dieses Interface zurückgeben - if (interface_exists($class)) { + if ($isInterface) { foreach ($initializerResults as $initializer) { $returnType = $initializer->additionalData['return'] ?? null; if ($returnType === $class) { @@ -279,7 +304,10 @@ final class DefaultContainer implements Container discoveredInitializers: $discoveredInitializers, matchingInitializers: $matchingInitializers, 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(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; + } + } } diff --git a/src/Framework/DI/Exceptions/ClassNotInstantiable.php b/src/Framework/DI/Exceptions/ClassNotInstantiable.php index 0686bfae..10a6d5c4 100644 --- a/src/Framework/DI/Exceptions/ClassNotInstantiable.php +++ b/src/Framework/DI/Exceptions/ClassNotInstantiable.php @@ -22,6 +22,9 @@ final class ClassNotInstantiable extends ContainerException * @param string[] $matchingInitializers Initializer-Klassen die dieses Interface zurückgeben * @param string|null $suggestedInitializer Vorgeschlagener Initializer basierend auf Interface-Name * @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( string $class, @@ -30,7 +33,10 @@ final class ClassNotInstantiable extends ContainerException array $discoveredInitializers = [], array $matchingInitializers = [], ?string $suggestedInitializer = null, - ?FailedInitializer $failedInitializer = null + ?FailedInitializer $failedInitializer = null, + bool $isInterface = false, + bool $isAbstract = false, + bool $isTrait = false ): self { // Calculate similar bindings based on class name $similarBindings = array_filter($availableBindings, function ($binding) use ($class) { @@ -45,7 +51,10 @@ final class ClassNotInstantiable extends ContainerException discoveredInitializers: $discoveredInitializers, matchingInitializers: $matchingInitializers, 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 ?string $suggestedInitializer = 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); - $isInterface = interface_exists($this->class); - $message = $isInterface - ? "Cannot instantiate interface '{$this->class}': interface cannot be instantiated directly.\n" - : "Cannot instantiate class '{$this->class}': class is not instantiable (interface, abstract class, or trait).\n"; + // Spezifische Fehlermeldungen je nach Typ + if ($this->isInterface) { + $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"; - - // Spezielle Behandlung für Interfaces - if ($isInterface) { - // Matching Initializer + // IMMER Initializer-Informationen anzeigen 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) { $message .= " - {$matching}\n"; } + $message .= "\n"; + } else { + $message .= "No initializers found that return this interface.\n\n"; } // Fehlgeschlagener Initializer 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 .= " - Return Type: {$this->failedInitializer->returnType->getFullyQualified()}\n"; $message .= " - Error: {$this->failedInitializer->errorMessage}\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 { // Vorgeschlagener Initializer if ($this->suggestedInitializer !== null) { - $message .= "\nSuggested initializer (based on interface name):\n"; - $message .= " - {$this->suggestedInitializer}\n"; + $message .= "Suggested initializer (based on interface name):\n"; + $message .= " - {$this->suggestedInitializer}\n\n"; } // Hinweise wenn kein Initializer gefunden wurde - if (empty($this->matchingInitializers) && $this->suggestedInitializer === null) { - $message .= "\nIf initializer exists but wasn't found in discovery:\n"; + if (empty($this->matchingInitializers)) { + $message .= "If initializer exists but wasn't found in discovery:\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 .= " - 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"; if (!empty($this->similarBindings)) { diff --git a/src/Framework/DI/ProactiveInitializerFinder.php b/src/Framework/DI/ProactiveInitializerFinder.php new file mode 100644 index 00000000..6f1278ce --- /dev/null +++ b/src/Framework/DI/ProactiveInitializerFinder.php @@ -0,0 +1,316 @@ +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); + } +} + diff --git a/src/Framework/DI/ValueObjects/InitializerInfo.php b/src/Framework/DI/ValueObjects/InitializerInfo.php new file mode 100644 index 00000000..8bf76f18 --- /dev/null +++ b/src/Framework/DI/ValueObjects/InitializerInfo.php @@ -0,0 +1,21 @@ +debug("InitializerProcessor: Processing " . count($initializerResults) . " initializers"); $dependencyGraph = new InitializerDependencyGraph($this->reflectionProvider); - /** @var FailedInitializer[] */ + /** @var FailedInitializer[] $failedInitializers */ $failedInitializers = []; // Phase 1: Setup-Initializer sofort ausführen & Service-Initializer zum Graph hinzufügen