refactor: reorganize project structure for better maintainability
- Move 45 debug/test files from root to organized scripts/ directories - Secure public/ directory by removing debug files (security improvement) - Create structured scripts organization: • scripts/debug/ (20 files) - Framework debugging tools • scripts/test/ (18 files) - Test and validation scripts • scripts/maintenance/ (5 files) - Maintenance utilities • scripts/dev/ (2 files) - Development tools Security improvements: - Removed all debug/test files from public/ directory - Only production files remain: index.php, health.php Root directory cleanup: - Reduced from 47 to 2 PHP files in root - Only essential production files: console.php, worker.php This improves: ✅ Security (no debug code in public/) ✅ Organization (clear separation of concerns) ✅ Maintainability (easy to find and manage scripts) ✅ Professional structure (clean root directory)
This commit is contained in:
211
src/Framework/Router/RouteInspector.php
Normal file
211
src/Framework/Router/RouteInspector.php
Normal file
@@ -0,0 +1,211 @@
|
||||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user