Router   A
last analyzed

Complexity

Total Complexity 29

Size/Duplication

Total Lines 172
Duplicated Lines 0 %

Importance

Changes 11
Bugs 4 Features 1
Metric Value
eloc 56
dl 0
loc 172
rs 10
c 11
b 4
f 1
wmc 29

8 Methods

Rating   Name   Duplication   Size   Complexity  
A alias() 0 9 5
A add() 0 12 5
A getFallback() 0 6 2
A fallback() 0 8 2
A match() 0 16 5
A createRoute() 0 4 1
A matchDynamicRoute() 0 25 5
A getPageRoute() 0 12 4
1
<?php
2
3
namespace alkemann\h2l;
4
5
use alkemann\h2l\util\Http;
6
use Closure;
7
8
/**
9
 * Class Router
10
 *
11
 * @package alkemann\h2l
12
 */
13
class Router implements interfaces\Router
14
{
15
    /**
16
     * Defines which character is used by regex dynamic routes
17
     * @var string
18
     */
19
    public static string $DELIMITER = '|';
20
    /**
21
     * @var array<string, string>
22
     */
23
    private static array $aliases = [];
24
    /**
25
     * @var array<string, array<string, Closure>>
26
     */
27
    private static array $routes = [];
28
    /**
29
     * @var null|Closure
30
     */
31
    private static ?Closure $fallback = null;
32
33
    /**
34
     * Add an alias route, i.e. `/` as alias for `home.html`
35
     *
36
     * @param string $alias
37
     * @param string $real
38
     */
39
    public static function alias(string $alias, string $real): void
40
    {
41
        if ($alias[0] !== static::$DELIMITER && $alias[0] !== '/') {
42
            $alias = '/' . $alias;
43
        }
44
        if ($real[0] !== static::$DELIMITER && $real[0] !== '/') {
45
            $real = '/' . $real;
46
        }
47
        self::$aliases[$alias] = $real;
48
    }
49
50
    /**
51
     * Add new dynamic route to application
52
     *
53
     * @param string $url Regex that is valid for preg_match, including named groups
54
     * @param callable $callable
55
     * @param string|string[] $methods a single Http::<GET/POST/PUT/PATCH/DELETE> or an array of multiple
56
     * @internal param Closure $closure Code to run on this match
57
     */
58
    public static function add(string $url, callable $callable, $methods = [Http::GET]): void
59
    {
60
        if ($url[0] !== static::$DELIMITER && $url[0] !== '/') {
61
            $url = '/' . $url;
62
        }
63
        if ($callable instanceof Closure) {
64
            $closure = $callable;
65
        } else {
66
            $closure = Closure::fromCallable($callable);
67
        }
68
        foreach ((array) $methods as $method) {
69
            self::$routes[$method][$url] = $closure;
70
        }
71
    }
72
73
    /**
74
     * Sets fallback route to be used if no other route is matched and Page is not used.
75
     *
76
     * @param callable $callable
77
     */
78
    public static function fallback(callable $callable): void
79
    {
80
        if ($callable instanceof Closure) {
81
            $closure = $callable;
82
        } else {
83
            $closure = Closure::fromCallable($callable);
84
        }
85
        self::$fallback = $closure;
86
    }
87
88
    /**
89
     * Returns the 404/fallback route, if it is configured
90
     *
91
     * @return null|interfaces\Route
92
     */
93
    public static function getFallback(): ?interfaces\Route
94
    {
95
        if (isset(self::$fallback)) {
96
            return static::createRoute('FALLBACK', self::$fallback);
97
        }
98
        return null;
99
    }
100
101
    /**
102
     * Given a request url and request method, identify route (dynamic or fixed)
103
     *
104
     * @param string $url Request url, i.e. '/api/user/32'
105
     * @param string $method Http::<GET/POST/PATCH/PUT/DELETE>
106
     * @return null|interfaces\Route
107
     */
108
    public static function match(string $url, string $method = Http::GET): ?interfaces\Route
109
    {
110
        if ($url[0] !== static::$DELIMITER && $url[0] !== '/') {
111
            $url = '/' . $url;
112
        }
113
        $url = self::$aliases[$url] ?? $url;
114
115
        if (isset(self::$routes[$method])) {
116
            if (isset(self::$routes[$method][$url])) {
117
                return static::createRoute($url, self::$routes[$method][$url]);
118
            }
119
120
            // TODO cache of previous matched dynamic routes
121
            return self::matchDynamicRoute($url, $method);
122
        }
123
        return null;
124
    }
125
126
    private static function matchDynamicRoute(string $url, string $method = Http::GET): ?interfaces\Route
127
    {
128
        foreach (self::$routes[$method] as $route => $cb) {
129
            if ($route[0] !== substr($route, -1) || $route[0] !== static::$DELIMITER) {
130
                continue;
131
            }
132
            $result = preg_match($route, $url, $matches);
133
            if (!$result) {
134
                continue;
135
            }
136
137
            $parameters = array_filter(
138
                $matches,
139
                /**
140
                 * @param string|int $v
141
                 * @return bool
142
                 */
143
                static fn($v) => !is_int($v),
144
                \ARRAY_FILTER_USE_KEY
145
            );
146
147
            return static::createRoute($url, $cb, $parameters);
148
        }
149
150
        return null;
151
    }
152
153
    /**
154
     * Set up a route that uses the Page response for url to view files automation
155
     *
156
     * @param string $url
157
     * @return interfaces\Route
158
     */
159
    public static function getPageRoute(string $url): interfaces\Route
160
    {
161
        if ($url[0] !== static::$DELIMITER && $url[0] !== '/') {
162
            $url = '/' . $url;
163
        }
164
        $url = self::$aliases[$url] ?? $url;
165
        return static::createRoute(
166
            $url,
167
            static function(Request $request): ?Response {
168
                $page = response\Page::fromRequest($request);
169
                // @TODO BC breaking, but move this?
170
                return $page->isValid() ? $page : null;
171
            }
172
        );
173
    }
174
175
    /**
176
     * @param string $url
177
     * @param callable $callback
178
     * @param array<string, mixed> $params
179
     * @return interfaces\Route
180
     */
181
    protected static function createRoute(string $url, callable $callback, array $params = []): interfaces\Route
182
    {
183
        // @TODO use an injectable Route factory
184
        return new Route($url, $callback, $params);
185
    }
186
}
187