1
|
|
|
<?php |
|
|
|
|
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Codeburner Framework. |
5
|
|
|
* |
6
|
|
|
* @author Alex Rohleder <[email protected]> |
7
|
|
|
* @copyright 2016 Alex Rohleder |
8
|
|
|
* @license http://opensource.org/licenses/MIT |
9
|
|
|
*/ |
10
|
|
|
|
11
|
|
|
namespace Codeburner\Router; |
12
|
|
|
|
13
|
|
|
use Codeburner\Router\Exceptions\BadRouteException; |
14
|
|
|
use Codeburner\Router\Exceptions\MethodNotSupportedException; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Explicit Avoiding autoload for classes and traits |
18
|
|
|
* that are aways needed. Don't need an condition of class exists |
19
|
|
|
* because the routes will not be used until the collector is used. |
20
|
|
|
*/ |
21
|
|
|
|
22
|
1 |
|
include __DIR__ . "/Route.php"; |
23
|
1 |
|
include __DIR__ . "/Group.php"; |
24
|
1 |
|
include __DIR__ . "/Collectors/ControllerCollectorTrait.php"; |
25
|
1 |
|
include __DIR__ . "/Collectors/ResourceCollectorTrait.php"; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* The Collector class hold, parse and build routes. |
29
|
|
|
* |
30
|
|
|
* @author Alex Rohleder <[email protected]> |
31
|
|
|
*/ |
32
|
|
|
|
33
|
|
|
class Collector |
34
|
|
|
{ |
35
|
|
|
|
36
|
|
|
use Collectors\ControllerCollectorTrait; |
37
|
|
|
use Collectors\ResourceCollectorTrait; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* These regex define the structure of a dynamic segment in a pattern. |
41
|
|
|
* |
42
|
|
|
* @var string |
43
|
|
|
*/ |
44
|
|
|
|
45
|
|
|
const DYNAMIC_REGEX = "{\s*(\w*)\s*(?::\s*([^{}]*(?:{(?-1)}*)*))?\s*}"; |
46
|
|
|
|
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* All the supported http methods separated by spaces. |
50
|
|
|
* |
51
|
|
|
* @var string |
52
|
|
|
*/ |
53
|
|
|
|
54
|
|
|
const HTTP_METHODS = "get post put patch delete"; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* The static routes are simple stored in a multidimensional array, the first |
58
|
|
|
* dimension is indexed by an http method and hold an array indexed with the patterns |
59
|
|
|
* and holding the route. ex. [METHOD => [PATTERN => ROUTE]] |
60
|
|
|
* |
61
|
|
|
* @var array |
62
|
|
|
*/ |
63
|
|
|
|
64
|
|
|
protected $statics = []; |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* The dynamic routes have parameters and are stored in a hashtable that every cell have |
68
|
|
|
* an array with route patterns as indexes and routes as values. ex. [INDEX => [PATTERN => ROUTE]] |
69
|
|
|
* |
70
|
|
|
* @var array |
71
|
|
|
*/ |
72
|
|
|
|
73
|
|
|
protected $dynamics = []; |
74
|
|
|
|
75
|
|
|
/** |
76
|
|
|
* Some regex wildcards for easily definition of dynamic routes. ps. all keys and values must start with : |
77
|
|
|
* |
78
|
|
|
* @var array |
79
|
|
|
*/ |
80
|
|
|
|
81
|
|
|
protected $wildcards = [ |
82
|
|
|
":uid" => ":uid-[a-zA-Z0-9]", |
83
|
|
|
":slug" => ":[a-z0-9-]", |
84
|
|
|
":string" => ":\w", |
85
|
|
|
":int" => ":\d", |
86
|
|
|
":integer" => ":\d", |
87
|
|
|
":float" => ":[-+]?\d*?[.]?\d", |
88
|
|
|
":double" => ":[-+]?\d*?[.]?\d", |
89
|
|
|
":hex" => ":0[xX][0-9a-fA-F]", |
90
|
|
|
":octal" => ":0[1-7][0-7]", |
91
|
|
|
":bool" => ":1|0|true|false|yes|no", |
92
|
|
|
":boolean" => ":1|0|true|false|yes|no", |
93
|
|
|
]; |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* @param string $method |
97
|
|
|
* @param string $pattern |
98
|
|
|
* @param string|array|\Closure $action |
99
|
|
|
* |
100
|
|
|
* @throws BadRouteException |
101
|
|
|
* @throws MethodNotSupportedException |
102
|
|
|
* |
103
|
|
|
* @return Group |
104
|
|
|
*/ |
105
|
|
|
|
106
|
39 |
|
public function set($method, $pattern, $action) |
107
|
|
|
{ |
108
|
39 |
|
$method = $this->parseMethod($method); |
109
|
38 |
|
$patterns = $this->parsePattern($pattern); |
110
|
35 |
|
$group = new Group; |
111
|
|
|
|
112
|
35 |
|
foreach ($patterns as $pattern) |
113
|
|
|
{ |
114
|
35 |
|
$route = new Route($this, $method, $pattern, $action); |
115
|
35 |
|
$group->setRoute($route); |
116
|
|
|
|
117
|
35 |
|
if (strpos($pattern, "{") !== false) { |
118
|
24 |
|
$index = $this->getDynamicIndex($method, $pattern); |
119
|
24 |
|
$this->dynamics[$index][$pattern] = $route; |
120
|
35 |
|
} else $this->statics[$method][$pattern] = $route; |
121
|
35 |
|
} |
122
|
|
|
|
123
|
35 |
|
return $group; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
public function get ($pattern, $action) { return $this->set("get" , $pattern, $action); } |
127
|
|
|
public function post ($pattern, $action) { return $this->set("post" , $pattern, $action); } |
128
|
|
|
public function put ($pattern, $action) { return $this->set("put" , $pattern, $action); } |
129
|
|
|
public function patch ($pattern, $action) { return $this->set("patch" , $pattern, $action); } |
130
|
|
|
public function delete($pattern, $action) { return $this->set("delete", $pattern, $action); } |
131
|
|
|
|
132
|
|
|
/** |
133
|
|
|
* Insert a route into several http methods. |
134
|
|
|
* |
135
|
|
|
* @param string[] $methods |
136
|
|
|
* @param string $pattern |
137
|
|
|
* @param string|array|\Closure $action |
138
|
|
|
* |
139
|
|
|
* @return Group |
140
|
|
|
*/ |
141
|
|
|
|
142
|
3 |
|
public function match(array $methods, $pattern, $action) |
143
|
|
|
{ |
144
|
3 |
|
$group = new Group; |
145
|
3 |
|
foreach ($methods as $method) |
146
|
3 |
|
$group->set($this->set($method, $pattern, $action)); |
147
|
3 |
|
return $group; |
148
|
|
|
} |
149
|
|
|
|
150
|
|
|
/** |
151
|
|
|
* Insert a route into every http method supported. |
152
|
|
|
* |
153
|
|
|
* @param string $pattern |
154
|
|
|
* @param string|array|\Closure $action |
155
|
|
|
* |
156
|
|
|
* @return Group |
157
|
|
|
*/ |
158
|
|
|
|
159
|
1 |
|
public function any($pattern, $action) |
160
|
|
|
{ |
161
|
1 |
|
return $this->match(explode(" ", self::HTTP_METHODS), $pattern, $action); |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Insert a route into every http method supported but the given ones. |
166
|
|
|
* |
167
|
|
|
* @param string[] $methods |
168
|
|
|
* @param string $pattern |
169
|
|
|
* @param string|array|\Closure $action |
170
|
|
|
* |
171
|
|
|
* @return Group |
172
|
|
|
*/ |
173
|
|
|
|
174
|
1 |
|
public function except(array $methods, $pattern, $action) |
175
|
|
|
{ |
176
|
1 |
|
return $this->match(array_diff(explode(" ", self::HTTP_METHODS), $methods), $pattern, $action); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Group all given routes. |
181
|
|
|
* |
182
|
|
|
* @param Route[] $routes |
183
|
|
|
* @return Group |
184
|
|
|
*/ |
185
|
|
|
|
186
|
2 |
|
public function group(array $routes) |
187
|
|
|
{ |
188
|
2 |
|
$group = new Group; |
189
|
2 |
|
foreach ($routes as $route) |
190
|
2 |
|
$group->set($route); |
191
|
2 |
|
return $group; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Remove a route from collector. |
196
|
|
|
* |
197
|
|
|
* @param string $method |
198
|
|
|
* @param string $pattern |
199
|
|
|
*/ |
200
|
|
|
|
201
|
5 |
|
public function forget($method, $pattern) |
202
|
|
|
{ |
203
|
5 |
|
if (strpos($pattern, "{") === false) { |
204
|
5 |
|
unset($this->statics[$method][$pattern]); |
205
|
5 |
|
} else unset($this->dynamics[$this->getDynamicIndex($method, $pattern)][$pattern]); |
206
|
5 |
|
} |
207
|
|
|
|
208
|
|
|
/** |
209
|
|
|
* Determine if the http method is valid. |
210
|
|
|
* |
211
|
|
|
* @param string $method |
212
|
|
|
* @throws MethodNotSupportedException |
213
|
|
|
* @return string |
214
|
|
|
*/ |
215
|
|
|
|
216
|
39 |
|
protected function parseMethod($method) |
217
|
|
|
{ |
218
|
39 |
|
$method = strtolower($method); |
219
|
|
|
|
220
|
39 |
|
if (strpos(self::HTTP_METHODS, $method) === false) { |
221
|
1 |
|
throw new MethodNotSupportedException($method); |
222
|
|
|
} |
223
|
|
|
|
224
|
38 |
|
return $method; |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Separate routes pattern with optional parts into n new patterns. |
229
|
|
|
* |
230
|
|
|
* @param string $pattern |
231
|
|
|
* @return array |
232
|
|
|
*/ |
233
|
|
|
|
234
|
38 |
|
protected function parsePattern($pattern) |
235
|
|
|
{ |
236
|
38 |
|
$withoutClosing = rtrim($pattern, "]"); |
237
|
38 |
|
$closingNumber = strlen($pattern) - strlen($withoutClosing); |
238
|
|
|
|
239
|
38 |
|
$segments = preg_split("~" . self::DYNAMIC_REGEX . "(*SKIP)(*F)|\[~x", $withoutClosing); |
240
|
38 |
|
$this->parseSegments($segments, $closingNumber, $withoutClosing); |
241
|
|
|
|
242
|
36 |
|
return $this->buildSegments($segments); |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Parse all the possible patterns seeking for an incorrect or incompatible pattern. |
247
|
|
|
* |
248
|
|
|
* @param string[] $segments Segments are all the possible patterns made on top of a pattern with optional segments. |
249
|
|
|
* @param int $closingNumber The count of optional segments. |
250
|
|
|
* @param string $withoutClosing The pattern without the closing token of an optional segment. aka: ] |
251
|
|
|
* |
252
|
|
|
* @throws BadRouteException |
253
|
|
|
*/ |
254
|
|
|
|
255
|
38 |
|
protected function parseSegments(array $segments, $closingNumber, $withoutClosing) |
256
|
|
|
{ |
257
|
38 |
|
if ($closingNumber !== count($segments) - 1) { |
258
|
2 |
|
if (preg_match("~" . self::DYNAMIC_REGEX . "(*SKIP)(*F)|\]~x", $withoutClosing)) { |
259
|
1 |
|
throw new BadRouteException(BadRouteException::OPTIONAL_SEGMENTS_ON_MIDDLE); |
260
|
1 |
|
} else throw new BadRouteException(BadRouteException::UNCLOSED_OPTIONAL_SEGMENTS); |
261
|
|
|
} |
262
|
36 |
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* @param string[] $segments |
266
|
|
|
* |
267
|
|
|
* @throws BadRouteException |
268
|
|
|
* @return array |
269
|
|
|
*/ |
270
|
|
|
|
271
|
36 |
|
protected function buildSegments(array $segments) |
272
|
|
|
{ |
273
|
36 |
|
$pattern = ""; |
274
|
36 |
|
$patterns = []; |
275
|
36 |
|
$wildcardTokens = array_keys($this->wildcards); |
276
|
36 |
|
$wildcardRegex = $this->wildcards; |
277
|
|
|
|
278
|
36 |
|
foreach ($segments as $n => $segment) { |
279
|
36 |
|
if ($segment === "" && $n !== 0) { |
280
|
1 |
|
throw new BadRouteException(BadRouteException::EMPTY_OPTIONAL_PARTS); |
281
|
|
|
} |
282
|
|
|
|
283
|
36 |
|
$patterns[] = $pattern .= str_replace($wildcardTokens, $wildcardRegex, $segment); |
284
|
36 |
|
} |
285
|
|
|
|
286
|
35 |
|
return $patterns; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* @param string $method |
291
|
|
|
* @param string $pattern |
292
|
|
|
* |
293
|
|
|
* @return Route|false |
294
|
|
|
*/ |
295
|
|
|
|
296
|
34 |
|
public function findStaticRoute($method, $pattern) |
297
|
|
|
{ |
298
|
34 |
|
$method = strtolower($method); |
299
|
34 |
|
if (isset($this->statics[$method]) && isset($this->statics[$method][$pattern])) |
300
|
34 |
|
return $this->statics[$method][$pattern]; |
301
|
28 |
|
return false; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
/** |
305
|
|
|
* @param string $method |
306
|
|
|
* @param string $pattern |
307
|
|
|
* |
308
|
|
|
* @return array|false |
309
|
|
|
*/ |
310
|
|
|
|
311
|
28 |
|
public function findDynamicRoutes($method, $pattern) |
312
|
|
|
{ |
313
|
28 |
|
$index = $this->getDynamicIndex($method, $pattern); |
314
|
28 |
|
return isset($this->dynamics[$index]) ? $this->dynamics[$index] : false; |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
/** |
318
|
|
|
* @param string $method |
319
|
|
|
* @param string $pattern |
320
|
|
|
* |
321
|
|
|
* @return int |
322
|
|
|
*/ |
323
|
|
|
|
324
|
28 |
|
protected function getDynamicIndex($method, $pattern) |
325
|
|
|
{ |
326
|
28 |
|
return crc32(strtolower($method)) + substr_count($pattern, "/"); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
/** |
330
|
|
|
* @return string[] |
331
|
|
|
*/ |
332
|
|
|
|
333
|
7 |
|
public function getWildcards() |
334
|
|
|
{ |
335
|
7 |
|
$wildcards = []; |
336
|
7 |
|
foreach ($this->wildcards as $token => $regex) |
337
|
7 |
|
$wildcards[substr($token, 1)] = substr($regex, 1); |
338
|
7 |
|
return $wildcards; |
339
|
|
|
} |
340
|
|
|
|
341
|
|
|
/** |
342
|
|
|
* @return string[] |
343
|
|
|
*/ |
344
|
|
|
|
345
|
1 |
|
public function getWildcardTokens() |
346
|
|
|
{ |
347
|
1 |
|
return $this->wildcards; |
348
|
|
|
} |
349
|
|
|
|
350
|
|
|
/** |
351
|
|
|
* @param string $wildcard |
352
|
|
|
* @return string|null |
353
|
|
|
*/ |
354
|
|
|
|
355
|
|
|
public function getWildcard($wildcard) |
356
|
|
|
{ |
357
|
|
|
return isset($this->wildcards[":$wildcard"]) ? $this->wildcards[":$wildcard"] : null; |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* @param string $wildcard |
362
|
|
|
* @param string $pattern |
363
|
|
|
*/ |
364
|
|
|
|
365
|
1 |
|
public function setWildcard($wildcard, $pattern) |
366
|
|
|
{ |
367
|
1 |
|
$this->wildcards[":$wildcard"] = ":$pattern"; |
368
|
1 |
|
} |
369
|
|
|
|
370
|
|
|
} |
371
|
|
|
|
The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.
The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.
To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.