179 lines
5.2 KiB
PHP
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');
|
|
}
|
|
} |