// Force opcache refresh with timestamp declare(strict_types=1); namespace App\Routing; final class Router { private array $routes = []; private string $method; private string $uri; public function __construct(string $method, string $uri) { $this->method = strtoupper($method); $parsed = parse_url($uri, PHP_URL_PATH); $this->uri = $parsed ?? '/'; // Normalize URI: remove trailing slashes (except root), ensure leading slash $this->uri = rtrim($this->uri, '/') ?: '/'; if ($this->uri !== '/' && !str_starts_with($this->uri, '/')) { $this->uri = '/' . $this->uri; } // Debug: Log the original URI and parsed URI error_log("Router constructor - Original URI: $uri"); error_log("Router constructor - Parsed URI path: " . $parsed); error_log("Router constructor - Normalized URI: " . $this->uri); error_log("Router constructor - REQUEST_URI: " . ($_SERVER['REQUEST_URI'] ?? 'not set')); error_log("Router constructor - SCRIPT_NAME: " . ($_SERVER['SCRIPT_NAME'] ?? 'not set')); } public function get(string $path, callable $handler): void { $this->addRoute('GET', $path, $handler); } public function post(string $path, callable $handler): void { $this->addRoute('POST', $path, $handler); } public function put(string $path, callable $handler): void { $this->addRoute('PUT', $path, $handler); } public function delete(string $path, callable $handler): void { $this->addRoute('DELETE', $path, $handler); } private function addRoute(string $method, string $path, callable $handler): void { // Normalize route path: remove trailing slashes (except root), ensure leading slash $normalizedPath = rtrim($path, '/') ?: '/'; if ($normalizedPath !== '/' && !str_starts_with($normalizedPath, '/')) { $normalizedPath = '/' . $normalizedPath; } // Debug logging for route registration error_log("Registering route: $method $path (normalized: $normalizedPath)"); if (!isset($this->routes[$method])) { $this->routes[$method] = []; } $this->routes[$method][$normalizedPath] = $handler; error_log("Route registered successfully. Total routes for $method: " . count($this->routes[$method])); } public function dispatch(): void { // IMMEDIATE OUTPUT to verify this code is running (bypasses all caching) // This will appear in response even if opcache is caching if (strpos($this->uri, 'import-all') !== false || strpos($_SERVER['REQUEST_URI'] ?? '', 'import-all') !== false) { // For import-all specifically, output debug immediately header('X-Router-Version: 2026-01-02-13:58-DEBUG-V4'); header('X-Router-URI: ' . $this->uri); header('X-Router-Method: ' . $this->method); } // Write to a debug file directly to bypass error_log issues // Try multiple possible paths $possiblePaths = [ dirname(__DIR__, 2) . '/public/router-debug.log', __DIR__ . '/../../public/router-debug.log', '/home/drjoekh/public_html/app.drjoekhoury.com/router-debug.log', dirname($_SERVER['SCRIPT_FILENAME'] ?? __DIR__) . '/router-debug.log', ]; $debugFile = $possiblePaths[0]; // Use first one, but try to write to all foreach ($possiblePaths as $path) { @file_put_contents($path, date('Y-m-d H:i:s') . " - Router::dispatch() START - Method: {$this->method}, URI: {$this->uri}\n", FILE_APPEND); } $debugContent = date('Y-m-d H:i:s') . " - Router::dispatch() START\n"; $debugContent .= "Method: " . $this->method . "\n"; $debugContent .= "URI: " . $this->uri . "\n"; $debugContent .= "REQUEST_URI: " . ($_SERVER['REQUEST_URI'] ?? 'not set') . "\n"; @file_put_contents($debugFile, $debugContent, FILE_APPEND); // Debug logging - also output to response for immediate visibility $debugOutput = []; $debugOutput[] = '=== Router::dispatch() START ==='; $debugOutput[] = 'Method: ' . $this->method; $debugOutput[] = 'URI: ' . $this->uri; $debugOutput[] = 'REQUEST_URI: ' . ($_SERVER['REQUEST_URI'] ?? 'not set'); foreach ($debugOutput as $line) { error_log($line); } $routes = $this->routes[$this->method] ?? []; $routeKeys = array_keys($routes); error_log('Total routes for ' . $this->method . ': ' . count($routeKeys)); error_log('Available routes: ' . json_encode($routeKeys)); // Write to debug file $debugContent = "Total routes: " . count($routeKeys) . "\n"; $debugContent .= "Routes: " . json_encode($routeKeys) . "\n"; @file_put_contents($debugFile, $debugContent, FILE_APPEND); // Check specifically for import-all route if (isset($routes['/api/admin/import-all'])) { $msg = '✅ /api/admin/import-all route EXISTS in routes array'; error_log($msg); @file_put_contents($debugFile, $msg . "\n", FILE_APPEND); } else { $msg = '❌ /api/admin/import-all route NOT FOUND in routes array'; error_log($msg); error_log('Looking for exact match in: ' . json_encode($routeKeys)); @file_put_contents($debugFile, $msg . "\n", FILE_APPEND); @file_put_contents($debugFile, "Looking in: " . json_encode($routeKeys) . "\n", FILE_APPEND); } // First, try exact match (faster and more reliable) // Check for import-all route specifically first $importAllRoute = '/api/admin/import-all'; if ($this->uri === $importAllRoute && isset($routes[$importAllRoute])) { error_log('✅ IMPORT-ALL ROUTE EXACT MATCH: ' . $this->uri); $result = $routes[$importAllRoute]([]); if ($result !== null) { echo $result; } return; } if (isset($routes[$this->uri])) { error_log('✅ EXACT ROUTE MATCHED: ' . $this->uri); $result = $routes[$this->uri]([]); if ($result !== null) { echo $result; } return; } // Try matching without query string $uriWithoutQuery = strtok($this->uri, '?'); if ($uriWithoutQuery !== $this->uri && isset($routes[$uriWithoutQuery])) { error_log('✅ EXACT ROUTE MATCHED (without query): ' . $uriWithoutQuery); $result = $routes[$uriWithoutQuery]([]); if ($result !== null) { echo $result; } return; } error_log('❌ No exact match for: ' . $this->uri); error_log('❌ Also tried without query: ' . $uriWithoutQuery); error_log('❌ Checking if import-all route exists: ' . (isset($routes[$importAllRoute]) ? 'YES' : 'NO')); // Check if similar routes exist foreach ($routeKeys as $route) { if (strpos($route, 'import') !== false || strpos($route, 'admin') !== false) { error_log('Similar route found: ' . $route); } } // Then try pattern matching for routes with parameters foreach ($routes as $path => $handler) { // Skip if already checked as exact match if ($path === $this->uri) { continue; } $pattern = '@^' . preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $path) . '$@'; error_log('Checking route: ' . $path . ' against URI: ' . $this->uri . ' with pattern: ' . $pattern); if (preg_match($pattern, $this->uri, $matches)) { error_log('Route matched: ' . $path); $params = array_filter($matches, static fn($key) => !is_int($key), ARRAY_FILTER_USE_KEY); $result = $handler($params); if ($result !== null) { echo $result; } return; } } $noMatchMsg = 'No route matched for ' . $this->method . ' ' . $this->uri; error_log($noMatchMsg); @file_put_contents($debugFile, $noMatchMsg . "\n", FILE_APPEND); $debugInfo = [ 'REQUEST_URI' => $_SERVER['REQUEST_URI'] ?? 'not set', 'SCRIPT_NAME' => $_SERVER['SCRIPT_NAME'] ?? 'not set', 'PHP_SELF' => $_SERVER['PHP_SELF'] ?? 'not set', 'PATH_INFO' => $_SERVER['PATH_INFO'] ?? 'not set', 'routes' => $routeKeys, ]; error_log('DEBUG INFO:'); foreach ($debugInfo as $key => $value) { $line = " $key: " . (is_array($value) ? json_encode($value) : $value); error_log($line); foreach ($possiblePaths as $path) { @file_put_contents($path, $line . "\n", FILE_APPEND); } } http_response_code(404); header('Content-Type: application/json'); // Always include debug info in response for troubleshooting // Version marker to verify which Router.php is running $routerVersion = '2026-01-02-13:52-DEBUG-V3'; try { $debugInfo = [ 'status' => 'error', 'message' => 'Not found', 'router_version' => $routerVersion, 'debug' => [ 'method' => $this->method, 'uri' => $this->uri, 'uri_normalized' => $this->uri, 'request_uri' => $_SERVER['REQUEST_URI'] ?? null, 'script_name' => $_SERVER['SCRIPT_NAME'] ?? null, 'php_self' => $_SERVER['PHP_SELF'] ?? null, 'path_info' => $_SERVER['PATH_INFO'] ?? null, 'available_routes' => $routeKeys, 'route_count' => count($routeKeys), 'import_all_exists' => isset($routes['/api/admin/import-all']), 'all_post_routes' => $routeKeys, ] ]; $json = json_encode($debugInfo, JSON_PRETTY_PRINT); if ($json === false) { throw new \RuntimeException('JSON encode failed: ' . json_last_error_msg()); } echo $json; } catch (\Throwable $e) { // Fallback if JSON encoding fails echo json_encode([ 'status' => 'error', 'message' => 'Not found', 'router_version' => $routerVersion, 'error' => $e->getMessage(), 'uri' => $this->uri, 'trace' => $e->getTraceAsString(), ]); } } }