- 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
661 lines
23 KiB
PHP
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;
|
|
}
|
|
}
|