aboutsummaryrefslogtreecommitdiffhomepage
path: root/Digitigrade/Router.php
diff options
context:
space:
mode:
Diffstat (limited to 'Digitigrade/Router.php')
-rw-r--r--Digitigrade/Router.php98
1 files changed, 98 insertions, 0 deletions
diff --git a/Digitigrade/Router.php b/Digitigrade/Router.php
new file mode 100644
index 0000000..fb984d5
--- /dev/null
+++ b/Digitigrade/Router.php
@@ -0,0 +1,98 @@
+<?php
+namespace Digitigrade;
+
+use Digitigrade\HttpResponseStatus\InternalServerError;
+use Digitigrade\HttpResponseStatus\NoHandlerFound;
+
+class Router extends Singleton {
+
+ private readonly string $requestPath;
+ private array $routes;
+
+ protected function __construct() {
+ $this->requestPath = $_GET['requestPath'] ?? $_SERVER['PATH_INFO'] ?? explode('?', $_SERVER['REQUEST_URI'], 2)[0] ?? '/';
+ $this->routes = [];
+ }
+
+ /**
+ * Processes and sends a response to the current request
+ * @return void
+ */
+ public function processRequest() {
+ try {
+ $this->processRequestInner();
+ } catch (HttpResponseStatus $e) {
+ http_response_code($e->httpCode());
+ echo '<h1>' . $e->httpReason() . '</h1><pre>' . $e->getMessage() . '</pre>';
+ }
+ }
+
+ private function processRequestInner() {
+ $context = null;
+ try {
+ $route = $this->matchPath($this->requestPath);
+ if ($route == null) {
+ throw new NoHandlerFound($this->requestPath);
+ }
+ $context = 'handler for /' . implode('/', $route[0]);
+ $route[1]($route[2]); // call handler with args
+ } catch (\Exception $e) {
+ if (is_a($e, HttpResponseStatus::class)) {
+ throw $e;
+ }
+ throw new InternalServerError($e, $context);
+ } catch (\Error $e) {
+ throw new InternalServerError($e, $context);
+ }
+ }
+
+ public function mount(string $pathSpec, callable $handler) {
+ $this->routes[] = [$this->explodePathPieces($pathSpec), $handler];
+ }
+
+ private function explodePathPieces(string $pathSpec): array {
+ $pieces = explode('/', $pathSpec);
+ if ($pieces[0] == '')
+ array_shift($pieces);
+ return $pieces;
+ }
+
+ private function matchPath(string $requestPath): ?array {
+ // find matching candidates
+ $pieces = $this->explodePathPieces($requestPath);
+ $candidates = [];
+ foreach ($this->routes as $route) {
+ $routePieces = $route[0];
+ if (count($routePieces) != count($pieces))
+ continue;
+ $args = [];
+ $matches = true;
+ for ($i = 0; $i < count($pieces); $i++) {
+ if (str_starts_with($routePieces[$i], ':')) { // this is a parameter
+ $args[substr($routePieces[$i], 1)] = $pieces[$i];
+ } elseif ($routePieces[$i] != $pieces[$i]) {
+ $matches = false;
+ break;
+ }
+ }
+ if ($matches) {
+ $candidates[] = [$route[0], $route[1], $args];
+ }
+ }
+
+ // select the best matching one (has the longest path, for now)
+ $bestScore = 0;
+ $bestItem = null;
+ foreach ($candidates as $c) {
+ $score = count($c[0]) - count(array_filter($c[0], function ($item) {
+ return str_starts_with($item, ':');
+ })) * 0.1;
+ if ($score > $bestScore) {
+ $bestItem = $c;
+ $bestScore = $score;
+ }
+ }
+ return $bestItem;
+ }
+
+}