aboutsummaryrefslogtreecommitdiffhomepage
path: root/Digitigrade/Router.php
blob: 0682b7638991ff1e9de44baa58ad355f83b755e9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
<?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 = [];
    }

    public function getCurrentPath(): string {
        return $this->requestPath;
    }

    /**
     * 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());
            $e->httpHeaders();
            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;
    }

}