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\MethodNotAllowedException; |
14
|
|
|
use Codeburner\Router\Exceptions\NotFoundException; |
15
|
|
|
use Exception; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* The matcher class find the route for a given http method and path. |
19
|
|
|
* |
20
|
|
|
* @author Alex Rohleder <[email protected]> |
21
|
|
|
*/ |
22
|
|
|
|
23
|
|
|
class Matcher |
24
|
|
|
{ |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var Collector |
28
|
|
|
*/ |
29
|
|
|
|
30
|
|
|
protected $collector; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* Define a basepath to all routes. |
34
|
|
|
* |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
|
38
|
|
|
protected $basepath = ""; |
39
|
|
|
|
40
|
|
|
/** |
41
|
|
|
* Construct the route dispatcher. |
42
|
|
|
* |
43
|
|
|
* @param Collector $collector |
44
|
|
|
* @param string $basepath Define a Path prefix that must be excluded on matches. |
45
|
|
|
*/ |
46
|
|
|
|
47
|
|
|
public function __construct(Collector $collector, $basepath = "") |
48
|
|
|
{ |
49
|
|
|
$this->collector = $collector; |
50
|
|
|
$this->basepath = $basepath; |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Find a route that matches the given arguments. |
55
|
|
|
* |
56
|
|
|
* @param string $httpMethod |
57
|
|
|
* @param string $path |
58
|
|
|
* |
59
|
|
|
* @throws NotFoundException |
60
|
|
|
* @throws MethodNotAllowedException |
61
|
|
|
* |
62
|
|
|
* @return Route |
63
|
|
|
*/ |
64
|
|
|
|
65
|
|
|
public function match($httpMethod, $path) |
66
|
|
|
{ |
67
|
|
|
$path = $this->parsePath($path); |
68
|
|
|
|
69
|
|
|
if ($route = $this->collector->findStaticRoute($httpMethod, $path)) { |
70
|
|
|
return $route; |
|
|
|
|
71
|
|
|
} |
72
|
|
|
|
73
|
|
|
if ($route = $this->matchDynamicRoute($httpMethod, $path)) { |
74
|
|
|
return $route; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
$this->matchSimilarRoute($httpMethod, $path); |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* Find and return the request dynamic route based on the compiled data and Path. |
82
|
|
|
* |
83
|
|
|
* @param string $httpMethod |
84
|
|
|
* @param string $path |
85
|
|
|
* |
86
|
|
|
* @return Route|false If the request match an array with the action and parameters will |
87
|
|
|
* be returned otherwise a false will. |
88
|
|
|
*/ |
89
|
|
|
|
90
|
|
|
protected function matchDynamicRoute($httpMethod, $path) |
91
|
|
|
{ |
92
|
|
|
$routes = $this->collector->findDynamicRoutes($httpMethod, $path); |
93
|
|
|
|
94
|
|
|
if (!$routes) { |
95
|
|
|
return false; |
96
|
|
|
} |
97
|
|
|
|
98
|
|
|
// chunk routes for smaller regex groups using the Sturges' Formula |
99
|
|
|
foreach (array_chunk($routes, round(1 + 3.3 * log(count($routes))), true) as $chunk) |
100
|
|
|
{ |
101
|
|
|
array_map([$this, "buildRoute"], $chunk); |
102
|
|
|
list($pattern, $map) = $this->buildGroup($chunk); |
103
|
|
|
|
104
|
|
|
if (!preg_match($pattern, $path, $matches)) { |
105
|
|
|
continue; |
106
|
|
|
} |
107
|
|
|
|
108
|
|
|
/** @var Route $route */ |
109
|
|
|
$route = $map[count($matches)]; |
110
|
|
|
// removing the Path from array. |
111
|
|
|
unset($matches[0]); |
112
|
|
|
// sometimes null values come with the matches so the array_filter must be called first. |
113
|
|
|
$route->setParams(array_combine($route->getParams(), array_filter($matches))); |
114
|
|
|
// route must know who match them |
115
|
|
|
$route->setMatcher($this); |
116
|
|
|
|
117
|
|
|
return $route; |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
return false; |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Parse the dynamic segments of the pattern and replace then for |
125
|
|
|
* corresponding regex. |
126
|
|
|
* |
127
|
|
|
* @param Route $route |
128
|
|
|
* @return Route |
129
|
|
|
*/ |
130
|
|
|
|
131
|
|
|
protected function buildRoute(Route $route) |
132
|
|
|
{ |
133
|
|
|
if ($route->blocked()) { |
134
|
|
|
return $route; |
135
|
|
|
} |
136
|
|
|
|
137
|
|
|
list($pattern, $params) = $this->parsePlaceholders($route->getPattern()); |
138
|
|
|
return $route->setPatternWithoutReset($pattern)->setParams($params)->block(); |
139
|
|
|
} |
140
|
|
|
|
141
|
|
|
/** |
142
|
|
|
* Group several dynamic routes patterns into one big regex and maps |
143
|
|
|
* the routes to the pattern positions in the big regex. |
144
|
|
|
* |
145
|
|
|
* @param array $routes |
146
|
|
|
* @return array |
147
|
|
|
*/ |
148
|
|
|
|
149
|
|
|
protected function buildGroup(array $routes) |
150
|
|
|
{ |
151
|
|
|
$groupCount = (int) $map = $regex = []; |
152
|
|
|
|
153
|
|
|
/** @var Route $route */ |
154
|
|
|
foreach ($routes as $route) { |
155
|
|
|
$params = $route->getParams(); |
156
|
|
|
$paramsCount = count($params); |
157
|
|
|
$groupCount = max($groupCount, $paramsCount) + 1; |
158
|
|
|
$regex[] = $route->getPattern() . str_repeat("()", $groupCount - $paramsCount - 1); |
159
|
|
|
$map[$groupCount] = $route; |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
return ["~^(?|" . implode("|", $regex) . ")$~", $map]; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Parse an route pattern seeking for parameters and build the route regex. |
167
|
|
|
* |
168
|
|
|
* @param string $pattern |
169
|
|
|
* @return array 0 => new route regex, 1 => map of parameter names |
170
|
|
|
*/ |
171
|
|
|
|
172
|
|
|
protected function parsePlaceholders($pattern) |
173
|
|
|
{ |
174
|
|
|
$params = []; |
175
|
|
|
preg_match_all("~" . Collector::DYNAMIC_REGEX . "~x", $pattern, $matches, PREG_SET_ORDER); |
176
|
|
|
|
177
|
|
|
foreach ((array) $matches as $key => $match) { |
178
|
|
|
$pattern = str_replace($match[0], isset($match[2]) ? "({$match[2]})" : "([^/]+)", $pattern); |
179
|
|
|
$params[$key] = $match[1]; |
180
|
|
|
} |
181
|
|
|
|
182
|
|
|
return [$pattern, $params]; |
183
|
|
|
} |
184
|
|
|
|
185
|
|
|
/** |
186
|
|
|
* Get only the path of a given url. |
187
|
|
|
* |
188
|
|
|
* @param string $path The given URL |
189
|
|
|
* |
190
|
|
|
* @throws Exception |
191
|
|
|
* @return string |
192
|
|
|
*/ |
193
|
|
|
|
194
|
|
|
protected function parsePath($path) |
195
|
|
|
{ |
196
|
|
|
$path = parse_url(substr(strstr(";" . $path, ";" . $this->basepath), strlen(";" . $this->basepath)), PHP_URL_PATH); |
197
|
|
|
|
198
|
|
|
if ($path === false) { |
199
|
|
|
throw new Exception("Seriously malformed URL passed to route dispatcher."); |
200
|
|
|
} |
201
|
|
|
|
202
|
|
|
return $path; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Generate an HTTP error request with method not allowed or not found. |
207
|
|
|
* |
208
|
|
|
* @param string $httpMethod |
209
|
|
|
* @param string $path |
210
|
|
|
* |
211
|
|
|
* @throws NotFoundException |
212
|
|
|
* @throws MethodNotAllowedException |
213
|
|
|
*/ |
214
|
|
|
|
215
|
|
|
protected function matchSimilarRoute($httpMethod, $path) |
216
|
|
|
{ |
217
|
|
|
$sm = $dm = []; |
|
|
|
|
218
|
|
|
|
219
|
|
|
if ($sm = ($this->checkStaticRouteInOtherMethods($httpMethod, $path)) |
220
|
|
|
|| $dm = ($this->checkDynamicRouteInOtherMethods($httpMethod, $path))) { |
221
|
|
|
throw new MethodNotAllowedException($httpMethod, $path, array_merge((array) $sm, (array) $dm)); |
222
|
|
|
} |
223
|
|
|
|
224
|
|
|
throw new NotFoundException($httpMethod, $path); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Verify if a static route match in another method than the requested. |
229
|
|
|
* |
230
|
|
|
* @param string $targetHttpMethod The HTTP method that must not be checked |
231
|
|
|
* @param string $path The Path that must be matched. |
232
|
|
|
* |
233
|
|
|
* @return array |
234
|
|
|
*/ |
235
|
|
|
|
236
|
|
|
protected function checkStaticRouteInOtherMethods($targetHttpMethod, $path) |
237
|
|
|
{ |
238
|
|
|
return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) { |
239
|
|
|
return (bool) $this->collector->findStaticRoute($httpMethod, $path); |
240
|
|
|
}); |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
/** |
244
|
|
|
* Verify if a dynamic route match in another method than the requested. |
245
|
|
|
* |
246
|
|
|
* @param string $targetHttpMethod The HTTP method that must not be checked |
247
|
|
|
* @param string $path The Path that must be matched. |
248
|
|
|
* |
249
|
|
|
* @return array |
250
|
|
|
*/ |
251
|
|
|
|
252
|
|
|
protected function checkDynamicRouteInOtherMethods($targetHttpMethod, $path) |
253
|
|
|
{ |
254
|
|
|
return array_filter($this->getHttpMethodsBut($targetHttpMethod), function ($httpMethod) use ($path) { |
255
|
|
|
return (bool) $this->matchDynamicRoute($httpMethod, $path); |
256
|
|
|
}); |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Strip the given http methods and return all the others. |
261
|
|
|
* |
262
|
|
|
* @param array|string |
263
|
|
|
* @return array |
264
|
|
|
*/ |
265
|
|
|
|
266
|
|
|
protected function getHttpMethodsBut($targetHttpMethod) |
267
|
|
|
{ |
268
|
|
|
return array_diff(explode(" ", Collector::HTTP_METHODS), (array) $targetHttpMethod); |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* @return Collector |
273
|
|
|
*/ |
274
|
|
|
|
275
|
|
|
public function getCollector() |
276
|
|
|
{ |
277
|
|
|
return $this->collector; |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
/** |
281
|
|
|
* @return string |
282
|
|
|
*/ |
283
|
|
|
|
284
|
|
|
public function getBasePath() |
285
|
|
|
{ |
286
|
|
|
return $this->basepath; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* Set a new basepath, this will be a prefix that must be excluded in |
291
|
|
|
* every requested Path. |
292
|
|
|
* |
293
|
|
|
* @param string $basepath The new basepath |
294
|
|
|
*/ |
295
|
|
|
|
296
|
|
|
public function setBasePath($basepath) |
297
|
|
|
{ |
298
|
|
|
$this->basepath = $basepath; |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
} |
302
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.