Files
michaelschiemer/src/Framework/Router/RouteInspector.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00

213 lines
7.7 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Framework\Router;
use ReflectionClass;
use ReflectionMethod;
/**
* Performs sanity checks on compiled routes (controller/action presence, parameter consistency, etc.)
*/
final readonly class RouteInspector
{
public function __construct(
private CompiledRoutes $compiledRoutes
) {
}
/**
* Analyze compiled routes and return structured diagnostics
* @return array<string, mixed>
*/
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<string, object> $namedRoutes
* @return array<int, array<string, mixed>>
*/
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<int, string>
*/
private function extractPathParams(string $path): array
{
$matches = [];
preg_match_all('/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/', $path, $matches);
/** @var array<int, string> $params */
$params = $matches[1] ?? [];
return $params;
}
/**
* Build a standardized issue array
* @return array<string, mixed>
*/
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,
];
}
}