zappy-base/src/Routing/Router.php
2024-05-19 15:45:12 +02:00

179 lines
5.2 KiB
PHP

<?php
namespace Musoka\Zap\Routing;
use Musoka\Zap\Application;
use Symfony\Component\HttpFoundation\HeaderBag;
use Musoka\Zap\Http\Request;
class Router implements RouterInterface
{
private Request $request;
protected RouteRepository $repository;
private ?Route $currentRoute;
/**
* @var array
*/
private array $routes;
/**
* @var string
*/
private string $requestUri;
/**
* @var string
*/
private string $requestMethod;
/**
* @var array
*/
private HeaderBag $requestHeaders;
/**
* @param Application $app
*/
public function __construct(Request $request, string $routeDirPath, string $cacheDirPath)
{
$this->request = $request;
$this->currentRoute = null;
//Load routes from cache, or fall back to loading from file
if(!$this->loadFromCache($cacheDirPath)) {
$this->loadFromRouteFile($routeDirPath);
}
}
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.');
}
$this->repository = new RouteRepository();
$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, $this->repository);
$this->routes = $this->repository->collectRoutes();
}
protected function readRequestData()
{
$uri = $this->request->getPathInfo();
// Remove trailing slash + enforce a slash at the start
$this->requestUri = '/' . trim($uri, '/');
$this->requestMethod = $this->request->getMethod();
$this->requestHeaders = $this->request->headers;
// 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';
}*/ //TODO: should this be done in application?
/*$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;
}
}*/
}
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.
//Maybe make middleware part of a base controller class?
return $route->run();
}
/**
* @return mixed|bool Returns a mixed response, or a boolean false if there was no valid route.
*/
public function run()
{
//Parse request
$this->readRequestData();
//Find the first matching route. Only one route will ever be executed.
$route = $this->findRoute($this->requestUri, $this->requestMethod);
//If a route is found, handle it and return its response.
//Otherwise, return a boolean false to denote no route could be found.
if($route) {
$this->currentRoute = $route;
return $this->handleRoute($route);
} else {
return false;
}
//If the request was a HEAD request, empty the output buffer.
/*if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
ob_end_clean();
}*/ //TODO: should this be done in application?
}
public function buildCache()
{
}
public function getCurrentRoute(): ?Route
{
return $this->currentRoute;
}
public function getRoute(string $pattern, ?string $method): ?Route
{
return $this->findRoute($pattern, $method ?? 'GET');
}
}