1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Spiral\Router; |
6
|
|
|
|
7
|
|
|
use Psr\Container\ContainerInterface; |
8
|
|
|
use Psr\EventDispatcher\EventDispatcherInterface; |
9
|
|
|
use Psr\Http\Message\ResponseInterface; |
10
|
|
|
use Psr\Http\Message\ServerRequestInterface; |
11
|
|
|
use Psr\Http\Message\UriInterface; |
12
|
|
|
use Spiral\Router\Event\RouteMatched; |
13
|
|
|
use Spiral\Router\Event\RouteNotFound; |
14
|
|
|
use Spiral\Router\Event\Routing; |
15
|
|
|
use Spiral\Router\Exception\RouteException; |
16
|
|
|
use Spiral\Router\Exception\RouteNotFoundException; |
17
|
|
|
use Spiral\Router\Exception\RouterException; |
18
|
|
|
use Spiral\Router\Exception\UndefinedRouteException; |
19
|
|
|
use Spiral\Router\Loader\Configurator\RoutingConfigurator; |
20
|
|
|
use Spiral\Router\Target\AbstractTarget; |
21
|
|
|
use Spiral\Telemetry\NullTracer; |
22
|
|
|
use Spiral\Telemetry\SpanInterface; |
23
|
|
|
use Spiral\Telemetry\TracerInterface; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Manages set of routes. |
27
|
|
|
* |
28
|
|
|
* @psalm-import-type Matches from UriHandler |
29
|
|
|
*/ |
30
|
|
|
final class Router implements RouterInterface |
31
|
|
|
{ |
32
|
|
|
// attribute to store active route in request |
33
|
|
|
public const ROUTE_ATTRIBUTE = 'route'; |
34
|
|
|
|
35
|
|
|
// attribute to store active route in request |
36
|
|
|
public const ROUTE_NAME = 'routeName'; |
37
|
|
|
|
38
|
|
|
// attribute to store active route in request |
39
|
|
|
public const ROUTE_MATCHES = 'matches'; |
40
|
|
|
|
41
|
|
|
private string $basePath = '/'; |
42
|
|
|
|
43
|
|
|
/** @var RouteInterface[] */ |
44
|
|
|
private array $routes = []; |
45
|
|
|
|
46
|
|
|
private ?RouteInterface $default = null; |
47
|
|
|
|
48
|
487 |
|
public function __construct( |
49
|
|
|
string $basePath, |
50
|
|
|
private readonly UriHandler $uriHandler, |
51
|
|
|
private readonly ContainerInterface $container, |
52
|
|
|
private readonly ?EventDispatcherInterface $eventDispatcher = null, |
53
|
|
|
private readonly TracerInterface $tracer = new NullTracer(), |
54
|
|
|
) { |
55
|
487 |
|
$this->basePath = '/' . \ltrim($basePath, '/'); |
56
|
|
|
} |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* @throws RouteNotFoundException |
60
|
|
|
* @throws RouterException |
61
|
|
|
*/ |
62
|
64 |
|
public function handle(ServerRequestInterface $request): ResponseInterface |
63
|
|
|
{ |
64
|
64 |
|
$this->eventDispatcher?->dispatch(new Routing($request)); |
65
|
|
|
|
66
|
64 |
|
return $this->tracer->trace( |
67
|
64 |
|
name: 'Routing', |
68
|
64 |
|
callback: function (SpanInterface $span) use ($request) { |
69
|
|
|
try { |
70
|
64 |
|
$route = $this->matchRoute($request, $routeName); |
71
|
|
|
} catch (RouteException $e) { |
72
|
|
|
throw new RouterException('Invalid route definition', $e->getCode(), $e); |
73
|
|
|
} |
74
|
|
|
|
75
|
64 |
|
if ($route === null) { |
76
|
12 |
|
$this->eventDispatcher?->dispatch(new RouteNotFound($request)); |
77
|
12 |
|
throw new RouteNotFoundException($request->getUri()); |
78
|
|
|
} |
79
|
|
|
|
80
|
53 |
|
$span |
81
|
53 |
|
->setAttribute('request.uri', (string)$request->getUri()) |
82
|
53 |
|
->setAttribute('route.name', $routeName) |
83
|
53 |
|
->setAttribute('route.matches', $route->getMatches() ?? []); |
84
|
|
|
|
85
|
53 |
|
$request = $request |
86
|
53 |
|
->withAttribute(self::ROUTE_ATTRIBUTE, $route) |
87
|
53 |
|
->withAttribute(self::ROUTE_NAME, $routeName) |
88
|
53 |
|
->withAttribute(self::ROUTE_MATCHES, $route->getMatches() ?? []); |
89
|
|
|
|
90
|
53 |
|
$this->eventDispatcher?->dispatch(new RouteMatched($request, $route)); |
91
|
|
|
|
92
|
53 |
|
return $route->handle($request); |
93
|
64 |
|
} |
94
|
64 |
|
); |
95
|
|
|
} |
96
|
|
|
|
97
|
429 |
|
public function setRoute(string $name, RouteInterface $route): void |
98
|
|
|
{ |
99
|
|
|
// each route must inherit basePath prefix |
100
|
429 |
|
$this->routes[$name] = $this->configure($route); |
101
|
|
|
} |
102
|
|
|
|
103
|
363 |
|
public function setDefault(RouteInterface $route): void |
104
|
|
|
{ |
105
|
363 |
|
$this->default = $this->configure($route); |
106
|
|
|
} |
107
|
|
|
|
108
|
36 |
|
public function getRoute(string $name): RouteInterface |
109
|
|
|
{ |
110
|
36 |
|
if (isset($this->routes[$name])) { |
111
|
32 |
|
return $this->routes[$name]; |
112
|
|
|
} |
113
|
|
|
|
114
|
9 |
|
throw new UndefinedRouteException(\sprintf('Undefined route `%s`', $name)); |
115
|
|
|
} |
116
|
|
|
|
117
|
3 |
|
public function getRoutes(): array |
118
|
|
|
{ |
119
|
3 |
|
if (!empty($this->default)) { |
120
|
2 |
|
return $this->routes + [null => $this->default]; |
121
|
|
|
} |
122
|
|
|
|
123
|
1 |
|
return $this->routes; |
124
|
|
|
} |
125
|
|
|
|
126
|
11 |
|
public function uri(string $route, iterable $parameters = []): UriInterface |
127
|
|
|
{ |
128
|
|
|
try { |
129
|
11 |
|
return $this->getRoute($route)->uri($parameters); |
130
|
9 |
|
} catch (UndefinedRouteException) { |
131
|
|
|
//In some cases route name can be provided as controller:action pair, we can try to |
132
|
|
|
//generate such route automatically based on our default/fallback route |
133
|
9 |
|
return $this->castRoute($route)->uri($parameters); |
134
|
|
|
} |
135
|
|
|
} |
136
|
|
|
|
137
|
363 |
|
public function import(RoutingConfigurator $routes): void |
138
|
|
|
{ |
139
|
|
|
/** @var GroupRegistry $groups */ |
140
|
363 |
|
$groups = $this->container->get(GroupRegistry::class); |
141
|
|
|
|
142
|
363 |
|
foreach ($routes->getCollection() as $name => $configurator) { |
143
|
358 |
|
$target = $configurator->target; |
144
|
358 |
|
if ($configurator->core !== null && $target instanceof AbstractTarget) { |
145
|
357 |
|
$target = $target->withCore($configurator->core); |
|
|
|
|
146
|
|
|
} |
147
|
|
|
|
148
|
358 |
|
$pattern = \str_starts_with($configurator->pattern, '//') |
149
|
1 |
|
? $configurator->pattern |
150
|
357 |
|
: \ltrim($configurator->pattern, '/'); |
151
|
358 |
|
$route = new Route($pattern, $target, $configurator->defaults); |
152
|
|
|
|
153
|
358 |
|
if ($configurator->middleware !== null) { |
154
|
357 |
|
$route = $route->withMiddleware(...$configurator->middleware); |
155
|
|
|
} |
156
|
|
|
|
157
|
358 |
|
if ($configurator->methods !== null) { |
158
|
|
|
$route = $route->withVerbs(...$configurator->methods); |
159
|
|
|
} |
160
|
|
|
|
161
|
358 |
|
if (!isset($this->routes[$name]) && $name !== RoutingConfigurator::DEFAULT_ROUTE_NAME) { |
162
|
358 |
|
$group = $groups->getGroup($configurator->group ?? $groups->getDefaultGroup()); |
163
|
358 |
|
$group->setPrefix($configurator->prefix); |
164
|
358 |
|
$group->addRoute($name, $route); |
165
|
|
|
} |
166
|
|
|
|
167
|
358 |
|
if ($name === RoutingConfigurator::DEFAULT_ROUTE_NAME) { |
168
|
357 |
|
$this->setDefault($route); |
169
|
|
|
} |
170
|
|
|
} |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
/** |
174
|
|
|
* Find route matched for given request. |
175
|
|
|
*/ |
176
|
64 |
|
protected function matchRoute(ServerRequestInterface $request, string &$routeName = null): ?RouteInterface |
177
|
|
|
{ |
178
|
64 |
|
foreach ($this->routes as $name => $route) { |
179
|
|
|
// Matched route will return new route instance with matched parameters |
180
|
60 |
|
$matched = $route->match($request); |
181
|
|
|
|
182
|
60 |
|
if ($matched !== null) { |
183
|
39 |
|
$routeName = $name; |
184
|
39 |
|
return $matched; |
185
|
|
|
} |
186
|
|
|
} |
187
|
|
|
|
188
|
26 |
|
if ($this->default !== null) { |
189
|
15 |
|
return $this->default->match($request); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
// unable to match any route |
193
|
11 |
|
return null; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* Configure route with needed dependencies. |
198
|
|
|
*/ |
199
|
434 |
|
protected function configure(RouteInterface $route): RouteInterface |
200
|
|
|
{ |
201
|
434 |
|
if ($route instanceof ContainerizedInterface && !$route->hasContainer()) { |
202
|
|
|
// isolating route in a given container |
203
|
429 |
|
$route = $route->withContainer($this->container); |
204
|
|
|
} |
205
|
|
|
|
206
|
|
|
try { |
207
|
432 |
|
$uriHandler = $route->getUriHandler(); |
208
|
408 |
|
} catch (\Throwable) { |
209
|
408 |
|
$uriHandler = $this->uriHandler; |
210
|
|
|
} |
211
|
|
|
|
212
|
432 |
|
return $route->withUriHandler($uriHandler->withBasePath($this->basePath)); |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
/** |
216
|
|
|
* Locates appropriate route by name. Support dynamic route allocation using following pattern: |
217
|
|
|
* Named route: `name/controller:action` |
218
|
|
|
* Default route: `controller:action` |
219
|
|
|
* Only action: `name/action` |
220
|
|
|
* |
221
|
|
|
* @throws UndefinedRouteException |
222
|
|
|
*/ |
223
|
9 |
|
protected function castRoute(string $route): RouteInterface |
224
|
|
|
{ |
225
|
|
|
if ( |
226
|
9 |
|
!\preg_match( |
227
|
9 |
|
'/^(?:(?P<name>[^\/]+)\/)?(?:(?P<controller>[^:]+):+)?(?P<action>[a-z_\-]+)$/i', |
228
|
9 |
|
$route, |
229
|
9 |
|
$matches |
230
|
9 |
|
) |
231
|
|
|
) { |
232
|
1 |
|
throw new UndefinedRouteException( |
233
|
1 |
|
"Unable to locate route or use default route with 'name/controller:action' pattern" |
234
|
1 |
|
); |
235
|
|
|
} |
236
|
|
|
|
237
|
|
|
/** |
238
|
|
|
* @var Matches $matches |
239
|
|
|
*/ |
240
|
8 |
|
if (!empty($matches['name'])) { |
241
|
5 |
|
$routeObject = $this->getRoute($matches['name']); |
242
|
3 |
|
} elseif ($this->default !== null) { |
243
|
2 |
|
$routeObject = $this->default; |
244
|
|
|
} else { |
245
|
1 |
|
throw new UndefinedRouteException(\sprintf('Unable to locate route candidate for `%s`', $route)); |
246
|
|
|
} |
247
|
|
|
|
248
|
7 |
|
return $routeObject->withDefaults( |
249
|
7 |
|
[ |
250
|
7 |
|
'controller' => $matches['controller'], |
251
|
7 |
|
'action' => $matches['action'], |
252
|
7 |
|
] |
253
|
7 |
|
); |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
This function has been deprecated. The supplier of the function has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.