routes[] = [ 'method' => strtoupper($method), 'pattern' => $pattern, 'handler' => $handler, ]; } public function get(string $pattern, callable $handler): void { $this->add('GET', $pattern, $handler); } public function post(string $pattern, callable $handler): void { $this->add('POST', $pattern, $handler); } public function put(string $pattern, callable $handler): void { $this->add('PUT', $pattern, $handler); } public function delete(string $pattern, callable $handler): void { $this->add('DELETE', $pattern, $handler); } public function dispatch(): void { $method = strtoupper($_SERVER['REQUEST_METHOD'] ?? 'GET'); // Support method override via hidden _method field or X-HTTP-Method-Override header if ($method === 'POST') { $override = $_POST['_method'] ?? $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ?? null; if ($override) { $method = strtoupper($override); } } $uri = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH); $uri = '/' . trim($uri, '/'); if ($uri === '') $uri = '/'; if (in_array($method, ['POST', 'PUT', 'DELETE', 'PATCH'], true)) { $token = $_POST['csrf_token'] ?? $_SERVER['HTTP_X_CSRF_TOKEN'] ?? ''; Auth::validateCsrfToken($token); } foreach ($this->routes as $route) { if ($route['method'] !== $method) continue; $params = $this->match($route['pattern'], $uri); if ($params !== null) { ($route['handler'])($params); return; } } http_response_code(404); $view = new View(); $view->render('layout/404.php', ['uri' => $uri]); } private function match(string $pattern, string $uri): ?array { // Convert route pattern to regex: {id} → named capture $regex = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $pattern); $regex = '#^' . $regex . '$#'; if (preg_match($regex, $uri, $matches)) { return array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY); } return null; } }