1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Scabbia2 Router Component |
4
|
|
|
* https://github.com/eserozvataf/scabbia2 |
5
|
|
|
* |
6
|
|
|
* For the full copyright and license information, please view the LICENSE |
7
|
|
|
* file that was distributed with this source code. |
8
|
|
|
* |
9
|
|
|
* @link https://github.com/eserozvataf/scabbia2-router for the canonical source repository |
10
|
|
|
* @copyright 2010-2016 Eser Ozvataf. (http://eser.ozvataf.com/) |
11
|
|
|
* @license http://www.apache.org/licenses/LICENSE-2.0 - Apache License, Version 2.0 |
12
|
|
|
*/ |
13
|
|
|
|
14
|
|
|
namespace Scabbia\Router; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* RouteCollection |
18
|
|
|
* |
19
|
|
|
* @package Scabbia\Router |
20
|
|
|
* @author Eser Ozvataf <[email protected]> |
21
|
|
|
* @since 2.0.0 |
22
|
|
|
* |
23
|
|
|
* Routing related code based on the nikic's FastRoute solution: |
24
|
|
|
* http://nikic.github.io/2014/02/18/Fast-request-routing-using-regular-expressions.html |
25
|
|
|
*/ |
26
|
|
|
class RouteCollection |
27
|
|
|
{ |
28
|
|
|
/** @type string VARIABLE_REGEX Regex expression of variables */ |
29
|
|
|
const VARIABLE_REGEX = <<<'REGEX' |
30
|
|
|
~\{ |
31
|
|
|
\s* ([a-zA-Z][a-zA-Z0-9_]*) \s* |
32
|
|
|
(?: |
33
|
|
|
: \s* ([^{}]*(?:\{(?-1)\}[^{}*])*) |
34
|
|
|
)? |
35
|
|
|
\}~x |
36
|
|
|
REGEX; |
37
|
|
|
|
38
|
|
|
/** @type string DEFAULT_DISPATCH_REGEX Regex expression of default dispatch */ |
39
|
|
|
const DEFAULT_DISPATCH_REGEX = "[^/]+"; |
40
|
|
|
|
41
|
|
|
/** @type string FILTER_VALIDATE_BOOLEAN a symbolic constant for boolean validation */ |
42
|
|
|
const APPROX_CHUNK_SIZE = 10; |
43
|
|
|
|
44
|
|
|
|
45
|
|
|
/** @type array route definitions */ |
46
|
|
|
public $routes = [ |
47
|
|
|
"static" => [], |
48
|
|
|
"variable" => null, |
49
|
|
|
"named" => [] |
50
|
|
|
]; |
51
|
|
|
/** @type array regex to routes map */ |
52
|
|
|
public $regexToRoutesMap = []; |
53
|
|
|
|
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* Parses routes of the following form: |
57
|
|
|
* "/user/{name}/{id:[0-9]+}" |
58
|
|
|
* |
59
|
|
|
* @param string $uRoute route pattern |
60
|
|
|
* |
61
|
|
|
* @return array |
62
|
|
|
*/ |
63
|
|
|
public function parse($uRoute) |
64
|
|
|
{ |
65
|
|
|
if (!preg_match_all(self::VARIABLE_REGEX, $uRoute, $tMatches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { |
66
|
|
|
return [$uRoute]; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
$tOffset = 0; |
70
|
|
|
$tRouteData = []; |
71
|
|
|
foreach ($tMatches as $tMatch) { |
72
|
|
|
if ($tMatch[0][1] > $tOffset) { |
73
|
|
|
$tRouteData[] = substr($uRoute, $tOffset, $tMatch[0][1] - $tOffset); |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
$tRouteData[] = [ |
77
|
|
|
$tMatch[1][0], |
78
|
|
|
isset($tMatch[2]) ? trim($tMatch[2][0]) : self::DEFAULT_DISPATCH_REGEX |
79
|
|
|
]; |
80
|
|
|
|
81
|
|
|
$tOffset = $tMatch[0][1] + strlen($tMatch[0][0]); |
82
|
|
|
} |
83
|
|
|
|
84
|
|
|
if ($tOffset !== strlen($uRoute)) { |
85
|
|
|
$tRouteData[] = substr($uRoute, $tOffset); |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
return $tRouteData; |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* Adds specified route |
93
|
|
|
* |
94
|
|
|
* @param string|array $uMethods http methods |
95
|
|
|
* @param string $uRoute route |
96
|
|
|
* @param callable $uCallback callback |
97
|
|
|
* @param string|null $uName name of route |
98
|
|
|
* |
99
|
|
|
* @return void |
100
|
|
|
*/ |
101
|
|
|
public function addRoute($uMethods, $uRoute, $uCallback, $uName = null) |
102
|
|
|
{ |
103
|
|
|
$tRouteData = $this->parse($uRoute); |
104
|
|
|
$tMethods = (array)$uMethods; |
105
|
|
|
|
106
|
|
|
if (count($tRouteData) === 1 && is_string($tRouteData[0])) { |
107
|
|
|
$this->addStaticRoute($tMethods, $tRouteData, $uCallback, $uName); |
108
|
|
|
} else { |
109
|
|
|
$this->addVariableRoute($tMethods, $tRouteData, $uCallback, $uName); |
110
|
|
|
} |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
/** |
114
|
|
|
* Adds a static route |
115
|
|
|
* |
116
|
|
|
* @param array $uMethods http methods |
117
|
|
|
* @param array $uRouteData route data |
118
|
|
|
* @param callable $uCallback callback |
119
|
|
|
* @param string|null $uName name of route |
120
|
|
|
* |
121
|
|
|
* @throws UnexpectedValueException if an routing problem occurs |
122
|
|
|
* @return void |
123
|
|
|
*/ |
124
|
|
|
public function addStaticRoute(array $uMethods, $uRouteData, $uCallback, $uName = null) |
125
|
|
|
{ |
126
|
|
|
$tRouteStr = $uRouteData[0]; |
127
|
|
|
|
128
|
|
|
foreach ($uMethods as $tMethod) { |
129
|
|
|
if (isset($this->routes["static"][$tRouteStr][$tMethod])) { |
130
|
|
|
throw new UnexpectedValueException(sprintf( |
131
|
|
|
"Cannot register two routes matching \"%s\" for method \"%s\"", |
132
|
|
|
$tRouteStr, |
133
|
|
|
$tMethod |
134
|
|
|
)); |
135
|
|
|
} |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
foreach ($uMethods as $tMethod) { |
139
|
|
|
if (isset($this->regexToRoutesMap[$tMethod])) { |
140
|
|
|
foreach ($this->regexToRoutesMap[$tMethod] as $tRoute) { |
141
|
|
|
if (preg_match("~^{$tRoute["regex"]}$~", $tRouteStr) === 1) { |
142
|
|
|
throw new UnexpectedValueException(sprintf( |
143
|
|
|
"Static route \"%s\" is shadowed by previously defined variable " . |
144
|
|
|
"route \"%s\" for method \"%s\"", |
145
|
|
|
$tRouteStr, |
146
|
|
|
$tRoute["regex"], |
147
|
|
|
$tMethod |
148
|
|
|
)); |
149
|
|
|
} |
150
|
|
|
} |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
$this->routes["static"][$tRouteStr][$tMethod] = $uCallback; |
154
|
|
|
|
155
|
|
|
/* |
|
|
|
|
156
|
|
|
if ($uName !== null) { |
157
|
|
|
if (!isset($this->routes["named"][$tMethod])) { |
158
|
|
|
$this->routes["named"][$tMethod] = []; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
$this->routes["named"][$tMethod][$uName] = [$tRouteStr, []]; |
162
|
|
|
} |
163
|
|
|
*/ |
164
|
|
View Code Duplication |
if ($uName !== null && !isset($this->routes["named"][$uName])) { |
|
|
|
|
165
|
|
|
$this->routes["named"][$uName] = [$tRouteStr, []]; |
166
|
|
|
} |
167
|
|
|
} |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* Adds a variable route |
172
|
|
|
* |
173
|
|
|
* @param array $uMethods http method |
174
|
|
|
* @param array $uRouteData route data |
175
|
|
|
* @param callable $uCallback callback |
176
|
|
|
* @param string|null $uName name of route |
177
|
|
|
* |
178
|
|
|
* @throws UnexpectedValueException if an routing problem occurs |
179
|
|
|
* @return void |
180
|
|
|
*/ |
181
|
|
|
public function addVariableRoute(array $uMethods, $uRouteData, $uCallback, $uName = null) |
182
|
|
|
{ |
183
|
|
|
$tRegex = ""; |
184
|
|
|
$tReverseRegex = ""; |
185
|
|
|
$tVariables = []; |
186
|
|
|
|
187
|
|
|
foreach ($uRouteData as $tPart) { |
188
|
|
|
if (is_string($tPart)) { |
189
|
|
|
$tRegex .= preg_quote($tPart, "~"); |
190
|
|
|
$tReverseRegex .= preg_quote($tPart, "~"); |
191
|
|
|
continue; |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
list($tVariableName, $tRegexPart) = $tPart; |
195
|
|
|
|
196
|
|
|
if (isset($tVariables[$tVariableName])) { |
197
|
|
|
throw new UnexpectedValueException(sprintf( |
198
|
|
|
"Cannot use the same placeholder \"%s\" twice", |
199
|
|
|
$tVariableName |
200
|
|
|
)); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
$tVariables[$tVariableName] = $tVariableName; |
204
|
|
|
$tRegex .= "({$tRegexPart})"; |
205
|
|
|
$tReverseRegex .= "{{$tVariableName}}"; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
foreach ($uMethods as $tMethod) { |
209
|
|
|
if (isset($this->regexToRoutesMap[$tMethod][$tRegex])) { |
210
|
|
|
throw new UnexpectedValueException( |
211
|
|
|
sprintf("Cannot register two routes matching \"%s\" for method \"%s\"", $tRegex, $tMethod) |
212
|
|
|
); |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
|
216
|
|
|
foreach ($uMethods as $tMethod) { |
217
|
|
|
if (!isset($this->regexToRoutesMap[$tMethod])) { |
218
|
|
|
$this->regexToRoutesMap[$tMethod] = []; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
$this->regexToRoutesMap[$tMethod][$tRegex] = [ |
222
|
|
|
// "method" => $tMethod, |
|
|
|
|
223
|
|
|
"callback" => $uCallback, |
224
|
|
|
"regex" => $tRegex, |
225
|
|
|
"variables" => $tVariables |
226
|
|
|
]; |
227
|
|
|
|
228
|
|
|
/* |
|
|
|
|
229
|
|
|
if ($uName !== null) { |
230
|
|
|
if (!isset($this->routes["named"][$tMethod])) { |
231
|
|
|
$this->routes["named"][$tMethod] = []; |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
$this->routes["named"][$tMethod][$uName] = [$tRegex, $tVariables]; |
235
|
|
|
} |
236
|
|
|
*/ |
237
|
|
View Code Duplication |
if ($uName !== null && !isset($this->routes["named"][$uName])) { |
|
|
|
|
238
|
|
|
$this->routes["named"][$uName] = [$tReverseRegex, array_values($tVariables)]; |
239
|
|
|
} |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
$this->routes["variable"] = null; |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
/** |
246
|
|
|
* Combines all route data |
247
|
|
|
* |
248
|
|
|
* @return void |
249
|
|
|
*/ |
250
|
|
|
public function compile() |
251
|
|
|
{ |
252
|
|
|
$this->routes["variable"] = []; |
253
|
|
|
foreach ($this->regexToRoutesMap as $tMethod => $tRegexToRoutesMapOfMethod) { |
254
|
|
|
$tRegexToRoutesMapOfMethodCount = count($tRegexToRoutesMapOfMethod); |
255
|
|
|
|
256
|
|
|
$tNumParts = max(1, round($tRegexToRoutesMapOfMethodCount / self::APPROX_CHUNK_SIZE)); |
257
|
|
|
$tChunkSize = ceil($tRegexToRoutesMapOfMethodCount / $tNumParts); |
258
|
|
|
|
259
|
|
|
$tChunks = array_chunk($tRegexToRoutesMapOfMethod, $tChunkSize, true); |
260
|
|
|
$this->routes["variable"][$tMethod] = array_map([$this, "processChunk"], $tChunks); |
261
|
|
|
} |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
/** |
265
|
|
|
* Returns route information in order to store it |
266
|
|
|
* |
267
|
|
|
* @return array |
268
|
|
|
*/ |
269
|
|
|
public function save() |
270
|
|
|
{ |
271
|
|
|
if ($this->routes["variable"] === null) { |
272
|
|
|
$this->compile(); |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
return $this->routes; |
276
|
|
|
} |
277
|
|
|
|
278
|
|
|
/** |
279
|
|
|
* Splits variable routes into chunks |
280
|
|
|
* |
281
|
|
|
* @param array $uRegexToRoutesMap route definitions |
282
|
|
|
* |
283
|
|
|
* @return array chunked |
284
|
|
|
*/ |
285
|
|
|
protected function processChunk(array $uRegexToRoutesMap) |
286
|
|
|
{ |
287
|
|
|
$tRouteMap = []; |
288
|
|
|
$tRegexes = []; |
289
|
|
|
$tNumGroups = 0; |
290
|
|
|
|
291
|
|
|
foreach ($uRegexToRoutesMap as $tRegex => $tRoute) { |
292
|
|
|
$tNumVariables = count($tRoute["variables"]); |
293
|
|
|
$tNumGroups = max($tNumGroups, $tNumVariables); |
294
|
|
|
|
295
|
|
|
$tRegexes[] = $tRegex . str_repeat("()", $tNumGroups - $tNumVariables); |
296
|
|
|
$tRouteMap[$tNumGroups + 1] = [$tRoute["callback"], $tRoute["variables"]]; |
297
|
|
|
|
298
|
|
|
++$tNumGroups; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
return [ |
302
|
|
|
"regex" => "~^(?|" . implode("|", $tRegexes) . ")$~", |
303
|
|
|
"routeMap" => $tRouteMap |
304
|
|
|
]; |
305
|
|
|
} |
306
|
|
|
} |
307
|
|
|
|
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.