subcon.town/libs/zap/Routing/Router.php

230 lines
6.3 KiB
PHP

<?php
namespace Subcon\Zap\Routing;
use Subcon\Zap\Application;
use Subcon\Zap\Http\Response;
class Router
{
/**
* @var Application
*/
private Application $app;
/**
* @var array
*/
private array $routes;
/**
* @var string
*/
private string $requestUri;
/**
* @var string
*/
private string $requestMethod;
/**
* @var array
*/
private array $requestHeaders;
/**
* @param Application $app
*/
public function __construct(Application $app)
{
$this->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);
}
}
}