routes['GET'][$path] = $handler; } public function post(string $path, callable|array $handler): void { $this->routes['POST'][$path] = $handler; } public function dispatch(): void { $method = $_SERVER['REQUEST_METHOD']; $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); // Strip base path $basePath = parse_url(APP_URL, PHP_URL_PATH); if ($basePath && str_starts_with($uri, $basePath)) { $uri = substr($uri, strlen($basePath)); } $uri = '/' . ltrim($uri, '/'); if ($uri !== '/' && str_ends_with($uri, '/')) { $uri = rtrim($uri, '/'); } $routes = $this->routes[$method] ?? []; // Two-pass: static routes (no {param}) matched first, dynamic routes second. // This ensures /verifikator/warga/tambah is never swallowed by /verifikator/warga/{id}. $staticRoutes = array_filter($routes, fn($p) => !str_contains($p, '{'), ARRAY_FILTER_USE_KEY); $dynamicRoutes = array_filter($routes, fn($p) => str_contains($p, '{'), ARRAY_FILTER_USE_KEY); foreach ([$staticRoutes, $dynamicRoutes] as $group) { foreach ($group as $pattern => $handler) { $regex = $this->buildRegex($pattern); if (preg_match($regex, $uri, $matches)) { array_shift($matches); $params = array_values($matches); if (is_array($handler)) { [$controllerName, $action] = $handler; $controller = new $controllerName(); $controller->$action(...$params); } else { $handler(...$params); } return; } } } // 404 http_response_code(404); require APP_ROOT . '/app/views/public/404.php'; } private function buildRegex(string $pattern): string { $pattern = preg_replace('/\{(\w+)\}/', '([^/]+)', $pattern); return '#^' . $pattern . '$#'; } }