|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* Spiral Framework. |
|
4
|
|
|
* |
|
5
|
|
|
* @license MIT |
|
6
|
|
|
* @author Anton Titov (Wolfy-J) |
|
7
|
|
|
*/ |
|
8
|
|
|
namespace Spiral\Http\Routing; |
|
9
|
|
|
|
|
10
|
|
|
use Cocur\Slugify\Slugify; |
|
11
|
|
|
use Cocur\Slugify\SlugifyInterface; |
|
12
|
|
|
use Psr\Http\Message\ResponseInterface as Response; |
|
13
|
|
|
use Psr\Http\Message\ServerRequestInterface as Request; |
|
14
|
|
|
use Spiral\Core\ContainerInterface; |
|
15
|
|
|
use Spiral\Core\Exceptions\ControllerException; |
|
16
|
|
|
use Spiral\Core\HMVC\CoreInterface; |
|
17
|
|
|
use Spiral\Http\Exceptions\ClientException; |
|
18
|
|
|
use Spiral\Http\MiddlewareInterface; |
|
19
|
|
|
use Spiral\Http\MiddlewarePipeline; |
|
20
|
|
|
use Spiral\Http\Uri; |
|
21
|
|
|
|
|
22
|
|
|
/** |
|
23
|
|
|
* Base for all spiral routes. |
|
24
|
|
|
* |
|
25
|
|
|
* Routing format (examples given in context of Core->bootstrap() method and Route): |
|
26
|
|
|
* |
|
27
|
|
|
* Static routes. |
|
28
|
|
|
* $this->http->route('profile-<id>', 'Controllers\UserController::showProfile'); |
|
29
|
|
|
* |
|
30
|
|
|
* Dynamic actions: |
|
31
|
|
|
* $this->http->route('account/<action>', 'Controllers\AccountController::<action>'); |
|
32
|
|
|
* |
|
33
|
|
|
* Optional segments: |
|
34
|
|
|
* $this->http->route('profile[/<id>]', 'Controllers\UserController::showProfile'); |
|
35
|
|
|
* |
|
36
|
|
|
* This route will react on URL's like /profile/ and /profile/someSegment/ |
|
37
|
|
|
* |
|
38
|
|
|
* To determinate your own pattern for segment use construction <segmentName:pattern> |
|
39
|
|
|
* $this->http->route('profile[/<id:\d+>]', 'Controllers\UserController::showProfile'); |
|
40
|
|
|
* |
|
41
|
|
|
* Will react only on /profile/ and /profile/1384978/ |
|
42
|
|
|
* |
|
43
|
|
|
* You can use custom pattern for controller and action segments. |
|
44
|
|
|
* $this->http->route('users[/<action:edit|save|open>]', 'Controllers\UserController::<action>'); |
|
45
|
|
|
* |
|
46
|
|
|
* Routes can be applied to URI host. |
|
47
|
|
|
* $this->http->route( |
|
48
|
|
|
* '<username>.domain.com[/<action>[/<id>]]', |
|
49
|
|
|
* 'Controllers\UserController::<action>' |
|
50
|
|
|
* )->useHost(); |
|
51
|
|
|
* |
|
52
|
|
|
* Routes can be used non only with controllers (no idea why you may need it): |
|
53
|
|
|
* $this->http->route('users', function () { |
|
54
|
|
|
* return "This is users route."; |
|
55
|
|
|
* }); |
|
56
|
|
|
*/ |
|
57
|
|
|
abstract class AbstractRoute implements RouteInterface |
|
58
|
|
|
{ |
|
59
|
|
|
/** |
|
60
|
|
|
* Default segment pattern, this patter can be applied to controller names, actions and etc. |
|
61
|
|
|
*/ |
|
62
|
|
|
const DEFAULT_SEGMENT = '[^\.\/]+'; |
|
63
|
|
|
|
|
64
|
|
|
/** |
|
65
|
|
|
* To execute actions. |
|
66
|
|
|
* |
|
67
|
|
|
* @invisible |
|
68
|
|
|
* @var CoreInterface |
|
69
|
|
|
*/ |
|
70
|
|
|
protected $core = null; |
|
71
|
|
|
|
|
72
|
|
|
/** |
|
73
|
|
|
* @var string |
|
74
|
|
|
*/ |
|
75
|
|
|
protected $name = ''; |
|
76
|
|
|
|
|
77
|
|
|
/** |
|
78
|
|
|
* @var array |
|
79
|
|
|
*/ |
|
80
|
|
|
protected $middlewares = []; |
|
81
|
|
|
|
|
82
|
|
|
/** |
|
83
|
|
|
* Route pattern includes simplified regular expressing later compiled to real regexp. |
|
84
|
|
|
* |
|
85
|
|
|
* @var string |
|
86
|
|
|
*/ |
|
87
|
|
|
protected $pattern = ''; |
|
88
|
|
|
|
|
89
|
|
|
/** |
|
90
|
|
|
* Default set of values to fill route matches and target pattern (if specified as pattern). |
|
91
|
|
|
* |
|
92
|
|
|
* @var array |
|
93
|
|
|
*/ |
|
94
|
|
|
protected $defaults = []; |
|
95
|
|
|
|
|
96
|
|
|
/** |
|
97
|
|
|
* If true route will be matched with URI host in addition to path. BasePath will be ignored. |
|
98
|
|
|
* |
|
99
|
|
|
* @var bool |
|
100
|
|
|
*/ |
|
101
|
|
|
protected $withHost = false; |
|
102
|
|
|
|
|
103
|
|
|
/** |
|
104
|
|
|
* Compiled route options, pattern and etc. Internal data. |
|
105
|
|
|
* |
|
106
|
|
|
* @invisible |
|
107
|
|
|
* @var array |
|
108
|
|
|
*/ |
|
109
|
|
|
protected $compiled = []; |
|
110
|
|
|
|
|
111
|
|
|
/** |
|
112
|
|
|
* Route matches, populated after match() method executed. Internal. |
|
113
|
|
|
* |
|
114
|
|
|
* @todo not sure if it's good idea to store matches in route? |
|
115
|
|
|
* @var array |
|
116
|
|
|
*/ |
|
117
|
|
|
protected $matches = []; |
|
118
|
|
|
|
|
119
|
|
|
/** |
|
120
|
|
|
* @param CoreInterface $core |
|
121
|
|
|
* @return $this |
|
122
|
|
|
*/ |
|
123
|
|
|
public function setCore(CoreInterface $core) |
|
124
|
|
|
{ |
|
125
|
|
|
$this->core = $core; |
|
126
|
|
|
|
|
127
|
|
|
return $this; |
|
128
|
|
|
} |
|
129
|
|
|
|
|
130
|
|
|
/** |
|
131
|
|
|
* {@inheritdoc} |
|
132
|
|
|
*/ |
|
133
|
|
|
public function getName() |
|
134
|
|
|
{ |
|
135
|
|
|
return $this->name; |
|
136
|
|
|
} |
|
137
|
|
|
|
|
138
|
|
|
/** |
|
139
|
|
|
* Declared route pattern. |
|
140
|
|
|
* |
|
141
|
|
|
* @return string |
|
142
|
|
|
*/ |
|
143
|
|
|
public function getPattern() |
|
144
|
|
|
{ |
|
145
|
|
|
return $this->pattern; |
|
146
|
|
|
} |
|
147
|
|
|
|
|
148
|
|
|
/** |
|
149
|
|
|
* If true (default) route will be matched against path + URI host. |
|
150
|
|
|
* |
|
151
|
|
|
* @param bool $withHost |
|
152
|
|
|
* @return $this |
|
153
|
|
|
*/ |
|
154
|
|
|
public function matchHost($withHost = true) |
|
155
|
|
|
{ |
|
156
|
|
|
$this->withHost = $withHost; |
|
157
|
|
|
|
|
158
|
|
|
return $this; |
|
159
|
|
|
} |
|
160
|
|
|
|
|
161
|
|
|
/** |
|
162
|
|
|
* Update route defaults (new values will be merged with existed data). |
|
163
|
|
|
* |
|
164
|
|
|
* @deprecated User withDefault method |
|
165
|
|
|
* @param array $defaults |
|
166
|
|
|
* @return $this |
|
167
|
|
|
*/ |
|
168
|
|
|
public function defaults(array $defaults) |
|
169
|
|
|
{ |
|
170
|
|
|
$this->defaults = $defaults + $this->defaults; |
|
171
|
|
|
|
|
172
|
|
|
return $this; |
|
173
|
|
|
} |
|
174
|
|
|
|
|
175
|
|
|
/** |
|
176
|
|
|
* {@inheritdoc} |
|
177
|
|
|
*/ |
|
178
|
|
|
public function withDefaults($name, array $matches) |
|
179
|
|
|
{ |
|
180
|
|
|
$copy = clone $this; |
|
181
|
|
|
$copy->name = (string)$name; |
|
182
|
|
|
$copy->defaults($matches); |
|
|
|
|
|
|
183
|
|
|
|
|
184
|
|
|
return $copy; |
|
185
|
|
|
} |
|
186
|
|
|
|
|
187
|
|
|
/** |
|
188
|
|
|
* Associated middleware with route. |
|
189
|
|
|
* |
|
190
|
|
|
* Example: |
|
191
|
|
|
* $route->with(new CacheMiddleware(100)); |
|
192
|
|
|
* $route->with(ProxyMiddleware::class); |
|
193
|
|
|
* $route->with([ProxyMiddleware::class, OtherMiddleware::class]); |
|
194
|
|
|
* |
|
195
|
|
|
* @param callable|MiddlewareInterface|array $middleware |
|
196
|
|
|
* @return $this |
|
197
|
|
|
*/ |
|
198
|
|
|
public function middleware($middleware) |
|
199
|
|
|
{ |
|
200
|
|
|
if (is_array($middleware)) { |
|
201
|
|
|
$this->middlewares = array_merge($this->middlewares, $middleware); |
|
202
|
|
|
} else { |
|
203
|
|
|
$this->middlewares[] = $middleware; |
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
return $this; |
|
207
|
|
|
} |
|
208
|
|
|
|
|
209
|
|
|
/** |
|
210
|
|
|
* {@inheritdoc} |
|
211
|
|
|
*/ |
|
212
|
|
|
public function match(Request $request, $basePath = '/') |
|
213
|
|
|
{ |
|
214
|
|
|
if (empty($this->compiled)) { |
|
215
|
|
|
$this->compile(); |
|
216
|
|
|
} |
|
217
|
|
|
|
|
218
|
|
|
$path = $request->getUri()->getPath(); |
|
219
|
|
|
if (empty($path) || $path[0] !== '/') { |
|
220
|
|
|
$path = '/' . $path; |
|
221
|
|
|
} |
|
222
|
|
|
|
|
223
|
|
|
if ($this->withHost) { |
|
224
|
|
|
$uri = $request->getUri()->getHost() . $path; |
|
225
|
|
|
} else { |
|
226
|
|
|
$uri = substr($path, strlen($basePath)); |
|
227
|
|
|
} |
|
228
|
|
|
|
|
229
|
|
|
if (preg_match($this->compiled['pattern'], rtrim($uri, '/'), $matches)) { |
|
230
|
|
|
$route = clone $this; |
|
231
|
|
|
|
|
232
|
|
|
$route->matches = $matches; |
|
233
|
|
|
|
|
234
|
|
|
//To get only named matches |
|
235
|
|
|
$route->matches = array_intersect_key($route->matches, $route->compiled['options']); |
|
236
|
|
|
$route->matches = array_merge( |
|
237
|
|
|
$route->compiled['options'], |
|
238
|
|
|
$route->defaults, |
|
239
|
|
|
$route->matches |
|
240
|
|
|
); |
|
241
|
|
|
|
|
242
|
|
|
return $route; |
|
243
|
|
|
} |
|
244
|
|
|
|
|
245
|
|
|
return false; |
|
|
|
|
|
|
246
|
|
|
} |
|
247
|
|
|
|
|
248
|
|
|
/** |
|
249
|
|
|
* {@inheritdoc} |
|
250
|
|
|
*/ |
|
251
|
|
|
public function perform(Request $request, Response $response, ContainerInterface $container) |
|
252
|
|
|
{ |
|
253
|
|
|
$pipeline = new MiddlewarePipeline($this->middlewares, $container); |
|
254
|
|
|
|
|
255
|
|
|
return $pipeline->target($this->createEndpoint($container))->run($request, $response); |
|
256
|
|
|
} |
|
257
|
|
|
|
|
258
|
|
|
/** |
|
259
|
|
|
* {@inheritdoc} |
|
260
|
|
|
* |
|
261
|
|
|
* @todo need improvement |
|
262
|
|
|
*/ |
|
263
|
|
|
public function uri( |
|
264
|
|
|
$parameters = [], |
|
265
|
|
|
$basePath = '/', |
|
266
|
|
|
SlugifyInterface $slugify = null |
|
267
|
|
|
) { |
|
268
|
|
|
if (empty($this->compiled)) { |
|
269
|
|
|
$this->compile(); |
|
270
|
|
|
} |
|
271
|
|
|
|
|
272
|
|
|
if (empty($slugify)) { |
|
273
|
|
|
$slugify = new Slugify(); |
|
274
|
|
|
} |
|
275
|
|
|
|
|
276
|
|
|
$parameters = $this->fetchParameters($parameters, $slugify); |
|
277
|
|
|
$parameters = $parameters + $this->matches + $this->defaults + $this->compiled['options']; |
|
278
|
|
|
|
|
279
|
|
|
//Uri without empty blocks (pretty stupid implementation) |
|
280
|
|
|
$path = strtr( |
|
281
|
|
|
\Spiral\interpolate($this->compiled['template'], $parameters, '<', '>'), |
|
282
|
|
|
['[]' => '', '[/]' => '', '[' => '', ']' => '', '//' => '/'] |
|
283
|
|
|
); |
|
284
|
|
|
|
|
285
|
|
|
$uri = new Uri( |
|
286
|
|
|
($this->withHost ? '' : $basePath) . rtrim($path, '/') |
|
287
|
|
|
); |
|
288
|
|
|
|
|
289
|
|
|
//Getting additional query parameters |
|
290
|
|
|
if (!empty($queryParameters = array_diff_key($parameters, $this->compiled['options']))) { |
|
291
|
|
|
$uri = $uri->withQuery(http_build_query($queryParameters)); |
|
292
|
|
|
} |
|
293
|
|
|
|
|
294
|
|
|
return $uri; |
|
295
|
|
|
} |
|
296
|
|
|
|
|
297
|
|
|
/** |
|
298
|
|
|
* Generate parameters list. |
|
299
|
|
|
* |
|
300
|
|
|
* @param \Traversable|array $parameters |
|
301
|
|
|
* @param SlugifyInterface $slugify |
|
302
|
|
|
* @return array |
|
303
|
|
|
*/ |
|
304
|
|
|
protected function fetchParameters($parameters, SlugifyInterface $slugify) |
|
305
|
|
|
{ |
|
306
|
|
|
$result = []; |
|
307
|
|
|
$allowed = array_keys($this->compiled['options']); |
|
308
|
|
|
|
|
309
|
|
|
foreach ($parameters as $key => $parameter) { |
|
310
|
|
|
if (!array_key_exists($key, $this->compiled['options'])) { |
|
311
|
|
|
//Numeric key? |
|
312
|
|
|
if (isset($allowed[$key])) { |
|
313
|
|
|
$key = $allowed[$key]; |
|
314
|
|
|
} else { |
|
315
|
|
|
continue; |
|
316
|
|
|
} |
|
317
|
|
|
} |
|
318
|
|
|
|
|
319
|
|
|
if (is_string($parameter) && !preg_match('/^[a-z\-_0-9]+$/i', $parameter)) { |
|
320
|
|
|
//Default Slugify is pretty slow, we'd better not apply it for every value |
|
321
|
|
|
$result[$key] = $slugify->slugify($parameter); |
|
322
|
|
|
continue; |
|
323
|
|
|
} |
|
324
|
|
|
|
|
325
|
|
|
$result[$key] = (string)$parameter; |
|
326
|
|
|
} |
|
327
|
|
|
|
|
328
|
|
|
return $result; |
|
329
|
|
|
} |
|
330
|
|
|
|
|
331
|
|
|
/** |
|
332
|
|
|
* Create callable route endpoint. |
|
333
|
|
|
* |
|
334
|
|
|
* @param ContainerInterface $container |
|
335
|
|
|
* @return callable |
|
336
|
|
|
*/ |
|
337
|
|
|
abstract protected function createEndpoint(ContainerInterface $container); |
|
338
|
|
|
|
|
339
|
|
|
/** |
|
340
|
|
|
* Internal helper used to create execute controller action using associated core instance. |
|
341
|
|
|
* |
|
342
|
|
|
* @param ContainerInterface $container |
|
343
|
|
|
* @param string $controller |
|
344
|
|
|
* @param string $action |
|
345
|
|
|
* @param array $parameters |
|
346
|
|
|
* @return mixed |
|
347
|
|
|
* @throws ClientException |
|
348
|
|
|
*/ |
|
349
|
|
|
protected function callAction( |
|
350
|
|
|
ContainerInterface $container, |
|
351
|
|
|
$controller, |
|
352
|
|
|
$action, |
|
353
|
|
|
array $parameters = [] |
|
354
|
|
|
) { |
|
355
|
|
|
if (empty($this->core)) { |
|
356
|
|
|
$this->core = $container->get(CoreInterface::class); |
|
357
|
|
|
} |
|
358
|
|
|
|
|
359
|
|
|
try { |
|
360
|
|
|
return $this->core->callAction($controller, $action, $parameters); |
|
361
|
|
|
} catch (ControllerException $e) { |
|
362
|
|
|
throw $this->convertException($e); |
|
363
|
|
|
} |
|
364
|
|
|
} |
|
365
|
|
|
|
|
366
|
|
|
/** |
|
367
|
|
|
* Converts controller exceptions into client exceptions. |
|
368
|
|
|
* |
|
369
|
|
|
* @param ControllerException $exception |
|
370
|
|
|
* @return ClientException |
|
371
|
|
|
*/ |
|
372
|
|
|
protected function convertException(ControllerException $exception) |
|
373
|
|
|
{ |
|
374
|
|
|
switch ($exception->getCode()) { |
|
375
|
|
|
case ControllerException::BAD_ACTION: |
|
376
|
|
|
case ControllerException::NOT_FOUND: |
|
377
|
|
|
return new ClientException(ClientException::NOT_FOUND, $exception->getMessage()); |
|
378
|
|
|
case ControllerException::FORBIDDEN: |
|
|
|
|
|
|
379
|
|
|
return new ClientException(ClientException::FORBIDDEN, $exception->getMessage()); |
|
380
|
|
|
default: |
|
381
|
|
|
return new ClientException(ClientException::BAD_DATA, $exception->getMessage()); |
|
382
|
|
|
} |
|
383
|
|
|
} |
|
384
|
|
|
|
|
385
|
|
|
/** |
|
386
|
|
|
* Compile router pattern into valid regexp. |
|
387
|
|
|
*/ |
|
388
|
|
|
private function compile() |
|
389
|
|
|
{ |
|
390
|
|
|
$replaces = ['/' => '\\/', '[' => '(?:', ']' => ')?', '.' => '\.']; |
|
391
|
|
|
|
|
392
|
|
|
$options = []; |
|
393
|
|
|
if (preg_match_all('/<(\w+):?(.*?)?>/', $this->pattern, $matches)) { |
|
394
|
|
|
$variables = array_combine($matches[1], $matches[2]); |
|
395
|
|
|
|
|
396
|
|
|
foreach ($variables as $name => $segment) { |
|
397
|
|
|
//Segment regex |
|
398
|
|
|
$segment = !empty($segment) ? $segment : self::DEFAULT_SEGMENT; |
|
399
|
|
|
$replaces["<$name>"] = "(?P<$name>$segment)"; |
|
400
|
|
|
$options[] = $name; |
|
401
|
|
|
} |
|
402
|
|
|
} |
|
403
|
|
|
|
|
404
|
|
|
$template = preg_replace('/<(\w+):?.*?>/', '<\1>', $this->pattern); |
|
405
|
|
|
|
|
406
|
|
|
$this->compiled = [ |
|
407
|
|
|
'pattern' => '/^' . strtr($template, $replaces) . '$/iu', |
|
408
|
|
|
'template' => stripslashes(str_replace('?', '', $template)), |
|
409
|
|
|
'options' => array_fill_keys($options, null) |
|
410
|
|
|
]; |
|
411
|
|
|
} |
|
412
|
|
|
} |
|
413
|
|
|
|
This method has been deprecated. The supplier of the class has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.