*/ public function analyze(): array { $issues = []; $staticRoutes = $this->compiledRoutes->getStaticRoutes(); $namedRoutes = $this->compiledRoutes->getAllNamedRoutes(); $totalStatic = 0; // Track seen routes for potential duplicates per method+subdomain+path $seen = []; foreach ($staticRoutes as $method => $subdomains) { foreach ($subdomains as $subdomain => $paths) { foreach ($paths as $path => $route) { $totalStatic++; $key = "{$method}|{$subdomain}|{$path}"; $seen[$key] = ($seen[$key] ?? 0) + 1; $routeName = $route->name ?? null; // Controller existence $controller = $route->controller ?? null; $action = $route->action ?? null; if (! is_string($controller) || $controller === '' || ! class_exists($controller)) { $issues[] = $this->issue('controller_missing', 'error', $method, $subdomain, $path, $routeName, "Controller class not found or invalid: " . var_export($controller, true)); continue; // skip further checks for this route } // Action existence and visibility if (! is_string($action) || $action === '') { $issues[] = $this->issue('action_missing', 'error', $method, $subdomain, $path, $routeName, 'Action method not defined or invalid'); } else { $refClass = new ReflectionClass($controller); if (! $refClass->hasMethod($action)) { $issues[] = $this->issue('action_missing', 'error', $method, $subdomain, $path, $routeName, "Action method '{$action}' not found in {$controller}"); } else { $refMethod = $refClass->getMethod($action); if (! $refMethod->isPublic()) { $issues[] = $this->issue('action_not_public', 'warning', $method, $subdomain, $path, $routeName, "Action method '{$action}' is not public"); } // Parameter consistency check (placeholders vs method signature) $this->checkParameterConsistency($issues, $method, $subdomain, $path, $routeName, $route, $refMethod); } } } } } // Duplicate path checks (should normally be prevented by map keys, but guard anyway) foreach ($seen as $k => $count) { if ($count > 1) { [$m, $sub, $p] = explode('|', $k, 3); $issues[] = $this->issue('duplicate_route', 'error', $m, $sub, $p, null, "Duplicate route detected for {$m} {$sub} {$p}"); } } // Named routes basic validation: ensure name -> route is consistent $namedIssues = $this->validateNamedRoutes($namedRoutes); array_push($issues, ...$namedIssues); $summary = [ 'total_static_routes' => $totalStatic, 'total_named_routes' => count($namedRoutes), 'issue_count' => count($issues), ]; return [ 'summary' => $summary, 'issues' => $issues, 'stats' => $this->compiledRoutes->getStats(), ]; } /** * Check that route parameters in path are consistent with action method signature */ private function checkParameterConsistency(array &$issues, string $method, string $subdomain, string $path, ?string $routeName, object $route, ReflectionMethod $refMethod): void { $pathParams = $this->extractPathParams($path); // Try to read expected parameters from route definition; otherwise from reflection $expected = []; if (isset($route->parameters) && is_array($route->parameters)) { // If associative, use keys; if list, use values $keys = array_keys($route->parameters); $expected = array_values(array_filter( count($keys) !== count($route->parameters) ? $route->parameters : $keys, fn ($v) => is_string($v) && $v !== '' )); } else { $expected = array_map( static fn (\ReflectionParameter $p) => $p->getName(), $refMethod->getParameters() ); } // Normalize unique sets $pathSet = array_values(array_unique($pathParams)); $expectedSet = array_values(array_unique($expected)); // Missing placeholders in path for expected parameters $missingInPath = array_values(array_diff($expectedSet, $pathSet)); if (! empty($missingInPath)) { $issues[] = $this->issue( 'param_mismatch', 'warning', $method, $subdomain, $path, $routeName, 'Expected parameters not present in path: ' . implode(', ', $missingInPath) ); } // Extra placeholders not expected by the action $extraInPath = array_values(array_diff($pathSet, $expectedSet)); if (! empty($extraInPath)) { $issues[] = $this->issue( 'param_mismatch', 'warning', $method, $subdomain, $path, $routeName, 'Path has placeholders not expected by action: ' . implode(', ', $extraInPath) ); } } /** * Validate named routes (basic structural checks) * @param array $namedRoutes * @return array> */ private function validateNamedRoutes(array $namedRoutes): array { $issues = []; foreach ($namedRoutes as $name => $route) { // Minimal: ensure a path exists $path = $route->path ?? null; if (! is_string($path) || $path === '') { $issues[] = [ 'type' => 'invalid_named_route', 'severity' => 'error', 'route_name' => $name, 'message' => 'Named route has no valid path', ]; } } return $issues; } /** * Extract placeholders from route path like /users/{id}/posts/{slug} * @return array */ private function extractPathParams(string $path): array { $matches = []; preg_match_all('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', $path, $matches); /** @var array $params */ $params = $matches[1] ?? []; return $params; } /** * Build a standardized issue array * @return array */ private function issue(string $type, string $severity, string $method, string $subdomain, string $path, ?string $name, string $message): array { return [ 'type' => $type, 'severity' => $severity, 'route' => [ 'method' => $method, 'subdomain' => $subdomain, 'path' => $path, 'name' => $name, ], 'message' => $message, ]; } }