Critical middlewares that must never be filtered out */ private const array CRITICAL_MIDDLEWARES = [ #'App\\Framework\\Http\\Middlewares\\DefaultResponseMiddleware', 'App\\Framework\\Http\\Middlewares\\ExceptionHandlingMiddleware', 'App\\Framework\\Http\\Middlewares\\RequestIdMiddleware', 'App\\Framework\\Http\\Middlewares\\RoutingMiddleware', ]; public function __construct( private ReflectionProvider $reflectionProvider, private Container $container, private Logger $logger ) { } /** * Resolve middleware dependencies and return them in correct execution order * * @param array $middlewareClasses * @throws MiddlewareDependencyException */ public function resolve(array $middlewareClasses): ResolvedMiddlewareStack { try { $classNames = $this->normalizeClassNames($middlewareClasses); $this->logger->debug("Starting resolution for " . count($classNames) . " middlewares"); $dependencyGraph = $this->buildDependencyGraph($classNames); $simplifiedGraph = $this->extractDependenciesForSort($dependencyGraph); $sortedClasses = $this->topologicalSort($simplifiedGraph); $availableMiddlewares = $this->filterAvailableWithFallbacks($sortedClasses); $this->validateCriticalMiddlewares($availableMiddlewares); $this->logger->info("Resolution completed with " . count($availableMiddlewares) . " middlewares"); return new ResolvedMiddlewareStack($availableMiddlewares, $dependencyGraph); } catch (MiddlewareDependencyException $e) { throw $e; } catch (\Throwable $e) { $this->logger->critical("Critical error during resolution: " . $e->getMessage()); throw MiddlewareDependencyException::cannotInstantiate( ClassName::create('Unknown'), "Dependency resolution failed: " . $e->getMessage() ); } } /** * Normalize class names to ClassName value objects * * @param array $middlewareClasses * @return array */ private function normalizeClassNames(array $middlewareClasses): array { $classNames = []; foreach ($middlewareClasses as $middlewareClass) { try { $className = ClassName::create($middlewareClass); if ($className->exists()) { $classNames[] = $className; } else { $this->logger->warning("Class not found: {$className->getShortName()}"); } } catch (\InvalidArgumentException $e) { $this->logger->warning("Invalid class name: {$middlewareClass} - " . $e->getMessage()); } } return $classNames; } /** * Build dependency graph by analyzing constructor parameters * * @param array $middlewareClasses * @return array, services: array, provides: array, optional: array, is_critical: bool}> */ private function buildDependencyGraph(array $middlewareClasses): array { $graph = []; foreach ($middlewareClasses as $className) { $classNameString = $className->getFullyQualified(); try { $graph[$classNameString] = [ 'dependencies' => $this->getMiddlewareDependencies($className), 'services' => $this->getServiceDependencies($className), 'provides' => $this->getProvidedServices($className), 'optional' => $this->getOptionalDependencies($className), 'is_critical' => $this->isCriticalMiddleware($className), ]; } catch (\Throwable $e) { $this->logger->error("Error analyzing {$className->getShortName()}: " . $e->getMessage()); // Add minimal entry for critical middlewares even if analysis fails if ($this->isCriticalMiddleware($className)) { $graph[$classNameString] = [ 'dependencies' => [], 'services' => [], 'provides' => [], 'optional' => [], 'is_critical' => true, ]; } } } return $graph; } /** * Extract dependencies for topological sorting * @param array, services: array, provides: array, optional: array, is_critical: bool}> $dependencyGraph * @return array> */ private function extractDependenciesForSort(array $dependencyGraph): array { $simplifiedGraph = []; foreach ($dependencyGraph as $className => $data) { $simplifiedGraph[$className] = $data['dependencies']; } return $simplifiedGraph; } /** * Check if middleware is critical and must never be filtered out */ private function isCriticalMiddleware(ClassName $className): bool { return in_array($className->getFullyQualified(), self::CRITICAL_MIDDLEWARES, true); } /** * Get middleware dependencies from constructor parameters * @return array */ private function getMiddlewareDependencies(ClassName $middlewareClass): array { try { if (! $middlewareClass->exists() || MethodName::construct()->existsIn($middlewareClass)) { return []; } $dependencies = []; $parameterCollection = $this->reflectionProvider->getMethodParameters( $middlewareClass, '__construct' ); foreach ($parameterCollection as $parameter) { if ($parameter->isBuiltin()) { continue; } $typeName = $parameter->getTypeName(); if (! $typeName) { continue; } $dependencyClass = ClassName::create($typeName); // Check if this parameter is another middleware if ($this->isMiddleware($dependencyClass)) { $dependencies[] = $dependencyClass->getFullyQualified(); } } return $dependencies; } catch (\Throwable $e) { $this->logger->error("Error analyzing {$middlewareClass->getShortName()}: " . $e->getMessage()); return []; } } /** * Get non-middleware service dependencies * @return array */ private function getServiceDependencies(ClassName $middlewareClass): array { try { if (! $middlewareClass->exists()) { return []; } $services = []; $parameterCollection = $this->reflectionProvider->getMethodParameters( $middlewareClass, '__construct' ); foreach ($parameterCollection as $parameter) { if ($parameter->isBuiltin()) { continue; } $typeName = $parameter->getTypeName(); if (! $typeName) { continue; } $serviceClass = ClassName::create($typeName); // Include non-middleware services if (! $this->isMiddleware($serviceClass)) { $services[] = $serviceClass->getFullyQualified(); } } return $services; } catch (\Throwable $e) { $this->logger->error("Error analyzing services for {$middlewareClass->getShortName()}: " . $e->getMessage()); return []; } } /** * Check what services this middleware provides (for future use) */ /** * @return array */ private function getProvidedServices(ClassName $middlewareClass): array { // This could be extended with attributes like #[Provides(['session', 'auth'])] $provides = []; $className = $middlewareClass->getShortName(); // Basic heuristics based on class name if (str_contains($className, 'Session')) { $provides[] = 'session'; } if (str_contains($className, 'Auth')) { $provides[] = 'authentication'; } if (str_contains($className, 'Csrf')) { $provides[] = 'csrf_protection'; } if (str_contains($className, 'Response')) { $provides[] = 'response_generation'; } if (str_contains($className, 'Routing')) { $provides[] = 'routing'; } return $provides; } /** * Get optional dependencies (non-required) */ /** * @return array */ private function getOptionalDependencies(ClassName $middlewareClass): array { try { if (! $middlewareClass->exists()) { return []; } $optional = []; $parameterCollection = $this->reflectionProvider->getMethodParameters( $middlewareClass, '__construct' ); foreach ($parameterCollection as $parameter) { if ($parameter->isOptional()) { if (! $parameter->isBuiltin()) { $typeName = $parameter->getTypeName(); if ($typeName) { $optional[] = $typeName; } } } } return $optional; } catch (\Throwable $e) { $this->logger->error("Error analyzing optional dependencies for {$middlewareClass->getShortName()}: " . $e->getMessage()); return []; } } /** * Check if a class is a middleware */ private function isMiddleware(ClassName $className): bool { try { if (! $className->exists()) { return false; } return $this->reflectionProvider->implementsInterface( $className, HttpMiddleware::class ); } catch (\Throwable $e) { $this->logger->error("Error checking middleware interface for {$className->getShortName()}: " . $e->getMessage()); return false; } } /** * Topological sort to determine execution order */ /** * @param array> $graph * @return array */ private function topologicalSort(array $graph): array { $sorted = []; $visited = []; $visiting = []; foreach (array_keys($graph) as $node) { if (! isset($visited[$node])) { $result = $this->topologicalSortVisit($node, $graph, $visited, $visiting, $sorted); $visited = $result['visited']; $visiting = $result['visiting']; $sorted = $result['sorted']; } } return array_reverse($sorted); } /** * Recursive topological sort visit (returns result instead of using references) * @param array> $graph * @param array $visited * @param array $visiting * @param array $sorted * @return array{visited: array, visiting: array, sorted: array} */ private function topologicalSortVisit(string $node, array $graph, array $visited, array $visiting, array $sorted): array { if (isset($visiting[$node])) { // Circular dependency detected $this->logger->warning("Circular dependency detected involving {$node}"); return ['visited' => $visited, 'visiting' => $visiting, 'sorted' => $sorted]; } if (isset($visited[$node])) { return ['visited' => $visited, 'visiting' => $visiting, 'sorted' => $sorted]; } $visiting[$node] = true; // Visit dependencies first foreach ($graph[$node] as $dependency) { if (isset($graph[$dependency])) { $result = $this->topologicalSortVisit($dependency, $graph, $visited, $visiting, $sorted); $visited = $result['visited']; $visiting = $result['visiting']; $sorted = $result['sorted']; } } unset($visiting[$node]); $visited[$node] = true; $sorted[] = $node; return ['visited' => $visited, 'visiting' => $visiting, 'sorted' => $sorted]; } /** * Filter out middlewares that can't be instantiated with fallback strategies * @param array $middlewareClasses * @return array */ private function filterAvailableWithFallbacks(array $middlewareClasses): array { $available = []; $filtered = []; foreach ($middlewareClasses as $middlewareClass) { $className = ClassName::create($middlewareClass); if ($this->canInstantiate($className)) { $available[] = $middlewareClass; } else { $filtered[] = $className; // Critical middlewares get special treatment if ($this->isCriticalMiddleware($className)) { // debug: CRITICAL - Cannot instantiate {$className->getShortName()}, applying fallback strategy // Try fallback strategies for critical middlewares if ($this->attemptFallbackInstantiation($className)) { $available[] = $middlewareClass; // debug: SUCCESS - Fallback strategy worked for {$className->getShortName()} } else { // debug: FAILURE - All fallback strategies failed for {$className->getShortName()} } } else { // debug: Cannot instantiate {$className->getShortName()}, skipping (not critical) } } } if (! empty($filtered)) { $filteredNames = array_map(fn (ClassName $c) => $c->getShortName(), $filtered); $this->logger->warning("Filtered out " . count($filtered) . " middlewares: " . implode(', ', $filteredNames)); } return $available; } /** * Attempt fallback instantiation strategies for critical middlewares */ private function attemptFallbackInstantiation(ClassName $className): bool { try { // Strategy 1: Try basic instantiation without dependency checking if ($this->reflectionProvider->isInstantiable($className)) { $this->logger->debug("Fallback strategy 1 (basic instantiation) succeeded for {$className->getShortName()}"); return true; } // Strategy 2: Check if we can provide minimal dependencies $parameterCollection = $this->reflectionProvider->getMethodParameters( $className, '__construct' ); $canProvideAll = true; foreach ($parameterCollection as $parameter) { if ($parameter->isBuiltin()) { continue; } if (! $parameter->isOptional()) { $typeName = $parameter->getTypeName(); if (! $typeName || ! $this->container->has($typeName)) { $canProvideAll = false; break; } } } if ($canProvideAll) { $this->logger->debug("Fallback strategy 2 (dependency provision) succeeded for {$className->getShortName()}"); return true; } // Strategy 3: For absolutely critical middlewares, allow them anyway if ($this->isCriticalMiddleware($className)) { $this->logger->warning("Fallback strategy 3 (critical override) applied for {$className->getShortName()}"); return true; } return false; } catch (\Throwable $e) { $this->logger->error("Fallback strategies failed for {$className->getShortName()}: " . $e->getMessage()); return false; } } /** * Check if a middleware can be instantiated by trying to resolve it through the container */ private function canInstantiate(ClassName $middlewareClass): bool { try { // Check if class exists if (! $middlewareClass->exists()) { $this->logger->warning("Class does not exist: {$middlewareClass->getShortName()}"); return false; } // Check if class is instantiable if (! $this->reflectionProvider->isInstantiable($middlewareClass)) { $this->logger->warning("Class is not instantiable: {$middlewareClass->getShortName()}"); return false; } // Try to actually resolve the middleware through the container // This is the most reliable way to check if all dependencies are available try { /** @var class-string $className */ $className = $middlewareClass->getFullyQualified(); $instance = $this->container->get($className); $this->logger->debug("Successfully resolved {$middlewareClass->getShortName()} through container"); return true; } catch (\Throwable $containerException) { $this->logger->warning("Cannot resolve {$middlewareClass->getShortName()} through container: " . $containerException->getMessage()); // Fallback to the old dependency checking method for more detailed info return $this->checkDependenciesManually($middlewareClass); } } catch (\Throwable $e) { $this->logger->error("Error checking instantiation for {$middlewareClass->getShortName()}: " . $e->getMessage()); return false; } } /** * Fallback method to manually check dependencies when container resolution fails */ private function checkDependenciesManually(ClassName $middlewareClass): bool { try { $parameterCollection = $this->reflectionProvider->getMethodParameters( $middlewareClass, '__construct' ); $missingDependencies = []; foreach ($parameterCollection as $parameter) { // Skip built-in types if ($parameter->isBuiltin()) { continue; } $typeName = $parameter->getTypeName(); if (! $typeName) { continue; } // Check if required dependency is available in container if (! $parameter->isOptional()) { if (! $this->container->has($typeName)) { $missingDependencies[] = ClassName::create($typeName); } } } if (! empty($missingDependencies)) { $depNames = array_map(fn (ClassName $dep) => $dep->getShortName(), $missingDependencies); $this->logger->debug("Manual dependency check - Missing dependencies for {$middlewareClass->getShortName()}: " . implode(', ', $depNames)); return false; } return true; } catch (\Throwable $e) { $this->logger->error("Error in manual dependency check for {$middlewareClass->getShortName()}: " . $e->getMessage()); return false; } } /** * Validate that all critical middlewares are present * * @throws MiddlewareDependencyException */ /** * @param array $availableMiddlewares */ private function validateCriticalMiddlewares(array $availableMiddlewares): void { $missingCritical = []; foreach (self::CRITICAL_MIDDLEWARES as $criticalMiddleware) { if (! in_array($criticalMiddleware, $availableMiddlewares, true)) { $missingCritical[] = ClassName::create($criticalMiddleware); } } if (! empty($missingCritical)) { $missingNames = array_map(fn (ClassName $c) => $c->getShortName(), $missingCritical); $this->logger->critical("CRITICAL - Missing essential middlewares: " . implode(', ', $missingNames)); throw MiddlewareDependencyException::missingDependencies( ClassName::create('CriticalMiddlewares'), $missingCritical ); } } /** * Get dependency information for debugging * * @param array $middlewareClasses * @return array */ public function getDependencyInfo(array $middlewareClasses): array { $info = []; $classNames = $this->normalizeClassNames($middlewareClasses); foreach ($classNames as $middlewareClass) { try { $info[$middlewareClass->getFullyQualified()] = [ 'short_name' => $middlewareClass->getShortName(), 'namespace' => $middlewareClass->getNamespace(), 'exists' => $middlewareClass->exists(), 'is_instantiable' => $this->reflectionProvider->isInstantiable($middlewareClass), 'is_critical' => $this->isCriticalMiddleware($middlewareClass), 'middleware_dependencies' => $this->getMiddlewareDependencies($middlewareClass), 'service_dependencies' => $this->getServiceDependencies($middlewareClass), 'provides' => $this->getProvidedServices($middlewareClass), 'optional' => $this->getOptionalDependencies($middlewareClass), 'can_instantiate' => $this->canInstantiate($middlewareClass), ]; } catch (\Throwable $e) { $info[$middlewareClass->getFullyQualified()] = [ 'error' => $e->getMessage(), 'can_instantiate' => false, ]; } } return $info; } }