Passed
Push — master ( f4aea8...91cb90 )
by Alexander
01:38
created

Router::createRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 3
dl 0
loc 4
rs 10
c 0
b 0
f 0
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 $DELIMITER = '|';
20
    /**
21
     * @var array
22
     */
23
    private static $aliases = [];
24
    /**
25
     * @var array
26
     */
27
    private static $routes = [];
28
    /**
29
     * @var callable|null
30
     */
31
    private static $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 mixed $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
        self::$fallback = $callable;
81
    }
82
83
    /**
84
     * Returns the 404/fallback route, if it is configured
85
     *
86
     * @return null|interfaces\Route
87
     */
88
    public static function getFallback(): ?interfaces\Route
89
    {
90
        if (isset(self::$fallback)) {
91
            return static::createRoute('FALLBACK', self::$fallback);
92
        }
93
        return null;
94
    }
95
96
    /**
97
     * Given a request url and request method, identify route (dynamic or fixed)
98
     *
99
     * @param string $url Request url, i.e. '/api/user/32'
100
     * @param string $method Http::<GET/POST/PATCH/PUT/DELETE>
101
     * @return null|interfaces\Route
102
     */
103
    public static function match(string $url, string $method = Http::GET): ?interfaces\Route
104
    {
105
        if ($url[0] !== static::$DELIMITER && $url[0] !== '/') {
106
            $url = '/' . $url;
107
        }
108
        $url = self::$aliases[$url] ?? $url;
109
110
        if (isset(self::$routes[$method])) {
111
            if (isset(self::$routes[$method][$url])) {
112
                return static::createRoute($url, self::$routes[$method][$url]);
113
            }
114
115
            // TODO cache of previous matched dynamic routes
116
            return self::matchDynamicRoute($url, $method);
117
        }
118
        return null;
119
    }
120
121
    private static function matchDynamicRoute(string $url, string $method = Http::GET): ?interfaces\Route
122
    {
123
        foreach (self::$routes[$method] as $route => $cb) {
124
            if ($route[0] !== substr($route, -1) || $route[0] !== static::$DELIMITER) {
125
                continue;
126
            }
127
            $result = preg_match($route, $url, $matches);
128
            if (!$result) {
129
                continue;
130
            }
131
132
            $parameters = array_filter(
133
                $matches,
134
                function ($v) {
135
                    return !is_int($v);
136
                },
137
                \ARRAY_FILTER_USE_KEY
138
            );
139
140
            return static::createRoute($url, $cb, $parameters);
141
        }
142
143
        return null;
144
    }
145
146
    /**
147
     * Set up a route that uses the Page response for url to view files automation
148
     *
149
     * @param string $url
150
     * @return interfaces\Route
151
     */
152
    public static function getPageRoute(string $url): interfaces\Route
153
    {
154
        if ($url[0] !== static::$DELIMITER && $url[0] !== '/') {
155
            $url = '/' . $url;
156
        }
157
        $url = self::$aliases[$url] ?? $url;
158
        return static::createRoute(
159
            $url,
160
            function (Request $request): ?Response {
161
                $page = response\Page::fromRequest($request);
162
                // @TODO BC breaking, but move this?
163
                return $page->isValid() ? $page : null;
164
            }
165
        );
166
    }
167
168
    protected static function createRoute(string $url, callable $callback, ?array $params = []): interfaces\Route
169
    {
170
        // @TODO use an injectable Route factory
171
        return new Route($url, $callback, $params);
172
    }
173
}
174