app = $app; //Load routes from cache, or fall back to loading from file if($this->loadFromCache(realpath($app->getBasePath('cache')))) { return; } $this->loadFromRouteFile(realpath($app->getBasePath('routes'))); } /** * @return void */ public function run(): void { //Parse request $this->requestUri = $this->getRequestUri(); $this->requestMethod = $this->getRequestMethod(); $this->requestHeaders ??= $this->getRequestHeaders(); //Find the first matching route. Only one route will ever be executed. $route = $this->findRoute($this->requestUri, $this->requestMethod); //If route was found, run it. Otherwise return a 404 response. if($route) { $response = $this->handleRoute($route); } else { $response = $this->handleNotFoundResponse(); } //Send response. if (!is_a($response, Response::class)) { $response = new Response((string) $response); } $response->send(); //If the request was a HEAD request, empty the output buffer. if ($_SERVER['REQUEST_METHOD'] == 'HEAD') { ob_end_clean(); } } public function buildCache() { } /** * Define the current relative URI. * * @return string */ protected function getRequestUri(): string { $uri = rawurldecode($_SERVER['REQUEST_URI']); // Don't take query params into account on the URL if (strstr($uri, '?')) { $uri = substr($uri, 0, strpos($uri, '?')); } // Remove trailing slash + enforce a slash at the start return '/' . trim($uri, '/'); } /** * Get all request headers. * * @return array The request headers */ protected function getRequestHeaders(): array { if(!empty($this->requestHeaders)) return $this->requestHeaders; $headers = []; // If getallheaders() is available, use that if (function_exists('getallheaders')) { $headers = getallheaders(); // getallheaders() can return false if something went wrong if ($headers !== false) { $this->requestHeaders = $headers; return $headers; } } // Method getallheaders() not available or went wrong: manually extract them foreach ($_SERVER as $name => $value) { if ((substr($name, 0, 5) == 'HTTP_') || ($name == 'CONTENT_TYPE') || ($name == 'CONTENT_LENGTH')) { $headers[str_replace([' ', 'Http'], ['-', 'HTTP'], ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))))] = $value; } } $this->requestHeaders = $headers; return $headers; } /** * Get the request method used, taking overrides into account. * * @return string The request method to handle */ protected function getRequestMethod(): string { // Take the method as found in $_SERVER $method = $_SERVER['REQUEST_METHOD']; // If it's a HEAD request override it to being GET and prevent any output, as per HTTP Specification // @url http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4 if ($_SERVER['REQUEST_METHOD'] == 'HEAD') { ob_start(); $method = 'GET'; } // If it's a POST request, check for a method override header elseif ($_SERVER['REQUEST_METHOD'] == 'POST') { $headers = $this->getRequestHeaders(); if (isset($headers['X-HTTP-Method-Override']) && in_array($headers['X-HTTP-Method-Override'], array('PUT', 'DELETE', 'PATCH'))) { $method = $headers['X-HTTP-Method-Override']; } } return $method; } protected function loadFromCache(string $cachePath): bool { if(!file_exists($cachePath) || !file_exists($cachePath . DIRECTORY_SEPARATOR . 'route-cache.php')) { return false; } $this->routes = require $cachePath . DIRECTORY_SEPARATOR . 'route-cache.php'; return true; } protected function loadFromRouteFile(string $routePath): void { if(!file_exists($routePath) || !file_exists($routePath . DIRECTORY_SEPARATOR . 'routes.php')) { throw new \RuntimeException('Unable to initialize routes: route file not found.'); } $repository = new Repository(); $routes = require $routePath . DIRECTORY_SEPARATOR . 'routes.php'; if(!is_callable($routes)) { throw new \RuntimeException('Unable to initialize routes: route file does not contain callable function.'); } //Initialize routes call_user_func($routes, $repository); $this->routes = $repository->getRoutes(); } protected function findRoute(string $path, string $method): ?Route { $method = strtoupper($method); if(key_exists($method, $this->routes)) { foreach($this->routes[$method] as $route) { if($route->match($path, $method)) { return $route; } } } return null; } protected function handleRoute(Route $route) { //Todo: run route middleware and route handler. return $route->run(); } protected function handleNotFoundResponse() { //Do we have an error template? $template = $this->app->getViewPath('errors/404.html'); if(file_exists($template)) { ob_start(); require $template; $response = ob_get_clean(); return new Response($response, 404); } else { return new Response('Whoops! Could not find that file.', 404); } } }