Passed
Pull Request — master (#816)
by butschster
06:28
created

Router::configure()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 2
nop 1
dl 0
loc 8
ccs 4
cts 4
cp 1
crap 3
rs 10
c 0
b 0
f 0
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
final class Router implements RouterInterface
29
{
30
    // attribute to store active route in request
31
    public const ROUTE_ATTRIBUTE = 'route';
32
33
    // attribute to store active route in request
34
    public const ROUTE_NAME = 'routeName';
35
36
    // attribute to store active route in request
37
    public const ROUTE_MATCHES = 'matches';
38
39
    private string $basePath = '/';
40
41
    /** @var RouteInterface[] */
42
    private array $routes = [];
43
44
    private ?RouteInterface $default = null;
45
46 358
    public function __construct(
47
        string $basePath,
48
        private readonly UriHandler $uriHandler,
49
        private readonly ContainerInterface $container,
50
        private readonly ?EventDispatcherInterface $eventDispatcher = null,
51
        private readonly ?TracerInterface $tracer = new NullTracer(),
52
    ) {
53 358
        $this->basePath = '/' . \ltrim($basePath, '/');
54
    }
55
56
    /**
57
     * @throws RouteNotFoundException
58
     * @throws RouterException
59
     */
60 67
    public function handle(ServerRequestInterface $request): ResponseInterface
61
    {
62 67
        $this->eventDispatcher?->dispatch(new Routing($request));
63
64 67
        return $this->tracer->trace(
0 ignored issues
show
Bug introduced by
The method trace() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

64
        return $this->tracer->/** @scrutinizer ignore-call */ trace(

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
65
            name: 'Routing',
66 67
            callback: function (SpanInterface $span) use ($request) {
67
                try {
68 67
                    $route = $this->matchRoute($request, $routeName);
69
                } catch (RouteException $e) {
70
                    throw new RouterException('Invalid route definition', $e->getCode(), $e);
71
                }
72
73 67
                if ($route === null) {
74 12
                    $this->eventDispatcher?->dispatch(new RouteNotFound($request));
75 12
                    throw new RouteNotFoundException($request->getUri());
76
                }
77
78
                $span
79 56
                    ->setAttribute('request.uri', (string)$request->getUri())
80 56
                    ->setAttribute('route.name', $routeName)
81 56
                    ->setAttribute('route.matches', $route->getMatches() ?? []);
82
83 56
                $request = $request
84 56
                    ->withAttribute(self::ROUTE_ATTRIBUTE, $route)
85 56
                    ->withAttribute(self::ROUTE_NAME, $routeName)
86 56
                    ->withAttribute(self::ROUTE_MATCHES, $route->getMatches() ?? []);
87
88 56
                $this->eventDispatcher?->dispatch(new RouteMatched($request, $route));
89
90 56
                return $route->handle($request);
91
            }
92
        );
93
    }
94
95 309
    public function setRoute(string $name, RouteInterface $route): void
96
    {
97
        // each route must inherit basePath prefix
98 309
        $this->routes[$name] = $this->configure($route);
99
    }
100
101 260
    public function setDefault(RouteInterface $route): void
102
    {
103 260
        $this->default = $this->configure($route);
104
    }
105
106 21
    public function getRoute(string $name): RouteInterface
107
    {
108 21
        if (isset($this->routes[$name])) {
109 17
            return $this->routes[$name];
110
        }
111
112 9
        throw new UndefinedRouteException(\sprintf('Undefined route `%s`', $name));
113
    }
114
115 3
    public function getRoutes(): array
116
    {
117 3
        if (!empty($this->default)) {
118 2
            return $this->routes + [null => $this->default];
119
        }
120
121 1
        return $this->routes;
122
    }
123
124 10
    public function uri(string $route, iterable $parameters = []): UriInterface
125
    {
126
        try {
127 10
            return $this->getRoute($route)->uri($parameters);
128 9
        } catch (UndefinedRouteException) {
129
            //In some cases route name can be provided as controller:action pair, we can try to
130
            //generate such route automatically based on our default/fallback route
131 9
            return $this->castRoute($route)->uri($parameters);
132
        }
133
    }
134
135 260
    public function import(RoutingConfigurator $routes): void
136
    {
137
        /** @var GroupRegistry $groups */
138 260
        $groups = $this->container->get(GroupRegistry::class);
139
140 260
        foreach ($routes->getCollection() as $name => $configurator) {
141 254
            $target = $configurator->target;
142 254
            if ($configurator->core !== null && $target instanceof AbstractTarget) {
143 254
                $target = $target->withCore($configurator->core);
144
            }
145
146 254
            $route = new Route(
147 254
                $configurator->prefix . '/' . \ltrim($configurator->pattern, '/'),
148
                $target,
149 254
                $configurator->defaults
150
            );
151
152 254
            if ($configurator->middleware !== null) {
153 254
                $route = $route->withMiddleware(...$configurator->middleware);
154
            }
155
156 254
            if ($configurator->methods !== null) {
157
                $route = $route->withVerbs(...$configurator->methods);
158
            }
159
160 254
            if (!isset($this->routes[$name]) && $name !== RoutingConfigurator::DEFAULT_ROUTE_NAME) {
161 254
                $groups->getGroup($configurator->group ?? $groups->getDefaultGroup())->addRoute($name, $route);
162
            }
163
164 254
            if ($name === RoutingConfigurator::DEFAULT_ROUTE_NAME) {
165 254
                $this->setDefault($route);
166
            }
167
        }
168
    }
169
170
    /**
171
     * Find route matched for given request.
172
     */
173 67
    protected function matchRoute(ServerRequestInterface $request, string &$routeName = null): ?RouteInterface
174
    {
175 67
        foreach ($this->routes as $name => $route) {
176
            // Matched route will return new route instance with matched parameters
177 63
            $matched = $route->match($request);
178
179 63
            if ($matched !== null) {
180 42
                $routeName = $name;
181 42
                return $matched;
182
            }
183
        }
184
185 26
        if ($this->default !== null) {
186 15
            return $this->default->match($request);
187
        }
188
189
        // unable to match any route
190 11
        return null;
191
    }
192
193
    /**
194
     * Configure route with needed dependencies.
195
     */
196 314
    protected function configure(RouteInterface $route): RouteInterface
197
    {
198 314
        if ($route instanceof ContainerizedInterface && !$route->hasContainer()) {
199
            // isolating route in a given container
200 313
            $route = $route->withContainer($this->container);
201
        }
202
203 312
        return $route->withUriHandler($this->uriHandler->withPrefix($this->basePath));
204
    }
205
206
    /**
207
     * Locates appropriate route by name. Support dynamic route allocation using following pattern:
208
     * Named route:   `name/controller:action`
209
     * Default route: `controller:action`
210
     * Only action:   `name/action`
211
     *
212
     * @throws UndefinedRouteException
213
     */
214 9
    protected function castRoute(string $route): RouteInterface
215
    {
216
        if (
217 9
            !\preg_match(
218
                '/^(?:(?P<name>[^\/]+)\/)?(?:(?P<controller>[^:]+):+)?(?P<action>[a-z_\-]+)$/i',
219
                $route,
220
                $matches
221
            )
222
        ) {
223 1
            throw new UndefinedRouteException(
224
                "Unable to locate route or use default route with 'name/controller:action' pattern"
225
            );
226
        }
227
228 8
        if (!empty($matches['name'])) {
229 5
            $routeObject = $this->getRoute($matches['name']);
230 3
        } elseif ($this->default !== null) {
231 2
            $routeObject = $this->default;
232
        } else {
233 1
            throw new UndefinedRouteException(\sprintf('Unable to locate route candidate for `%s`', $route));
234
        }
235
236 7
        return $routeObject->withDefaults(
237
            [
238 7
                'controller' => $matches['controller'],
239 7
                'action' => $matches['action'],
240
            ]
241
        );
242
    }
243
}
244