Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug - Add DISCOVERY_SHOW_PROGRESS=true - Temporary changes for debugging InitializerProcessor fixes on production
This commit is contained in:
@@ -4,78 +4,132 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Framework\Core;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Discovery\ValueObjects\DiscoveredAttribute;
|
||||
use App\Framework\Router\CompiledPattern;
|
||||
use App\Framework\Router\CompiledRoutes;
|
||||
use App\Framework\Router\RouteData;
|
||||
use App\Framework\Router\ValueObjects\MethodParameter;
|
||||
use App\Framework\Router\ValueObjects\ParameterCollection;
|
||||
use App\Framework\Router\ValueObjects\SubdomainPattern;
|
||||
|
||||
final readonly class RouteCompiler implements AttributeCompiler
|
||||
final readonly class RouteCompiler
|
||||
{
|
||||
/** @var array<string, StaticRoute|DynamicRoute> */
|
||||
private array $named;
|
||||
|
||||
/**
|
||||
* @param array<int, array{method: string, path: string, controller: class-string, handler: string}> $routes
|
||||
* @return array<string, array{static: array<string, array>, dynamic: array<int, array{regex: string, params: array, handler: array}>}>
|
||||
* Compile routes directly from DiscoveredAttribute objects with subdomain support
|
||||
* @return array<string, array<string, array{static: array<string, StaticRoute>, dynamic: array<int, DynamicRoute>}>>
|
||||
*/
|
||||
public function compile(array $routes): array
|
||||
public function compile(DiscoveredAttribute ...$discoveredRoutes): array
|
||||
{
|
||||
$compiled = [];
|
||||
$named = [];
|
||||
|
||||
foreach ($routes as $route) {
|
||||
foreach ($discoveredRoutes as $discoveredAttribute) {
|
||||
// Create actual Route attribute instance
|
||||
$routeAttribute = $discoveredAttribute->createAttributeInstance();
|
||||
if (! $routeAttribute instanceof Route) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$method = is_string($route['http_method']) ? strtoupper($route['http_method']) : $route['http_method']->value;
|
||||
$path = $route['path'];
|
||||
$routeName = $route['name'] ?? '';
|
||||
// Extract route data directly from the Route attribute
|
||||
$method = strtoupper($routeAttribute->method->value);
|
||||
$path = $routeAttribute->path;
|
||||
$routeName = $routeAttribute->name;
|
||||
|
||||
$compiled[$method] ??= ['static' => [], 'dynamic' => []];
|
||||
// Process subdomain patterns
|
||||
$subdomainPatterns = SubdomainPattern::fromInput($routeAttribute->subdomain);
|
||||
|
||||
if (! str_contains($path, '{')) {
|
||||
// Statische Route
|
||||
$staticRoute = new StaticRoute(
|
||||
$route['class'],
|
||||
$route['method'],
|
||||
$route['parameters'] ?? [],
|
||||
$routeName,
|
||||
$path,
|
||||
$route['attributes']
|
||||
);
|
||||
// If no subdomains specified, use default
|
||||
if (empty($subdomainPatterns)) {
|
||||
$subdomainPatterns = [new SubdomainPattern('')];
|
||||
}
|
||||
|
||||
$compiled[$method]['static'][$path] = $staticRoute;
|
||||
foreach ($subdomainPatterns as $subdomainPattern) {
|
||||
$subdomainKey = $subdomainPattern->getCompilationKey();
|
||||
|
||||
if($routeName) {
|
||||
$named[$routeName] = $staticRoute;
|
||||
}
|
||||
$compiled[$method] ??= [];
|
||||
$compiled[$method][$subdomainKey] ??= ['static' => [], 'dynamic' => []];
|
||||
|
||||
} else {
|
||||
// Dynamische Route
|
||||
$paramNames = [];
|
||||
$regex = $this->convertPathToRegex($path, $paramNames);
|
||||
if (! str_contains($path, '{')) {
|
||||
// Static route
|
||||
$parameterCollection = $this->createParameterCollection($discoveredAttribute->additionalData['parameters'] ?? []);
|
||||
$staticRoute = new StaticRoute(
|
||||
controller : $discoveredAttribute->className->getFullyQualified(),
|
||||
action : $discoveredAttribute->methodName?->toString() ?? '',
|
||||
parameters : $discoveredAttribute->additionalData['parameters'] ?? [],
|
||||
name : $routeName ?? '',
|
||||
path : $path,
|
||||
attributes : $discoveredAttribute->additionalData['attributes'] ?? [],
|
||||
parameterCollection: $parameterCollection
|
||||
);
|
||||
|
||||
$dynamicRoute = new DynamicRoute(
|
||||
$regex,
|
||||
$paramNames,
|
||||
$route['class'],
|
||||
$route['method'],
|
||||
$route['parameters'],
|
||||
[],
|
||||
$routeName,
|
||||
$path,
|
||||
$route['attributes']
|
||||
);
|
||||
$compiled[$method][$subdomainKey]['static'][$path] = $staticRoute;
|
||||
|
||||
$compiled[$method]['dynamic'][] = $dynamicRoute;
|
||||
if ($routeName) {
|
||||
$named[$routeName] = $staticRoute;
|
||||
}
|
||||
} else {
|
||||
// Dynamic route
|
||||
$paramNames = [];
|
||||
$regex = $this->convertPathToRegex($path, $paramNames);
|
||||
$parameterCollection = $this->createParameterCollection($discoveredAttribute->additionalData['parameters'] ?? []);
|
||||
|
||||
if($routeName) {
|
||||
$named[$routeName] = $dynamicRoute;
|
||||
$dynamicRoute = new DynamicRoute(
|
||||
regex : $regex,
|
||||
paramNames : $paramNames,
|
||||
controller : $discoveredAttribute->className->getFullyQualified(),
|
||||
action : $discoveredAttribute->methodName?->toString() ?? '',
|
||||
parameters : $discoveredAttribute->additionalData['parameters'] ?? [],
|
||||
paramValues : [],
|
||||
name : $routeName ?? '',
|
||||
path : $path,
|
||||
attributes : $discoveredAttribute->additionalData['attributes'] ?? [],
|
||||
parameterCollection: $parameterCollection
|
||||
);
|
||||
|
||||
$compiled[$method][$subdomainKey]['dynamic'][] = $dynamicRoute;
|
||||
|
||||
if ($routeName) {
|
||||
$named[$routeName] = $dynamicRoute;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!isset($this->named)) {
|
||||
if (! isset($this->named)) {
|
||||
$this->named = $named;
|
||||
}
|
||||
|
||||
return $compiled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert legacy parameters array to ParameterCollection
|
||||
*/
|
||||
private function createParameterCollection(array $parameters): ParameterCollection
|
||||
{
|
||||
$methodParameters = [];
|
||||
|
||||
foreach ($parameters as $param) {
|
||||
$methodParameters[] = new MethodParameter(
|
||||
name: $param['name'],
|
||||
type: $param['type'],
|
||||
isBuiltin: $param['isBuiltin'],
|
||||
hasDefault: $param['hasDefault'],
|
||||
default: $param['default'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
return new ParameterCollection(...$methodParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{method: string, path: string, controller: class-string, handler: string}> $routes
|
||||
* @return array<string, StaticRoute|DynamicRoute>
|
||||
*/
|
||||
public function compileNamedRoutes(array $routes): array
|
||||
{
|
||||
return $this->named;
|
||||
@@ -110,58 +164,153 @@ final readonly class RouteCompiler implements AttributeCompiler
|
||||
return Route::class;
|
||||
}
|
||||
|
||||
public function compileOptimized(array $routes): CompiledRoutes
|
||||
/**
|
||||
* Compile optimized routes directly from DiscoveredAttribute objects with subdomain support
|
||||
*/
|
||||
public function compileOptimized(DiscoveredAttribute ...$discoveredRoutes): CompiledRoutes
|
||||
{
|
||||
$compiled = $this->compile($routes);
|
||||
$compiled = $this->compile(...$discoveredRoutes);
|
||||
$optimizedStatic = [];
|
||||
$optimizedDynamic = [];
|
||||
|
||||
foreach($compiled as $method => $routes) {
|
||||
$optimizedStatic[$method] = $routes['static'];
|
||||
foreach ($compiled as $method => $subdomainRoutes) {
|
||||
foreach ($subdomainRoutes as $subdomainKey => $routes) {
|
||||
$optimizedStatic[$method][$subdomainKey] = $routes['static'];
|
||||
|
||||
if(!empty($routes['dynamic'])) {
|
||||
$optimizedDynamic[$method] = $this->createCompiledPattern($routes['dynamic']);
|
||||
if (! empty($routes['dynamic'])) {
|
||||
$optimizedDynamic[$method][$subdomainKey] = $this->createCompiledPattern($routes['dynamic']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CompiledRoutes($optimizedStatic, $optimizedDynamic, $this->named);
|
||||
}
|
||||
|
||||
private function createCompiledPattern(mixed $dynamicRoutes): CompiledPattern
|
||||
/**
|
||||
* Creates optimized compiled patterns with route prioritization
|
||||
* @param array<int, DynamicRoute> $dynamicRoutes
|
||||
*/
|
||||
private function createCompiledPattern(array $dynamicRoutes): CompiledPattern
|
||||
{
|
||||
$patterns = [];
|
||||
// Sort routes by priority (specific patterns first, then by path complexity)
|
||||
$prioritizedRoutes = $this->prioritizeRoutes($dynamicRoutes);
|
||||
|
||||
// Group routes into batches for smaller regex patterns
|
||||
$routeBatches = $this->groupRoutesByComplexity($prioritizedRoutes);
|
||||
|
||||
$compiledBatches = [];
|
||||
/** @var array<int, RouteData> $routeData */
|
||||
$routeData = [];
|
||||
$currentIndex = 1; // Nach Full Match (Index 0)
|
||||
$globalIndex = 0;
|
||||
|
||||
foreach($dynamicRoutes as $index => $route) {
|
||||
$pattern = $this->stripAnchors($route->regex);
|
||||
$patterns[] = "({$pattern})";
|
||||
foreach ($routeBatches as $batchName => $batch) {
|
||||
$patterns = [];
|
||||
$currentIndex = 1; // After full match (Index 0)
|
||||
|
||||
// Route-Gruppe Index merken
|
||||
$routeGroupIndex = $currentIndex++;
|
||||
foreach ($batch as $route) {
|
||||
$pattern = $this->stripAnchors($route->regex);
|
||||
$patterns[] = "({$pattern})";
|
||||
|
||||
// Parameter-Mapping KORREKT berechnen
|
||||
$paramMap = [];
|
||||
foreach($route->paramNames as $paramName) {
|
||||
$paramMap[$paramName] = $currentIndex++;
|
||||
// Route-Gruppe Index merken
|
||||
$routeGroupIndex = $currentIndex++;
|
||||
|
||||
// Parameter-Mapping berechnen
|
||||
$paramMap = [];
|
||||
foreach ($route->paramNames as $paramName) {
|
||||
$paramMap[$paramName] = $currentIndex++;
|
||||
}
|
||||
|
||||
$routeData[$globalIndex] = new RouteData(
|
||||
route: $route,
|
||||
paramMap: $paramMap,
|
||||
routeGroupIndex: $routeGroupIndex,
|
||||
batch: $batchName,
|
||||
regex: '~^' . $this->stripAnchors($route->regex) . '$~' // Pre-compiled individual regex
|
||||
);
|
||||
|
||||
$globalIndex++;
|
||||
}
|
||||
|
||||
$routeData[$index] = [
|
||||
'route' => $route,
|
||||
'paramMap' => $paramMap,
|
||||
'routeGroupIndex' => $routeGroupIndex
|
||||
$compiledBatches[$batchName] = [
|
||||
'regex' => '~^(?:' . implode('|', $patterns) . ')$~',
|
||||
'routes' => array_slice($routeData, $globalIndex - count($batch), count($batch), true),
|
||||
];
|
||||
}
|
||||
|
||||
$combinedRegex = '~^(?:' . implode('|', $patterns) . ')$~';
|
||||
return new CompiledPattern($combinedRegex, $routeData);
|
||||
// Use the first batch's regex for backward compatibility, but store all batches
|
||||
$primaryRegex = ! empty($compiledBatches) ? reset($compiledBatches)['regex'] : '~^$~';
|
||||
|
||||
return new CompiledPattern($primaryRegex, $routeData, $compiledBatches);
|
||||
}
|
||||
|
||||
private function stripAnchors($regex): string
|
||||
/**
|
||||
* Prioritize routes by specificity and expected frequency
|
||||
* @param array<DynamicRoute> $routes
|
||||
* @return array<DynamicRoute>
|
||||
*/
|
||||
private function prioritizeRoutes(array $routes): array
|
||||
{
|
||||
usort($routes, function ($a, $b) {
|
||||
// 1. API routes first (typically more frequent)
|
||||
$aIsApi = str_starts_with($a->path, '/api/');
|
||||
$bIsApi = str_starts_with($b->path, '/api/');
|
||||
if ($aIsApi !== $bIsApi) {
|
||||
return $bIsApi <=> $aIsApi; // API routes first
|
||||
}
|
||||
|
||||
// 2. Routes with fewer parameters first (more specific)
|
||||
$aParamCount = count($a->paramNames);
|
||||
$bParamCount = count($b->paramNames);
|
||||
if ($aParamCount !== $bParamCount) {
|
||||
return $aParamCount <=> $bParamCount;
|
||||
}
|
||||
|
||||
// 3. Shorter paths first (typically more specific)
|
||||
$aLength = strlen($a->path);
|
||||
$bLength = strlen($b->path);
|
||||
|
||||
return $aLength <=> $bLength;
|
||||
});
|
||||
|
||||
return $routes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group routes by complexity for optimized batch processing
|
||||
* @param array<DynamicRoute> $routes
|
||||
* @return array<string, array<DynamicRoute>>
|
||||
*/
|
||||
private function groupRoutesByComplexity(array $routes): array
|
||||
{
|
||||
$groups = [
|
||||
'simple' => [], // /api/{id}, /user/{id} - 1 parameter
|
||||
'medium' => [], // /api/{type}/{id} - 2 parameters
|
||||
'complex' => [], // 3+ parameters or wildcards
|
||||
];
|
||||
|
||||
foreach ($routes as $route) {
|
||||
$paramCount = count($route->paramNames);
|
||||
$hasWildcard = str_contains($route->path, '{*}');
|
||||
|
||||
if ($hasWildcard || $paramCount >= 3) {
|
||||
$groups['complex'][] = $route;
|
||||
} elseif ($paramCount === 2) {
|
||||
$groups['medium'][] = $route;
|
||||
} else {
|
||||
$groups['simple'][] = $route;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove empty groups
|
||||
return array_filter($groups, fn ($group) => ! empty($group));
|
||||
}
|
||||
|
||||
private function stripAnchors(string $regex): string
|
||||
{
|
||||
if (preg_match('/^~\^(.+)\$~$/', $regex, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return $regex;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user