diff --git a/src/Framework/DI/DefaultContainer.php b/src/Framework/DI/DefaultContainer.php index 9425e96e..ee36542b 100644 --- a/src/Framework/DI/DefaultContainer.php +++ b/src/Framework/DI/DefaultContainer.php @@ -12,6 +12,8 @@ use App\Framework\DI\Exceptions\ContainerException; use App\Framework\DI\Exceptions\CyclicDependencyException; use App\Framework\DI\Exceptions\LazyLoadingException; use App\Framework\Discovery\Results\DiscoveryRegistry; +use App\Framework\DI\FailedInitializerRegistry; +use App\Framework\DI\ValueObjects\FailedInitializer; use App\Framework\Logging\Logger; use App\Framework\Logging\ValueObjects\LogContext; use App\Framework\Metrics\FrameworkMetricsCollector; @@ -217,6 +219,10 @@ final class DefaultContainer implements Container // Try to get DiscoveryRegistry from container and include discovered initializers $discoveredInitializers = []; + $matchingInitializers = []; + $suggestedInitializer = null; + $failedInitializer = null; + if ($this->has(DiscoveryRegistry::class)) { try { $discoveryRegistry = $this->get(DiscoveryRegistry::class); @@ -227,17 +233,53 @@ final class DefaultContainer implements Container fn($attr) => $attr->className->getFullyQualified(), $initializerResults ); + + // Spezielle Behandlung für Interfaces: Suche nach Initializern die dieses Interface zurückgeben + if (interface_exists($class)) { + foreach ($initializerResults as $initializer) { + $returnType = $initializer->additionalData['return'] ?? null; + if ($returnType === $class) { + $matchingInitializers[] = $initializer->className->getFullyQualified(); + } + } + + // Vorschlag basierend auf Interface-Name (z.B. ComponentRegistryInterface -> ComponentRegistryInitializer) + $interfaceName = basename(str_replace('\\', '/', $class)); + $suggestedName = str_replace('Interface', '', $interfaceName) . 'Initializer'; + + foreach ($discoveredInitializers as $initializer) { + if (str_contains($initializer, $suggestedName)) { + $suggestedInitializer = $initializer; + break; + } + } + } } } catch (\Throwable $e) { // Silently ignore errors when trying to access DiscoveryRegistry to avoid masking the original error } } + // Prüfe ob ein Initializer für dieses Interface fehlgeschlagen ist + if ($this->has(FailedInitializerRegistry::class)) { + try { + $failedRegistry = $this->get(FailedInitializerRegistry::class); + if ($failedRegistry->hasFailedInitializer($class)) { + $failedInitializer = $failedRegistry->getFailedInitializer($class); + } + } catch (\Throwable $e) { + // Silently ignore errors when trying to access FailedInitializerRegistry + } + } + throw ClassNotInstantiable::fromContainerContext( class: $class, dependencyChain: $this->resolving, availableBindings: $availableBindings, - discoveredInitializers: $discoveredInitializers + discoveredInitializers: $discoveredInitializers, + matchingInitializers: $matchingInitializers, + suggestedInitializer: $suggestedInitializer, + failedInitializer: $failedInitializer ); } @@ -261,12 +303,52 @@ final class DefaultContainer implements Container default => 'object' }; + // Für callable bindings: Prüfe ob ein Initializer fehlgeschlagen ist + $failedInitializer = null; + $matchingInitializer = null; + + if ($bindingType === 'callable') { + // Prüfe ob ein fehlgeschlagener Initializer für diese Klasse existiert + if ($this->has(FailedInitializerRegistry::class)) { + try { + $failedRegistry = $this->get(FailedInitializerRegistry::class); + if ($failedRegistry->hasFailedInitializer($class)) { + $failedInitializer = $failedRegistry->getFailedInitializer($class); + } + } catch (\Throwable $registryError) { + // Silently ignore errors when trying to access FailedInitializerRegistry + } + } + + // Suche auch in DiscoveryRegistry nach Initializern die diese Klasse zurückgeben + if ($failedInitializer === null && $this->has(DiscoveryRegistry::class)) { + try { + $discoveryRegistry = $this->get(DiscoveryRegistry::class); + $initializerResults = $discoveryRegistry->attributes->get(Initializer::class); + + if (! empty($initializerResults)) { + foreach ($initializerResults as $initializer) { + $returnType = $initializer->additionalData['return'] ?? null; + if ($returnType === $class) { + $matchingInitializer = $initializer->className->getFullyQualified(); + break; + } + } + } + } catch (\Throwable $registryError) { + // Silently ignore errors when trying to access DiscoveryRegistry + } + } + } + throw ClassResolutionException::fromBindingResolution( class: $class, previous: $e, availableBindings: array_keys($this->bindings->getAllBindings()), dependencyChain: $this->resolving, - bindingType: $bindingType + bindingType: $bindingType, + failedInitializer: $failedInitializer, + matchingInitializer: $matchingInitializer ); } } diff --git a/src/Framework/DI/Exceptions/ClassNotInstantiable.php b/src/Framework/DI/Exceptions/ClassNotInstantiable.php index e26dcfad..0686bfae 100644 --- a/src/Framework/DI/Exceptions/ClassNotInstantiable.php +++ b/src/Framework/DI/Exceptions/ClassNotInstantiable.php @@ -3,6 +3,7 @@ declare(strict_types=1); namespace App\Framework\DI\Exceptions; +use App\Framework\DI\ValueObjects\FailedInitializer; use App\Framework\Exception\ExceptionContext; /** @@ -18,12 +19,18 @@ final class ClassNotInstantiable extends ContainerException * @param string[] $dependencyChain * @param string[] $availableBindings * @param string[] $discoveredInitializers + * @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 */ public static function fromContainerContext( string $class, array $dependencyChain, array $availableBindings, - array $discoveredInitializers = [] + array $discoveredInitializers = [], + array $matchingInitializers = [], + ?string $suggestedInitializer = null, + ?FailedInitializer $failedInitializer = null ): self { // Calculate similar bindings based on class name $similarBindings = array_filter($availableBindings, function ($binding) use ($class) { @@ -35,7 +42,10 @@ final class ClassNotInstantiable extends ContainerException dependencyChain: $dependencyChain, availableBindings: $availableBindings, similarBindings: $similarBindings, - discoveredInitializers: $discoveredInitializers + discoveredInitializers: $discoveredInitializers, + matchingInitializers: $matchingInitializers, + suggestedInitializer: $suggestedInitializer, + failedInitializer: $failedInitializer ); } @@ -45,6 +55,7 @@ final class ClassNotInstantiable extends ContainerException * @param string[] $availableBindings * @param string[] $similarBindings * @param string[] $discoveredInitializers + * @param string[] $matchingInitializers */ public function __construct( public readonly string $class, @@ -52,27 +63,68 @@ final class ClassNotInstantiable extends ContainerException public readonly array $availableBindings, public readonly array $similarBindings = [], public readonly array $discoveredInitializers = [], + public readonly array $matchingInitializers = [], + public readonly ?string $suggestedInitializer = null, + public readonly ?FailedInitializer $failedInitializer = null, ) { $dependencyChainStr = implode(' -> ', $this->dependencyChain); + $isInterface = interface_exists($this->class); - $message = "Cannot instantiate class '{$this->class}': class is not instantiable (interface, abstract class, or trait).\n" . - "Dependency resolution chain: {$dependencyChainStr}\n" . - 'Total available bindings: ' . count($this->availableBindings) . "\n"; + $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"; + + $message .= "Dependency resolution chain: {$dependencyChainStr}\n"; + + // Spezielle Behandlung für Interfaces + if ($isInterface) { + // Matching Initializer + if (! empty($this->matchingInitializers)) { + $message .= "\nFound " . count($this->matchingInitializers) . " initializer(s) that return this interface:\n"; + foreach ($this->matchingInitializers as $matching) { + $message .= " - {$matching}\n"; + } + } + + // Fehlgeschlagener Initializer + if ($this->failedInitializer !== null) { + $message .= "\n⚠️ 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"; + } else { + // Vorgeschlagener Initializer + if ($this->suggestedInitializer !== null) { + $message .= "\nSuggested initializer (based on interface name):\n"; + $message .= " - {$this->suggestedInitializer}\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"; + $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 .= "\nTotal available bindings: " . count($this->availableBindings) . "\n"; if (!empty($this->similarBindings)) { $message .= 'Similar bindings found: ' . implode(', ', $this->similarBindings) . "\n"; } if (!empty($this->discoveredInitializers)) { - $initializers = []; foreach($this->discoveredInitializers as $initializer) { $array = explode('\\', $initializer); $initializers[] = end($array); } - $message .= "Discovered initializers (" . count($this->discoveredInitializers) . "): " . implode(', ', $initializers) . "\n"; } else { @@ -86,6 +138,9 @@ final class ClassNotInstantiable extends ContainerException 'availableBindings' => $this->availableBindings, 'similarBindings' => $this->similarBindings, 'discoveredInitializers' => $this->discoveredInitializers, + 'matchingInitializers' => $this->matchingInitializers, + 'suggestedInitializer' => $this->suggestedInitializer, + 'failedInitializer' => $this->failedInitializer?->initializerClass->getFullyQualified(), 'bindingCount' => count($this->availableBindings), 'initializerCount' => count($this->discoveredInitializers), ]); diff --git a/src/Framework/DI/Exceptions/ClassResolutionException.php b/src/Framework/DI/Exceptions/ClassResolutionException.php index 11794949..b65fb666 100644 --- a/src/Framework/DI/Exceptions/ClassResolutionException.php +++ b/src/Framework/DI/Exceptions/ClassResolutionException.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Framework\DI\Exceptions; +use App\Framework\DI\ValueObjects\FailedInitializer; use App\Framework\Exception\ExceptionContext; /** @@ -19,15 +20,24 @@ final class ClassResolutionException extends ContainerException * @param string[] $availableBindings * @param string[] $dependencyChain * @param string $bindingType One of 'callable', 'string', or 'object' + * @param FailedInitializer|null $failedInitializer Fehlgeschlagener Initializer für diese Klasse + * @param string|null $matchingInitializer Initializer-Klasse die diese Klasse zurückgibt (falls bekannt) */ public static function fromBindingResolution( string $class, \Throwable $previous, array $availableBindings, array $dependencyChain, - string $bindingType + string $bindingType, + ?FailedInitializer $failedInitializer = null, + ?string $matchingInitializer = null ): self { - $reason = "Binding resolution failed (binding type: {$bindingType}): " . $previous->getMessage(); + // Für callable bindings: Spezielle Fehlermeldung wenn Initializer beteiligt ist + if ($bindingType === 'callable' && ($failedInitializer !== null || $matchingInitializer !== null)) { + $reason = "Initializer failed during execution."; + } else { + $reason = "Binding resolution failed (binding type: {$bindingType}): " . $previous->getMessage(); + } return new self( class: $class, @@ -35,7 +45,9 @@ final class ClassResolutionException extends ContainerException availableBindings: $availableBindings, dependencyChain: $dependencyChain, previous: $previous, - bindingType: $bindingType + bindingType: $bindingType, + failedInitializer: $failedInitializer, + matchingInitializer: $matchingInitializer ); } @@ -73,11 +85,40 @@ final class ClassResolutionException extends ContainerException array $dependencyChain, int $code = 0, ?\Throwable $previous = null, - ?string $bindingType = null + ?string $bindingType = null, + public readonly ?FailedInitializer $failedInitializer = null, + public readonly ?string $matchingInitializer = null ) { - $message = "Cannot resolve class '{$class}': {$reason}. " . - "Available bindings: " . implode(', ', $availableBindings) . - ". Dependency chain: " . implode(' -> ', $dependencyChain); + $dependencyChainStr = implode(' -> ', $dependencyChain); + + // Für callable bindings mit fehlgeschlagenem Initializer: Detaillierte Fehlermeldung + if ($bindingType === 'callable' && ($this->failedInitializer !== null || $this->matchingInitializer !== null)) { + $message = "Cannot resolve class '{$class}': {$reason}\n\n"; + $message .= "⚠️ Initializer failed:\n"; + + if ($this->failedInitializer !== null) { + // Initializer wurde beim Registrieren als fehlgeschlagen getrackt + $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"; + } elseif ($this->matchingInitializer !== null) { + // Initializer wurde gefunden, aber nicht getrackt (Fehler beim Factory-Aufruf) + $message .= " - Initializer: {$this->matchingInitializer}\n"; + $message .= " - Return Type: {$class}\n"; + $errorMessage = $previous !== null ? $previous->getMessage() : $reason; + $message .= " - Error: {$errorMessage}\n"; + $message .= " - Exception: " . ($previous !== null ? get_class($previous) : 'Unknown') . "\n"; + } + + $message .= " - Check logs for full stack trace\n\n"; + $message .= "Dependency resolution chain: {$dependencyChainStr}\n"; + $message .= "Available bindings: " . implode(', ', $availableBindings); + } else { + $message = "Cannot resolve class '{$class}': {$reason}. " . + "Available bindings: " . implode(', ', $availableBindings) . + ". Dependency chain: {$dependencyChainStr}"; + } $contextData = [ 'class' => $class, @@ -91,6 +132,17 @@ final class ClassResolutionException extends ContainerException $contextData['bindingType'] = $bindingType; } + if ($this->failedInitializer !== null) { + $contextData['failedInitializer'] = $this->failedInitializer->initializerClass->getFullyQualified(); + $contextData['failedReturnType'] = $this->failedInitializer->returnType->getFullyQualified(); + $contextData['failedErrorMessage'] = $this->failedInitializer->errorMessage; + $contextData['failedExceptionClass'] = $this->failedInitializer->exceptionClass->getFullyQualified(); + } + + if ($this->matchingInitializer !== null) { + $contextData['matchingInitializer'] = $this->matchingInitializer; + } + $context = ExceptionContext::forOperation('class_resolution', 'DI Container') ->withData($contextData); diff --git a/src/Framework/DI/FailedInitializerRegistry.php b/src/Framework/DI/FailedInitializerRegistry.php new file mode 100644 index 00000000..8d9b8bd7 --- /dev/null +++ b/src/Framework/DI/FailedInitializerRegistry.php @@ -0,0 +1,81 @@ + Indexed by return type */ + private array $byReturnType; + + /** @var array Indexed by initializer class */ + private array $byInitializerClass; + + /** + * @param FailedInitializer ...$failedInitializers + */ + public function __construct(FailedInitializer ...$failedInitializers) + { + foreach ($failedInitializers as $failedInitializer) { + $returnType = $failedInitializer->returnType->getFullyQualified(); + $initializerClass = $failedInitializer->initializerClass->getFullyQualified(); + + $byReturnType[$returnType] = $failedInitializer; + $byInitializerClass[$initializerClass] = $failedInitializer; + } + $this->byReturnType = $byReturnType; + $this->byInitializerClass = $byInitializerClass; + } + + /** + * Prüft ob ein Initializer für einen Return-Type fehlgeschlagen ist + */ + public function hasFailedInitializer(string $returnType): bool + { + return isset($this->byReturnType[$returnType]); + } + + /** + * Gibt einen fehlgeschlagenen Initializer für einen Return-Type zurück + */ + public function getFailedInitializer(string $returnType): ?FailedInitializer + { + return $this->byReturnType[$returnType] ?? null; + } + + /** + * Gibt alle fehlgeschlagenen Initializer zurück + * @return array + */ + public function getAll(): array + { + return array_values($this->byReturnType); + } + + /** + * Gibt einen fehlgeschlagenen Initializer nach Initializer-Klasse zurück + */ + public function getByInitializerClass(string $class): ?FailedInitializer + { + return $this->byInitializerClass[$class] ?? null; + } + + /** + * Gibt einen fehlgeschlagenen Initializer nach Initializer-Klasse zurück (mit ClassName) + */ + public function getByInitializerClassName(ClassName $className): ?FailedInitializer + { + return $this->getByInitializerClass($className->getFullyQualified()); + } +} + diff --git a/src/Framework/DI/ValueObjects/FailedInitializer.php b/src/Framework/DI/ValueObjects/FailedInitializer.php new file mode 100644 index 00000000..83d1ff63 --- /dev/null +++ b/src/Framework/DI/ValueObjects/FailedInitializer.php @@ -0,0 +1,21 @@ +debug("InitializerProcessor: Processing " . count($initializerResults) . " initializers"); $dependencyGraph = new InitializerDependencyGraph($this->reflectionProvider); + /** @var FailedInitializer[] */ + $failedInitializers = []; // Phase 1: Setup-Initializer sofort ausführen & Service-Initializer zum Graph hinzufügen foreach ($initializerResults as $discoveredAttribute) { @@ -94,18 +98,35 @@ final readonly class InitializerProcessor ]) ); + // Track fehlgeschlagene Initializer für spätere Diagnose + if ($returnType !== null && $returnType !== 'void') { + $failedInitializers[] = new FailedInitializer( + initializerClass: $discoveredAttribute->className, + returnType: ClassName::create($returnType), + errorMessage: $e->getMessage(), + exceptionClass: ClassName::create(get_class($e)) + ); + } + // Skip failed initializers to prevent breaking the entire application } } // Phase 2: Service-Initializer in optimaler Reihenfolge registrieren - $this->registerServicesWithDependencyGraph($dependencyGraph); + $this->registerServicesWithDependencyGraph($dependencyGraph, $failedInitializers); + + // Phase 3: Registry für fehlgeschlagene Initializer im Container registrieren + if (! empty($failedInitializers)) { + $registry = new FailedInitializerRegistry(...$failedInitializers); + $this->container->instance(FailedInitializerRegistry::class, $registry); + } } /** * Registriert Services basierend auf Dependency-Graph in optimaler Reihenfolge + * @param FailedInitializer[] $failedInitializers */ - private function registerServicesWithDependencyGraph(InitializerDependencyGraph $graph): void + private function registerServicesWithDependencyGraph(InitializerDependencyGraph $graph, array &$failedInitializers): void { try { $executionOrder = $graph->getExecutionOrder(); @@ -118,7 +139,8 @@ final readonly class InitializerProcessor $this->registerLazyService( $returnType, $node->getClassName(), - $node->getMethodName() + $node->getMethodName(), + $failedInitializers ); } } @@ -139,7 +161,8 @@ final readonly class InitializerProcessor $this->registerLazyService( $returnType, $node->getClassName(), - $node->getMethodName() + $node->getMethodName(), + $failedInitializers ); } } catch (\Throwable $e) { @@ -155,7 +178,8 @@ final readonly class InitializerProcessor $this->registerLazyService( $returnType, $node->getClassName(), - $node->getMethodName() + $node->getMethodName(), + $failedInitializers ); } } @@ -186,8 +210,9 @@ final readonly class InitializerProcessor /** * Registriert einen Service lazy im Container mit Dual-Registration für Interfaces + * @param FailedInitializer[] $failedInitializers */ - private function registerLazyService(string $returnType, string $className, string $methodName): void + private function registerLazyService(string $returnType, string $className, string $methodName, array &$failedInitializers): void { $factory = function ($container) use ($className, $methodName, $returnType) { try { @@ -222,6 +247,14 @@ final readonly class InitializerProcessor ]) ); + // Track fehlgeschlagenen Initializer für spätere Diagnose + $failedInitializers[] = new FailedInitializer( + initializerClass: ClassName::create($className), + returnType: ClassName::create($returnType), + errorMessage: $e->getMessage(), + exceptionClass: ClassName::create(get_class($e)) + ); + // Service registration failed - continue to prevent breaking the entire application } } diff --git a/src/Framework/Router/RouterSetup.php b/src/Framework/Router/RouterSetup.php index 269c8d69..30b1ebfd 100644 --- a/src/Framework/Router/RouterSetup.php +++ b/src/Framework/Router/RouterSetup.php @@ -15,6 +15,8 @@ use App\Framework\DI\DefaultContainer; use App\Framework\DI\Initializer; use App\Framework\Discovery\Results\DiscoveryRegistry; use App\Framework\Http\Method; +use Exception; +use PHPUnit\Runner\ErrorException; /** * Verantwortlich für die Einrichtung des Routers