Files
michaelschiemer/src/Framework/Http/MiddlewareDependencyResolver.php
Michael Schiemer 0ca382f80b refactor: add circular dependency detection and error handling in DI container
- Introduce `InitializerCycleException` for detailed cycle reporting
- Enhance `InitializerProcessor` fallback with explicit discovery order handling and logging
- Implement proactive cycle detection in `InitializerDependencyGraph`
- Improve `ClassName` and `MethodName` with `Stringable` support
2025-11-03 15:37:40 +01:00

661 lines
23 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Http;
use App\Framework\Core\ValueObjects\ClassName;
use App\Framework\Core\ValueObjects\MethodName;
use App\Framework\DI\Container;
use App\Framework\Logging\Logger;
use App\Framework\Reflection\ReflectionProvider;
/**
* Resolves middleware dependencies by analyzing constructor parameters
* and automatically sorting middlewares in the correct execution order.
*
* Enhanced with robustness features:
* - ClassName value objects for type safety
* - Fallback strategies for critical middlewares
* - Comprehensive error handling and logging
* - Protection for essential middlewares
*/
final readonly class MiddlewareDependencyResolver
{
/** @var array<string> 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<string> $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<string> $middlewareClasses
* @return array<ClassName>
*/
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<ClassName> $middlewareClasses
* @return array<string, array{dependencies: array<string>, services: array<string>, provides: array<string>, optional: array<string>, 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<string, array{dependencies: array<string>, services: array<string>, provides: array<string>, optional: array<string>, is_critical: bool}> $dependencyGraph
* @return array<string, array<string>>
*/
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<string>
*/
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<string>
*/
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<string>
*/
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<string>
*/
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<string, array<string>> $graph
* @return array<string>
*/
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<string, array<string>> $graph
* @param array<string, bool> $visited
* @param array<string, bool> $visiting
* @param array<string> $sorted
* @return array{visited: array<string, bool>, visiting: array<string, bool>, sorted: array<string>}
*/
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<string> $middlewareClasses
* @return array<string>
*/
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<object> $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<string> $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<string> $middlewareClasses
* @return array<string, mixed>
*/
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;
}
}