1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Selami Router |
4
|
|
|
* PHP version 7+ |
5
|
|
|
* |
6
|
|
|
* @license https://github.com/selamiphp/router/blob/master/LICENSE (MIT License) |
7
|
|
|
* @link https://github.com/selamiphp/router |
8
|
|
|
* @package router |
9
|
|
|
* @category library |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
declare(strict_types = 1); |
13
|
|
|
|
14
|
|
|
namespace Selami; |
15
|
|
|
|
16
|
|
|
use FastRoute; |
17
|
|
|
use InvalidArgumentException; |
18
|
|
|
use UnexpectedValueException; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* Router |
22
|
|
|
* |
23
|
|
|
* This class is responsible for registering route objects, |
24
|
|
|
* determining aliases if available and finding requested route |
25
|
|
|
*/ |
26
|
|
|
final class Router |
27
|
|
|
{ |
28
|
|
|
/** |
29
|
|
|
* routes array to be registered. |
30
|
|
|
* Some routes may have aliases to be used in templating system |
31
|
|
|
* Route item can be defined using array key as an alias key. |
32
|
|
|
* @var array |
33
|
|
|
*/ |
34
|
|
|
private $routes = []; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* aliases array to be registered. |
38
|
|
|
* Each route item is an array has items respectively : Request Method, Request Uri, Controller/Action, Return Type. |
39
|
|
|
* @var array |
40
|
|
|
*/ |
41
|
|
|
private $aliases = []; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* HTTP request Method |
45
|
|
|
* @var string |
46
|
|
|
*/ |
47
|
|
|
private $method; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Request Uri |
51
|
|
|
* @var string |
52
|
|
|
*/ |
53
|
|
|
private $requestedPath; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Default return type if not noted in the $routes |
57
|
|
|
* @var string |
58
|
|
|
*/ |
59
|
|
|
private $defaultReturnType; |
60
|
|
|
|
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* Translation array. |
64
|
|
|
* Make sures about return type. |
65
|
|
|
* @var array |
66
|
|
|
*/ |
67
|
|
|
private static $translations = [ |
68
|
|
|
'h' => 'html', |
69
|
|
|
'html' => 'html', |
70
|
|
|
'r' => 'redirect', |
71
|
|
|
'redirect' => 'redirect', |
72
|
|
|
'j' => 'json', |
73
|
|
|
'json' => 'json', |
74
|
|
|
't' => 'text', |
75
|
|
|
'text' => 'text', |
76
|
|
|
'd' => 'download', |
77
|
|
|
'download' => 'download' |
78
|
|
|
]; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Valid Request Methods array. |
82
|
|
|
* Make sures about requested methods. |
83
|
|
|
* @var array |
84
|
|
|
*/ |
85
|
|
|
private static $validRequestMethods = [ |
86
|
|
|
'GET', |
87
|
|
|
'OPTIONS', |
88
|
|
|
'HEAD', |
89
|
|
|
'POST', |
90
|
|
|
'PUT', |
91
|
|
|
'DELETE', |
92
|
|
|
'PATCH' |
93
|
|
|
]; |
94
|
|
|
|
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Valid Request Methods array. |
98
|
|
|
* Make sures about return type. |
99
|
|
|
* Index 0 is also default value. |
100
|
|
|
* @var array |
101
|
|
|
*/ |
102
|
|
|
private static $validReturnTypes = [ |
103
|
|
|
'html', |
104
|
|
|
'json', |
105
|
|
|
'text', |
106
|
|
|
'redirect', |
107
|
|
|
'download' |
108
|
|
|
]; |
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* Router constructor. |
112
|
|
|
* Create new router. |
113
|
|
|
* |
114
|
|
|
* @param string $defaultReturnType |
115
|
|
|
* @param string $method |
116
|
|
|
* @param string $requestedPath |
117
|
|
|
* @param string $folder |
118
|
|
|
* @throws UnexpectedValueException |
119
|
|
|
*/ |
120
|
11 |
|
public function __construct( |
121
|
|
|
string $defaultReturnType, |
122
|
|
|
string $method, |
123
|
|
|
string $requestedPath, |
124
|
|
|
string $folder = '' |
125
|
|
|
) { |
126
|
11 |
View Code Duplication |
if (!in_array($method, self::$validRequestMethods, true)) { |
|
|
|
|
127
|
1 |
|
$message = sprintf('%s is not valid Http request method.', $method); |
128
|
1 |
|
throw new UnexpectedValueException($message); |
129
|
|
|
} |
130
|
10 |
|
$this->method = $method; |
131
|
10 |
|
$this->requestedPath = $this->extractFolder($requestedPath, $folder); |
132
|
10 |
|
$this->defaultReturnType = self::$translations[$defaultReturnType] ?? self::$validReturnTypes[0]; |
133
|
10 |
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* Remove sub folder from requestedPath if defined |
137
|
|
|
* @param string $requestPath |
138
|
|
|
* @param string $folder |
139
|
|
|
* @return string |
140
|
|
|
*/ |
141
|
10 |
|
private function extractFolder(string $requestPath, string $folder) |
142
|
|
|
{ |
143
|
10 |
|
if (!empty($folder)) { |
144
|
1 |
|
$requestPath = '/' . trim(preg_replace('#^/' . $folder . '#msi', '/', $requestPath), '/'); |
145
|
|
|
} |
146
|
10 |
|
if ($requestPath === '') { |
147
|
1 |
|
$requestPath = '/'; |
148
|
|
|
} |
149
|
10 |
|
return $requestPath; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
/** |
153
|
|
|
* add route to routes list |
154
|
|
|
* @param string|array requestMethods |
155
|
|
|
* @param string $route |
156
|
|
|
* @param string $action |
157
|
|
|
* @param string $returnType |
158
|
|
|
* @param string $alias |
159
|
|
|
* @throws InvalidArgumentException |
160
|
|
|
* @throws UnexpectedValueException |
161
|
|
|
*/ |
162
|
9 |
|
public function add($requestMethods, string $route, string $action, string $returnType=null, string $alias=null) |
163
|
|
|
{ |
164
|
9 |
|
$requestMethodsGiven = is_array($requestMethods) ? (array) $requestMethods : [0 => $requestMethods]; |
165
|
9 |
|
$returnType = $this->checkReturnType($returnType); |
166
|
9 |
|
foreach ($requestMethodsGiven as $requestMethod) { |
167
|
9 |
|
$this->checkRequestMethodParameterType($requestMethod); |
168
|
8 |
|
$this->checkRequestMethodIsValid($requestMethod); |
169
|
7 |
|
if ($alias !== null) { |
170
|
7 |
|
$this->aliases[$alias] = $route; |
171
|
|
|
} |
172
|
7 |
|
$this->routes[] = [strtoupper($requestMethod), $route, $action, $returnType]; |
173
|
|
|
} |
174
|
7 |
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* @param string $method |
178
|
|
|
* @param array $args |
179
|
|
|
* @throws UnexpectedValueException |
180
|
|
|
*/ |
181
|
2 |
|
public function __call(string $method, array $args) |
182
|
|
|
{ |
183
|
|
|
|
184
|
2 |
|
$this->checkRequestMethodIsValid($method); |
185
|
|
|
$defaults = [ |
186
|
1 |
|
null, |
187
|
|
|
null, |
188
|
1 |
|
$this->defaultReturnType, |
189
|
|
|
null |
190
|
|
|
]; |
191
|
1 |
|
list($route, $action, $returnType, $alias) = array_merge($args, $defaults); |
192
|
1 |
|
$this->add($method, $route, $action, $returnType, $alias); |
193
|
1 |
|
} |
194
|
|
|
|
195
|
|
|
/** |
196
|
|
|
* @param string|null $returnType |
197
|
|
|
* @return string |
198
|
|
|
*/ |
199
|
9 |
|
private function checkReturnType($returnType) { |
200
|
9 |
|
return $returnType === null ? $this->defaultReturnType : self::$validReturnTypes[$returnType] ?? $this->defaultReturnType; |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* @param string $requestMethod |
205
|
|
|
* Checks if request method is valid |
206
|
|
|
* @throws UnexpectedValueException; |
207
|
|
|
*/ |
208
|
9 |
|
private function checkRequestMethodIsValid(string $requestMethod) |
209
|
|
|
{ |
210
|
9 |
View Code Duplication |
if (!in_array(strtoupper($requestMethod), self::$validRequestMethods, true)) { |
|
|
|
|
211
|
2 |
|
$message = sprintf('%s is not valid Http request method.', $requestMethod); |
212
|
2 |
|
throw new UnexpectedValueException($message); |
213
|
|
|
} |
214
|
7 |
|
} |
215
|
|
|
|
216
|
|
|
/** |
217
|
|
|
* @param $requestMethod |
218
|
|
|
* @throws InvalidArgumentException |
219
|
|
|
*/ |
220
|
9 |
|
private function checkRequestMethodParameterType($requestMethod) |
221
|
|
|
{ |
222
|
9 |
|
$requestMethodParameterType = gettype($requestMethod); |
223
|
9 |
|
if (!in_array($requestMethodParameterType, ['array', 'string'], true)) { |
224
|
1 |
|
$message = sprintf( |
225
|
1 |
|
'Request method must be string or array but %s given.', |
226
|
|
|
$requestMethodParameterType); |
227
|
1 |
|
throw new InvalidArgumentException($message); |
228
|
|
|
} |
229
|
8 |
|
} |
230
|
|
|
|
231
|
|
|
/** |
232
|
|
|
* Dispatch against the provided HTTP method verb and URI. |
233
|
|
|
* @return array |
234
|
|
|
*/ |
235
|
3 |
|
private function dispatcher() |
236
|
|
|
{ |
237
|
|
|
$options = [ |
238
|
3 |
|
'routeParser' => FastRoute\RouteParser\Std::class, |
239
|
|
|
'dataGenerator' => FastRoute\DataGenerator\GroupCountBased::class, |
240
|
|
|
'dispatcher' => FastRoute\Dispatcher\GroupCountBased::class, |
241
|
|
|
'routeCollector' => FastRoute\RouteCollector::class, |
242
|
|
|
]; |
243
|
|
|
/** @var RouteCollector $routeCollector */ |
244
|
3 |
|
$routeCollector = new $options['routeCollector']( |
245
|
3 |
|
new $options['routeParser'], new $options['dataGenerator'] |
246
|
|
|
); |
247
|
3 |
|
$this->addRoutes($routeCollector); |
248
|
3 |
|
return new $options['dispatcher']($routeCollector->getData()); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Define Closures for all routes that returns controller info to be used. |
253
|
|
|
* @param FastRoute\RouteCollector $route |
254
|
|
|
*/ |
255
|
3 |
|
private function addRoutes(FastRoute\RouteCollector $route) |
256
|
|
|
{ |
257
|
3 |
|
foreach ($this->routes as $definedRoute) { |
258
|
3 |
|
$definedRoute[3] = $definedRoute[3] ?? $this->defaultReturnType; |
259
|
3 |
|
$route->addRoute(strtoupper($definedRoute[0]), $definedRoute[1], function ($args) use ($definedRoute) { |
260
|
1 |
|
list(,,$controller, $returnType) = $definedRoute; |
261
|
1 |
|
$returnType = Router::$translations[$returnType] ?? $this->defaultReturnType; |
262
|
1 |
|
return ['controller' => $controller, 'returnType'=> $returnType, 'args'=> $args]; |
263
|
3 |
|
}); |
264
|
|
|
} |
265
|
3 |
|
} |
266
|
|
|
|
267
|
|
|
|
268
|
|
|
|
269
|
|
|
/** |
270
|
|
|
* Get router data that includes route info and aliases |
271
|
|
|
*/ |
272
|
3 |
|
public function getRoute() |
273
|
|
|
{ |
274
|
3 |
|
$dispatcher = $this->dispatcher(); |
275
|
3 |
|
$routeInfo = $dispatcher->dispatch($this->method, $this->requestedPath); |
276
|
|
|
$routerData = [ |
277
|
3 |
|
'route' => $this->runDispatcher($routeInfo), |
278
|
3 |
|
'aliases' => $this->aliases |
279
|
|
|
]; |
280
|
3 |
|
return $routerData; |
281
|
|
|
} |
282
|
|
|
|
283
|
|
|
|
284
|
|
|
/** |
285
|
|
|
* Get route info for requested uri |
286
|
|
|
* @param array $routeInfo |
287
|
|
|
* @return array $routerData |
288
|
|
|
*/ |
289
|
3 |
|
private function runDispatcher(array $routeInfo) |
290
|
|
|
{ |
291
|
3 |
|
$routeData = $this->getRouteData($routeInfo); |
292
|
|
|
$dispatchResults = [ |
293
|
3 |
|
FastRoute\Dispatcher::METHOD_NOT_ALLOWED => [ |
294
|
|
|
'status' => 405 |
295
|
3 |
|
], |
296
|
3 |
|
FastRoute\Dispatcher::FOUND => [ |
297
|
|
|
'status' => 200 |
298
|
|
|
], |
299
|
3 |
|
FastRoute\Dispatcher::NOT_FOUND => [ |
300
|
|
|
'status' => 404 |
301
|
|
|
] |
302
|
|
|
]; |
303
|
3 |
|
return array_merge($routeData, $dispatchResults[$routeInfo[0]]); |
304
|
|
|
} |
305
|
|
|
|
306
|
|
|
/** |
307
|
|
|
* Get routeData according to dispatcher's results |
308
|
|
|
* @param array $routeInfo |
309
|
|
|
* @return array |
310
|
|
|
*/ |
311
|
3 |
|
private function getRouteData(array $routeInfo) |
312
|
|
|
{ |
313
|
3 |
|
if ($routeInfo[0] === FastRoute\Dispatcher::FOUND) { |
314
|
1 |
|
list(, $handler, $vars) = $routeInfo; |
315
|
1 |
|
return $handler($vars); |
316
|
|
|
} |
317
|
|
|
return [ |
318
|
2 |
|
'status' => 200, |
319
|
|
|
'returnType' => 'html', |
320
|
|
|
'definedRoute' => null, |
321
|
|
|
'args' => [] |
322
|
|
|
]; |
323
|
|
|
} |
324
|
|
|
} |
325
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.