390 lines
11 KiB
PHP
390 lines
11 KiB
PHP
|
<?php
|
||
|
|
||
|
namespace Objects {
|
||
|
|
||
|
use Driver\Logger\Logger;
|
||
|
use Objects\Router\AbstractRoute;
|
||
|
|
||
|
class Router {
|
||
|
|
||
|
private User $user;
|
||
|
private Logger $logger;
|
||
|
protected array $routes;
|
||
|
protected array $statusCodeRoutes;
|
||
|
|
||
|
public function __construct(User $user) {
|
||
|
$this->user = $user;
|
||
|
$this->logger = new Logger("Router", $user->getSQL());
|
||
|
$this->routes = [];
|
||
|
$this->statusCodeRoutes = [];
|
||
|
}
|
||
|
|
||
|
public function run(string $url): string {
|
||
|
|
||
|
// TODO: do we want a global try cache and return status page 500 on any error?
|
||
|
// or do we want to have a global status page function here?
|
||
|
|
||
|
$url = strtok($url, "?");
|
||
|
foreach ($this->routes as $route) {
|
||
|
$pathParams = $route->match($url);
|
||
|
if ($pathParams !== false) {
|
||
|
return $route->call($this, $pathParams);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $this->returnStatusCode(404);
|
||
|
}
|
||
|
|
||
|
public function returnStatusCode(int $code, array $params = []): string {
|
||
|
http_response_code($code);
|
||
|
$params["status_code"] = $code;
|
||
|
$params["status_description"] = HTTP_STATUS_DESCRIPTIONS[$code] ?? "Unknown Error";
|
||
|
$route = $this->statusCodeRoutes[strval($code)] ?? null;
|
||
|
if ($route) {
|
||
|
return $route->call($this, $params);
|
||
|
} else {
|
||
|
$req = new \Api\Template\Render($this->user);
|
||
|
$res = $req->execute(["file" => "error_document.twig", "parameters" => $params]);
|
||
|
if ($res) {
|
||
|
return $req->getResult()["html"];
|
||
|
} else {
|
||
|
var_dump($req->getLastError());
|
||
|
$description = htmlspecialchars($params["status_description"]);
|
||
|
return "<b>$code - $description</b>";
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function addRoute(AbstractRoute $route) {
|
||
|
if (preg_match("/^\d+$/", $route->getPattern())) {
|
||
|
$this->statusCodeRoutes[$route->getPattern()] = $route;
|
||
|
} else {
|
||
|
$this->routes[] = $route;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function writeCache(string $file): bool {
|
||
|
|
||
|
$routes = "";
|
||
|
foreach ($this->routes as $route) {
|
||
|
$constructor = $route->generateCache();
|
||
|
$routes .= "\n \$this->addRoute($constructor);";
|
||
|
}
|
||
|
|
||
|
$date = (new \DateTime())->format("Y/m/d H:i:s");
|
||
|
$code = "<?php
|
||
|
|
||
|
/**
|
||
|
* DO NOT EDIT!
|
||
|
* This file is automatically generated by the RoutesAPI on $date.
|
||
|
*/
|
||
|
|
||
|
namespace Cache;
|
||
|
use Objects\User;
|
||
|
use Objects\Router;
|
||
|
|
||
|
class RouterCache extends Router {
|
||
|
|
||
|
public function __construct(User \$user) {
|
||
|
parent::__construct(\$user);$routes
|
||
|
}
|
||
|
}
|
||
|
";
|
||
|
|
||
|
if (@file_put_contents($file, $code) === false) {
|
||
|
$this->logger->severe("Could not write Router cache file: $file");
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public function getUser(): User {
|
||
|
return $this->user;
|
||
|
}
|
||
|
|
||
|
public function getLogger(): Logger {
|
||
|
return $this->logger;
|
||
|
}
|
||
|
|
||
|
public static function cleanURL(string $url, bool $cleanGET = true): string {
|
||
|
// strip GET parameters
|
||
|
if ($cleanGET) {
|
||
|
if (($index = strpos($url, "?")) !== false) {
|
||
|
$url = substr($url, 0, $index);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// strip document reference part
|
||
|
if (($index = strpos($url, "#")) !== false) {
|
||
|
$url = substr($url, 0, $index);
|
||
|
}
|
||
|
|
||
|
// strip leading slash
|
||
|
return preg_replace("/^\/+/", "", $url);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
namespace Objects\Router {
|
||
|
|
||
|
use Api\Parameter\Parameter;
|
||
|
use Elements\Document;
|
||
|
use Objects\Router;
|
||
|
use PHPUnit\TextUI\ReflectionException;
|
||
|
|
||
|
abstract class AbstractRoute {
|
||
|
|
||
|
private string $pattern;
|
||
|
private bool $exact;
|
||
|
|
||
|
public function __construct(string $pattern, bool $exact = true) {
|
||
|
$this->pattern = $pattern;
|
||
|
$this->exact = $exact;
|
||
|
}
|
||
|
|
||
|
private static function parseParamType(?string $type): ?int {
|
||
|
if ($type === null || trim($type) === "") {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
$type = strtolower(trim($type));
|
||
|
if (in_array($type, ["int", "integer"])) {
|
||
|
return Parameter::TYPE_INT;
|
||
|
} else if (in_array($type, ["float", "double"])) {
|
||
|
return Parameter::TYPE_FLOAT;
|
||
|
} else if (in_array($type, ["bool", "boolean"])) {
|
||
|
return Parameter::TYPE_BOOLEAN;
|
||
|
} else {
|
||
|
return Parameter::TYPE_STRING;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
public function getPattern(): string {
|
||
|
return $this->pattern;
|
||
|
}
|
||
|
|
||
|
public abstract function call(Router $router, array $params): string;
|
||
|
|
||
|
protected function getArgs(): array {
|
||
|
return [$this->pattern, $this->exact];
|
||
|
}
|
||
|
|
||
|
public function generateCache(): string {
|
||
|
$reflection = new \ReflectionClass($this);
|
||
|
$className = $reflection->getName();
|
||
|
$args = implode(", ", array_map(function ($arg) {
|
||
|
return var_export($arg, true);
|
||
|
}, $this->getArgs()));
|
||
|
return "new \\$className($args)";
|
||
|
}
|
||
|
|
||
|
public function match(string $url) {
|
||
|
|
||
|
# /test/{abc}/{param:?}/{xyz:int}/{aaa:int?}
|
||
|
$patternParts = explode("/", Router::cleanURL($this->pattern, false));
|
||
|
$countPattern = count($patternParts);
|
||
|
$patternOffset = 0;
|
||
|
|
||
|
# /test/param/optional/123
|
||
|
$urlParts = explode("/", $url);
|
||
|
$countUrl = count($urlParts);
|
||
|
$urlOffset = 0;
|
||
|
|
||
|
$params = [];
|
||
|
for (; $patternOffset < $countPattern; $patternOffset++) {
|
||
|
|
||
|
if (!preg_match("/^{.*}$/", $patternParts[$patternOffset])) {
|
||
|
|
||
|
// not a parameter? check if it matches
|
||
|
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] !== $patternParts[$patternOffset]) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
$urlOffset++;
|
||
|
|
||
|
} else {
|
||
|
|
||
|
// we got a parameter here
|
||
|
$paramDefinition = explode(":", substr($patternParts[$patternOffset], 1, -1));
|
||
|
$paramName = array_shift($paramDefinition);
|
||
|
$paramType = array_shift($paramDefinition);
|
||
|
$paramOptional = endsWith($paramType, "?");
|
||
|
if ($paramOptional) {
|
||
|
$paramType = substr($paramType, 0, -1);
|
||
|
}
|
||
|
|
||
|
$paramType = self::parseParamType($paramType);
|
||
|
if ($urlOffset >= $countUrl || $urlParts[$urlOffset] === "") {
|
||
|
if ($paramOptional) {
|
||
|
$param = $urlParts[$urlOffset] ?? null;
|
||
|
if ($param !== null && $paramType !== null && Parameter::parseType($param) !== $paramType) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
$params[$paramName] = $param;
|
||
|
if ($urlOffset < $countUrl) {
|
||
|
$urlOffset++;
|
||
|
}
|
||
|
} else {
|
||
|
return false;
|
||
|
}
|
||
|
} else {
|
||
|
$param = $urlParts[$urlOffset];
|
||
|
if ($paramType !== null && Parameter::parseType($param) !== $paramType) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
$params[$paramName] = $param;
|
||
|
$urlOffset++;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($urlOffset !== $countUrl && $this->exact) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return $params;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class EmptyRoute extends AbstractRoute {
|
||
|
|
||
|
public function __construct(string $pattern, bool $exact = true) {
|
||
|
parent::__construct($pattern, $exact);
|
||
|
}
|
||
|
|
||
|
public function call(Router $router, array $params): string {
|
||
|
return "";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class StaticFileRoute extends AbstractRoute {
|
||
|
|
||
|
private string $path;
|
||
|
private int $code;
|
||
|
|
||
|
public function __construct(string $pattern, bool $exact, string $path, int $code = 200) {
|
||
|
parent::__construct($pattern, $exact);
|
||
|
$this->path = $path;
|
||
|
$this->code = $code;
|
||
|
}
|
||
|
|
||
|
public function call(Router $router, array $params): string {
|
||
|
http_response_code($this->code);
|
||
|
return serveStatic(WEBROOT, $this->path);
|
||
|
}
|
||
|
|
||
|
protected function getArgs(): array {
|
||
|
return array_merge(parent::getArgs(), [$this->path, $this->code]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class StaticRoute extends AbstractRoute {
|
||
|
|
||
|
private string $data;
|
||
|
private int $code;
|
||
|
|
||
|
public function __construct(string $pattern, bool $exact, string $data, int $code = 200) {
|
||
|
parent::__construct($pattern, $exact);
|
||
|
$this->data = $data;
|
||
|
$this->code = $code;
|
||
|
}
|
||
|
|
||
|
public function call(Router $router, array $params): string {
|
||
|
http_response_code($this->code);
|
||
|
return $this->data;
|
||
|
}
|
||
|
|
||
|
protected function getArgs(): array {
|
||
|
return array_merge(parent::getArgs(), [$this->data, $this->code]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class RedirectRoute extends AbstractRoute {
|
||
|
|
||
|
private string $destination;
|
||
|
private int $code;
|
||
|
|
||
|
public function __construct(string $pattern, bool $exact, string $destination, int $code = 307) {
|
||
|
parent::__construct($pattern, $exact);
|
||
|
$this->destination = $destination;
|
||
|
$this->code = $code;
|
||
|
}
|
||
|
|
||
|
public function call(Router $router, array $params): string {
|
||
|
header("Location: $this->destination");
|
||
|
http_response_code($this->code);
|
||
|
return "";
|
||
|
}
|
||
|
|
||
|
protected function getArgs(): array {
|
||
|
return array_merge(parent::getArgs(), [$this->destination, $this->code]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class DocumentRoute extends AbstractRoute {
|
||
|
|
||
|
private string $className;
|
||
|
private array $args;
|
||
|
private ?\ReflectionClass $reflectionClass;
|
||
|
|
||
|
public function __construct(string $pattern, bool $exact, string $className, ...$args) {
|
||
|
parent::__construct($pattern, $exact);
|
||
|
$this->className = $className;
|
||
|
$this->args = $args;
|
||
|
$this->reflectionClass = null;
|
||
|
}
|
||
|
|
||
|
private function loadClass(): bool {
|
||
|
|
||
|
if ($this->reflectionClass === null) {
|
||
|
try {
|
||
|
$file = getClassPath($this->className);
|
||
|
if (file_exists($file)) {
|
||
|
$this->reflectionClass = new \ReflectionClass($this->className);
|
||
|
if ($this->reflectionClass->isSubclassOf(Document::class)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
} catch (ReflectionException $exception) {
|
||
|
$this->reflectionClass = null;
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
$this->reflectionClass = null;
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
public function match(string $url) {
|
||
|
$match = parent::match($url);
|
||
|
if ($match === false || !$this->loadClass()) {
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return $match;
|
||
|
}
|
||
|
|
||
|
protected function getArgs(): array {
|
||
|
return array_merge(parent::getArgs(), [$this->className], $this->args);
|
||
|
}
|
||
|
|
||
|
public function call(Router $router, array $params): string {
|
||
|
if (!$this->loadClass()) {
|
||
|
return $router->returnStatusCode(500, [ "message" => "Error loading class: $this->className"]);
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$args = array_merge([$router->getUser()], $this->args);
|
||
|
$document = $this->reflectionClass->newInstanceArgs($args);
|
||
|
return $document->getCode($params);
|
||
|
} catch (\ReflectionException $e) {
|
||
|
return $router->returnStatusCode(500, [ "message" => "Error loading class $this->className: " . $e->getMessage()]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|