230 lines
6.3 KiB
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);
|
|
}
|
|
}
|
|
} |