1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Linna Framework. |
5
|
|
|
* |
6
|
|
|
* @author Sebastian Rapetti <[email protected]> |
7
|
|
|
* @copyright (c) 2017, Sebastian Rapetti |
8
|
|
|
* @license http://opensource.org/licenses/MIT MIT License |
9
|
|
|
*/ |
10
|
|
|
declare(strict_types=1); |
11
|
|
|
|
12
|
|
|
namespace Linna\Http; |
13
|
|
|
|
14
|
|
|
use BadMethodCallException; |
15
|
|
|
use Linna\Shared\ClassOptionsTrait; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Router. |
19
|
|
|
* |
20
|
|
|
* Manage routes, verify every resource requested by browser and return |
21
|
|
|
* a RouteInterface Object. |
22
|
|
|
*/ |
23
|
|
|
class Router |
24
|
|
|
{ |
25
|
|
|
use ClassOptionsTrait; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var array Config options for class |
29
|
|
|
*/ |
30
|
|
|
protected $options = [ |
31
|
|
|
'basePath' => '/', |
32
|
|
|
'badRoute' => false, |
33
|
|
|
'rewriteMode' => false, |
34
|
|
|
'rewriteModeOffRouter' => '/index.php?', |
35
|
|
|
]; |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* @var RouteInterface Utilized for return the most recently parsed route |
39
|
|
|
*/ |
40
|
|
|
protected $route; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* @var array Passed from constructor, is the list of registerd routes for the app |
44
|
|
|
*/ |
45
|
|
|
private $routes; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var array List of regex for find parameter inside passed routes |
49
|
|
|
*/ |
50
|
|
|
private $matchTypes = [ |
51
|
|
|
'`\[[0-9A-Za-z]+\]`', |
52
|
|
|
]; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var array List of regex for find type of parameter inside passed routes |
56
|
|
|
*/ |
57
|
|
|
private $types = [ |
58
|
|
|
'[0-9A-Za-z]++', |
59
|
|
|
]; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Constructor. |
63
|
|
|
* Accept as parameter a list routes and options. |
64
|
|
|
* |
65
|
|
|
* @param array $routes |
66
|
|
|
* @param array $options |
67
|
|
|
*/ |
68
|
90 |
|
public function __construct(array $routes = [], array $options = []) |
69
|
|
|
{ |
70
|
|
|
//set options |
71
|
90 |
|
$this->setOptions($options); |
72
|
|
|
|
73
|
|
|
//set routes |
74
|
90 |
|
$this->routes = $routes; |
75
|
90 |
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* Evaluate request uri. |
79
|
|
|
* |
80
|
|
|
* @param string $requestUri |
81
|
|
|
* @param string $requestMethod |
82
|
|
|
* |
83
|
|
|
* @return bool |
84
|
|
|
*/ |
85
|
66 |
|
public function validate(string $requestUri, string $requestMethod) : bool |
86
|
|
|
{ |
87
|
66 |
|
$route = $this->findRoute($this->filterUri($requestUri), $requestMethod); |
88
|
|
|
|
89
|
66 |
|
if ($route) { |
|
|
|
|
90
|
55 |
|
$this->buildValidRoute($route); |
91
|
|
|
|
92
|
55 |
|
return true; |
93
|
|
|
} |
94
|
|
|
|
95
|
12 |
|
$this->buildErrorRoute(); |
96
|
|
|
|
97
|
12 |
|
return false; |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Find if provided route match with one of registered routes. |
102
|
|
|
* |
103
|
|
|
* @param string $uri |
104
|
|
|
* @param string $method |
105
|
|
|
* |
106
|
|
|
* @return array |
107
|
|
|
*/ |
108
|
66 |
|
private function findRoute(string $uri, string $method) : array |
109
|
|
|
{ |
110
|
66 |
|
$matches = []; |
111
|
66 |
|
$route = []; |
112
|
|
|
|
113
|
66 |
|
foreach ($this->routes as $value) { |
114
|
66 |
|
$urlMatch = preg_match('`^'.preg_replace($this->matchTypes, $this->types, $value['url']).'/?$`', $uri, $matches); |
115
|
66 |
|
$methodMatch = strpos($value['method'], $method); |
116
|
|
|
|
117
|
66 |
|
if ($urlMatch && $methodMatch !== false) { |
118
|
55 |
|
$route = $value; |
119
|
55 |
|
$route['matches'] = $matches; |
120
|
55 |
|
break; |
121
|
|
|
} |
122
|
|
|
} |
123
|
|
|
|
124
|
66 |
|
return $route; |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Build a valid route. |
129
|
|
|
* |
130
|
|
|
* @param array $route |
131
|
|
|
*/ |
132
|
55 |
|
private function buildValidRoute(array $route) |
133
|
|
|
{ |
134
|
|
|
//add to route array the passed uri for param check when call |
135
|
55 |
|
$matches = $route['matches']; |
136
|
|
|
|
137
|
|
|
//route match and there is a subpattern with action |
138
|
55 |
|
if (count($matches) > 1) { |
139
|
|
|
//assume that subpattern rapresent action |
140
|
17 |
|
$route['action'] = $matches[1]; |
141
|
|
|
|
142
|
|
|
//url clean |
143
|
17 |
|
$route['url'] = preg_replace('`\([0-9A-Za-z\|]++\)`', $matches[1], $route['url']); |
144
|
|
|
} |
145
|
|
|
|
146
|
55 |
|
$route['param'] = $this->buildParam($route); |
147
|
|
|
|
148
|
|
|
//delete matches key because not required inside route object |
149
|
55 |
|
unset($route['matches']); |
150
|
|
|
|
151
|
55 |
|
$this->route = new Route($route); |
152
|
55 |
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Try to find parameters in a valid route and return it. |
156
|
|
|
* |
157
|
|
|
* @param array $route |
158
|
|
|
* |
159
|
|
|
* @return array |
160
|
|
|
*/ |
161
|
55 |
|
private function buildParam(array $route): array |
162
|
|
|
{ |
163
|
55 |
|
$param = []; |
164
|
|
|
|
165
|
55 |
|
$url = explode('/', $route['url']); |
166
|
55 |
|
$matches = explode('/', $route['matches'][0]); |
167
|
|
|
|
168
|
55 |
|
$rawParam = array_diff($matches, $url); |
169
|
|
|
|
170
|
55 |
|
foreach ($rawParam as $key => $value) { |
171
|
24 |
|
$paramName = strtr($url[$key], ['[' => '', ']' => '']); |
172
|
24 |
|
$param[$paramName] = $value; |
173
|
|
|
} |
174
|
|
|
|
175
|
55 |
|
return $param; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* Actions for error route. |
180
|
|
|
* |
181
|
|
|
* @return void |
182
|
|
|
*/ |
183
|
12 |
|
private function buildErrorRoute() |
184
|
|
|
{ |
185
|
|
|
//check if there is a declared route for errors, if no exit with false |
186
|
12 |
|
if (($key = array_search($this->options['badRoute'], array_column($this->routes, 'name'), true)) === false) { |
187
|
3 |
|
$this->route = new NullRoute(); |
188
|
|
|
|
189
|
3 |
|
return; |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
//pick route for errors |
193
|
9 |
|
$route = $this->routes[$key]; |
194
|
|
|
|
195
|
|
|
//build and store route for errors |
196
|
9 |
|
$this->route = new Route($route); |
197
|
9 |
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Check if a route is valid and |
201
|
|
|
* return the route object else return a bad route object. |
202
|
|
|
* |
203
|
|
|
* @return RouteInterface |
204
|
|
|
*/ |
205
|
61 |
|
public function getRoute() : RouteInterface |
206
|
|
|
{ |
207
|
61 |
|
return $this->route; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
/** |
211
|
|
|
* Analize $_SERVER['REQUEST_URI'] for current uri, sanitize and return it. |
212
|
|
|
* |
213
|
|
|
* @param string $passedUri |
214
|
|
|
* |
215
|
|
|
* @return string |
216
|
|
|
*/ |
217
|
66 |
|
private function filterUri(string $passedUri): string |
218
|
|
|
{ |
219
|
|
|
//sanitize url |
220
|
66 |
|
$url = filter_var($passedUri, FILTER_SANITIZE_URL); |
221
|
|
|
|
222
|
|
|
//check for rewrite mode |
223
|
66 |
|
$url = str_replace($this->options['rewriteModeOffRouter'], '', $url); |
224
|
|
|
|
225
|
|
|
//remove basepath |
226
|
66 |
|
$url = substr($url, strlen($this->options['basePath'])); |
227
|
|
|
|
228
|
|
|
//remove doubled slash |
229
|
66 |
|
$url = str_replace('//', '/', $url); |
230
|
|
|
|
231
|
66 |
|
return (substr($url, 0, 1) === '/') ? $url : '/'.$url; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* @var array Allowed Http methods for fast route mapping. |
236
|
|
|
*/ |
237
|
|
|
private $fastMapMethods = [ |
238
|
|
|
'GET' => true, |
239
|
|
|
'POST' => true, |
240
|
|
|
'PUT' => true, |
241
|
|
|
'PATCH' => true, |
242
|
|
|
'DELETE' => true, |
243
|
|
|
]; |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Map a route. |
247
|
|
|
* |
248
|
|
|
* @param array $route |
249
|
|
|
*/ |
250
|
25 |
|
public function map(array $route) |
251
|
|
|
{ |
252
|
25 |
|
array_push($this->routes, $route); |
253
|
25 |
|
} |
254
|
|
|
|
255
|
|
|
/** |
256
|
|
|
* Fast route mapping. |
257
|
|
|
* |
258
|
|
|
* @param string $name |
259
|
|
|
* @param array $arguments |
260
|
|
|
* |
261
|
|
|
* @return void |
262
|
|
|
* |
263
|
|
|
* @throws BadMethodCallException |
264
|
|
|
*/ |
265
|
15 |
|
public function __call(string $name, array $arguments) |
266
|
|
|
{ |
267
|
15 |
|
$method = strtoupper($name); |
268
|
|
|
|
269
|
15 |
|
if (isset($this->fastMapMethods[$method])) { |
270
|
15 |
|
$this->map($this->createRouteArray($method, $arguments[0], $arguments[1], $arguments[2] ?? [])); |
271
|
|
|
|
272
|
15 |
|
return; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
throw new BadMethodCallException(__METHOD__.": Router->{$name}() method do not exist."); |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Create route array for previous methods. |
280
|
|
|
* |
281
|
|
|
* @param string $method |
282
|
|
|
* @param string $url |
283
|
|
|
* @param callable $callback |
284
|
|
|
* @param array $options |
285
|
|
|
* |
286
|
|
|
* @return array |
287
|
|
|
*/ |
288
|
15 |
|
private function createRouteArray(string $method, string $url, callable $callback, array $options) : array |
289
|
|
|
{ |
290
|
15 |
|
$routeArray = (new Route([ |
291
|
15 |
|
'method' => $method, |
292
|
15 |
|
'url' => $url, |
293
|
15 |
|
'callback' => $callback, |
294
|
15 |
|
]))->toArray(); |
295
|
|
|
|
296
|
15 |
|
$route = array_replace_recursive($routeArray, $options); |
297
|
|
|
|
298
|
15 |
|
return $route; |
299
|
|
|
} |
300
|
|
|
} |
301
|
|
|
|
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.