basePath = rtrim($basePath, '/'); $this->detectBasePath(); } /** * Auto-detect base path based on deployment configuration */ private function detectBasePath(): void { $config = $this->getConfig(); switch ($config['deployment_type']) { case 'subdomain': // For subdomain deployment (api.domain.com) $this->basePath = ''; break; case 'subfolder': // For subfolder deployment (domain.com/store/api) $this->basePath = rtrim($config['api_base_path'], '/'); break; case 'domain': // For dedicated domain (api-domain.com) $this->basePath = ''; break; default: // Default to subfolder if not specified $this->basePath = rtrim($config['api_base_path'], '/'); } } /** * Add global middleware that runs on all routes */ public function addGlobalMiddleware(string $middleware): void { $this->globalMiddleware[] = $middleware; } /** * Create a route group with shared attributes */ public function group(array $attributes, callable $callback): void { $previousPrefix = $this->currentGroupPrefix; $previousMiddleware = $this->currentGroupMiddleware; // Set group attributes $this->currentGroupPrefix = ($this->currentGroupPrefix ?? '').($attributes['prefix'] ?? ''); $this->currentGroupMiddleware = array_merge( $this->currentGroupMiddleware, $attributes['middleware'] ?? [] ); // Execute the group callback $callback($this); // Restore previous group state $this->currentGroupPrefix = $previousPrefix; $this->currentGroupMiddleware = $previousMiddleware; } /** * Add a route with specified method */ public function addRoute(string $method, string $path, $handler, array $middleware = []): void { // Apply group prefix $fullPath = ($this->currentGroupPrefix ?? '').$path; // Combine group middleware with route middleware $allMiddleware = array_merge( $this->globalMiddleware, $this->currentGroupMiddleware, $middleware ); $pattern = $this->convertToRegex($fullPath); $this->routes[] = [ 'method' => strtoupper($method), 'pattern' => $pattern, 'handler' => $handler, 'middleware' => $allMiddleware, 'path' => $fullPath, 'original_path' => $path ]; } /** * HTTP Method shortcuts */ public function get(string $path, $handler, array $middleware = []): void { $this->addRoute('GET', $path, $handler, $middleware); } /** * @param string $path * @param $handler * @param array $middleware */ public function post(string $path, $handler, array $middleware = []): void { $this->addRoute('POST', $path, $handler, $middleware); } /** * @param string $path * @param $handler * @param array $middleware */ public function put(string $path, $handler, array $middleware = []): void { $this->addRoute('PUT', $path, $handler, $middleware); } /** * @param string $path * @param $handler * @param array $middleware */ public function patch(string $path, $handler, array $middleware = []): void { $this->addRoute('PATCH', $path, $handler, $middleware); } /** * @param string $path * @param $handler * @param array $middleware */ public function delete(string $path, $handler, array $middleware = []): void { $this->addRoute('DELETE', $path, $handler, $middleware); } /** * @param string $path * @param $handler * @param array $middleware */ public function options(string $path, $handler, array $middleware = []): void { $this->addRoute('OPTIONS', $path, $handler, $middleware); } /** * Handle preflight OPTIONS requests for CORS */ public function handleCors(): void { if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { $config = $this->getConfig(); if ($config['cors_enabled']) { $origin = $_SERVER['HTTP_ORIGIN'] ?? ''; $allowedOrigins = $config['cors_origins']; if (in_array('*', $allowedOrigins) || in_array($origin, $allowedOrigins)) { header('Access-Control-Allow-Origin: '.($origin ?: '*')); header('Access-Control-Allow-Methods: '.implode(', ', $config['cors_methods'])); header('Access-Control-Allow-Headers: '.implode(', ', $config['cors_headers'])); if ($config['cors_credentials']) { header('Access-Control-Allow-Credentials: true'); } header('Access-Control-Max-Age: 86400'); // 24 hours } } http_response_code(200); exit; } } /** * Get request method with _method override support */ private function getRequestMethod(): string { $method = $_SERVER['REQUEST_METHOD']; // Support method override for POST requests only if ($method === 'POST' && isset($_POST['_method'])) { $overrideMethod = strtoupper($_POST['_method']); // Only allow safe method overrides to simulate REST methods if (in_array($overrideMethod, ['PUT', 'PATCH', 'DELETE'])) { return $overrideMethod; } } return $method; } /** * Main dispatch method */ public function dispatch(): void { try { // Handle CORS preflight requests $this->handleCors(); $method = $this->getRequestMethod(); $uri = $this->getCurrentUri(); // Find matching route $matchedRoute = $this->findRoute($method, $uri); if (!$matchedRoute) { $this->handleNotFound(); return; } // Execute middleware pipeline $this->executeMiddleware($matchedRoute['middleware']); // Execute route handler $this->executeHandler($matchedRoute); } catch (\Exception $e) { $this->handleException($e); } } /** * Get current URI with base path removed */ private function getCurrentUri(): string { $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); // Remove base path if present if ($this->basePath && strpos($uri, $this->basePath) === 0) { $uri = substr($uri, strlen($this->basePath)); } // Ensure URI starts with / return '/'.ltrim($uri, '/'); } /** * Find matching route */ private function findRoute(string $method, string $uri): ?array { foreach ($this->routes as $route) { if ($route['method'] === $method && preg_match($route['pattern'], $uri, $matches)) { // Remove full match from parameters array_shift($matches); $route['parameters'] = $matches; return $route; } } return null; } /** * Execute middleware pipeline */ private function executeMiddleware(array $middleware): void { foreach ($middleware as $middlewareClass) { if (class_exists($middlewareClass)) { $middlewareInstance = new $middlewareClass(); if (method_exists($middlewareInstance, 'handle')) { $result = $middlewareInstance->handle(); // If middleware returns false, stop execution if ($result === false) { return; } } } } } /** * Execute route handler */ private function executeHandler(array $route): void { $handler = $route['handler']; $parameters = $route['parameters'] ?? []; if (is_callable($handler)) { // Direct callable $result = call_user_func_array($handler, $parameters); } elseif (is_array($handler) && count($handler) === 2) { // Controller@method format [$controllerClass, $method] = $handler; if (class_exists($controllerClass)) { $controller = new $controllerClass(); if (method_exists($controller, $method)) { $result = call_user_func_array([$controller, $method], $parameters); } else { throw new \Exception("Method {$method} not found in {$controllerClass}"); } } else { throw new \Exception("Controller {$controllerClass} not found"); } } else { throw new \Exception("Invalid route handler"); } // Handle response $this->handleResponse($result); } /** * Handle response output */ private function handleResponse($result): void { if ($result === null) { return; } if (is_array($result) || is_object($result)) { header('Content-Type: application/json'); echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); } else { echo $result; } } /** * Convert route path to regex pattern */ private function convertToRegex(string $path): string { // Escape forward slashes $pattern = str_replace('/', '\/', $path); // Convert {param} to named capture groups $pattern = preg_replace('/\{([^}]+)\}/', '([^\/]+)', $pattern); // Convert {param?} to optional parameters $pattern = preg_replace('/\{([^}]+)\?\}/', '([^\/]*)', $pattern); return '/^'.$pattern.'$/'; } /** * Handle 404 Not Found */ private function handleNotFound(): void { http_response_code(404); header('Content-Type: application/json'); echo json_encode([ 'error' => 'Not Found', 'message' => 'The requested resource was not found', 'code' => 404 ]); } /** * Handle exceptions */ private function handleException(\Exception $e): void { $config = $this->getConfig(); http_response_code(500); header('Content-Type: application/json'); if ($config['debug']) { echo json_encode([ 'error' => 'Internal Server Error', 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'trace' => $e->getTrace() ], JSON_PRETTY_PRINT); } else { echo json_encode([ 'error' => 'Internal Server Error', 'message' => 'An unexpected error occurred', 'code' => 500 ]); } // Log the error error_log("Router Exception: ".$e->getMessage()." in ".$e->getFile().":".$e->getLine()); } /** * Get application configuration */ private function getConfig(): array { /** * @var mixed */ static $config = null; if ($config === null) { // Try store-specific config first, fallback to default $storeConfigFile = __DIR__.'/../../config/app-store.php'; $defaultConfigFile = __DIR__.'/../../config/app.php'; if (file_exists($storeConfigFile)) { $config = require $storeConfigFile; } elseif (file_exists($defaultConfigFile)) { $config = require $defaultConfigFile; } else { $config = []; } } return $config; } /** * Get all registered routes (for debugging) */ public function getRoutes(): array { return $this->routes; } /** * Generate URL for named route */ public function url(string $name, array $parameters = []): string { // This would be implemented if we add named routes // For now, return a basic URL construction $config = $this->getConfig(); $baseUrl = $config['app_url'].$this->basePath; return $baseUrl.$name; } }