Passed
Push — master ( 41e710...91ffae )
by Aleksei
06:19 queued 21s
created

Router::import()   B

Complexity

Conditions 11
Paths 97

Size

Total Lines 34
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 11

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 34
ccs 21
cts 21
cp 1
rs 7.3166
c 0
b 0
f 0
cc 11
nc 97
nop 1
crap 11

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    private readonly TracerInterface $tracer;
48
49 527
    public function __construct(
50
        string $basePath,
51
        private readonly UriHandler $uriHandler,
52
        private readonly ContainerInterface $container,
53
        private readonly ?EventDispatcherInterface $eventDispatcher = null,
54
        ?TracerInterface $tracer = null,
55
    ) {
56 527
        $this->tracer = $tracer ?? new NullTracer();
0 ignored issues
show
Bug introduced by
The property tracer is declared read-only in Spiral\Router\Router.
Loading history...
57 527
        $this->basePath = '/' . \ltrim($basePath, '/');
58
    }
59
60
    /**
61
     * @throws RouteNotFoundException
62
     * @throws RouterException
63
     */
64 66
    public function handle(ServerRequestInterface $request): ResponseInterface
65
    {
66 66
        $this->eventDispatcher?->dispatch(new Routing($request));
67
68 66
        return $this->tracer->trace(
69 66
            name: 'Routing',
70 66
            callback: function (SpanInterface $span) use ($request): ResponseInterface {
71
                try {
72 66
                    $route = $this->matchRoute($request, $routeName);
73
                } catch (RouteException $e) {
74
                    throw new RouterException('Invalid route definition', $e->getCode(), $e);
75
                }
76
77 66
                if ($route === null) {
78 12
                    $this->eventDispatcher?->dispatch(new RouteNotFound($request));
79 12
                    throw new RouteNotFoundException($request->getUri());
80
                }
81
82 55
                $span
83 55
                    ->setAttribute('request.uri', (string) $request->getUri())
84 55
                    ->setAttribute('route.name', $routeName)
85 55
                    ->setAttribute('route.matches', $route->getMatches() ?? []);
86
87 55
                $request = $request
88 55
                    ->withAttribute(self::ROUTE_ATTRIBUTE, $route)
89 55
                    ->withAttribute(self::ROUTE_NAME, $routeName)
90 55
                    ->withAttribute(self::ROUTE_MATCHES, $route->getMatches() ?? []);
91
92 55
                $this->eventDispatcher?->dispatch(new RouteMatched($request, $route));
93
94 55
                return $route->handle($request);
95 66
            },
96 66
        );
97
    }
98
99 469
    public function setRoute(string $name, RouteInterface $route): void
100
    {
101
        // each route must inherit basePath prefix
102 469
        $this->routes[$name] = $this->configure($route);
103
    }
104
105 364
    public function setDefault(RouteInterface $route): void
106
    {
107 364
        $this->default = $this->configure($route);
108
    }
109
110 73
    public function getRoute(string $name): RouteInterface
111
    {
112 73
        if (isset($this->routes[$name])) {
113 69
            return $this->routes[$name];
114
        }
115
116 9
        throw new UndefinedRouteException(\sprintf('Undefined route `%s`', $name));
117
    }
118
119 3
    public function getRoutes(): array
120
    {
121 3
        if (!empty($this->default)) {
122 2
            return $this->routes + [null => $this->default];
123
        }
124
125 1
        return $this->routes;
126
    }
127
128 12
    public function uri(string $route, iterable $parameters = []): UriInterface
129
    {
130
        try {
131 12
            return $this->getRoute($route)->uri($parameters);
132 9
        } catch (UndefinedRouteException) {
133
            //In some cases route name can be provided as controller:action pair, we can try to
134
            //generate such route automatically based on our default/fallback route
135 9
            return $this->castRoute($route)->uri($parameters);
136
        }
137
    }
138
139 385
    public function import(RoutingConfigurator $routes): void
140
    {
141
        /** @var GroupRegistry $groups */
142 385
        $groups = $this->container->get(GroupRegistry::class);
143
144 385
        foreach ($routes->getCollection() as $name => $configurator) {
145 360
            $target = $configurator->target;
146 360
            if ($configurator->core !== null && $target instanceof AbstractTarget) {
147 358
                $target = $target->withCore($configurator->core);
0 ignored issues
show
Deprecated Code introduced by
The function Spiral\Router\Target\AbstractTarget::withCore() has been deprecated: Use {@see withHandler()} instead. ( Ignorable by Annotation )

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

147
                $target = /** @scrutinizer ignore-deprecated */ $target->withCore($configurator->core);

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.

Loading history...
148
            }
149
150 360
            $pattern = \str_starts_with($configurator->pattern, '//')
151 1
                ? $configurator->pattern
152 359
                : \ltrim($configurator->pattern, '/');
153 360
            $route = new Route($pattern, $target, $configurator->defaults);
154
155 360
            if ($configurator->middleware !== null) {
156 358
                $route = $route->withMiddleware(...$configurator->middleware);
157
            }
158
159 360
            if ($configurator->methods !== null) {
160 1
                $route = $route->withVerbs(...$configurator->methods);
161
            }
162
163 360
            if (!isset($this->routes[$name]) && $name !== RoutingConfigurator::DEFAULT_ROUTE_NAME) {
164 360
                $group = $groups->getGroup($configurator->group ?? $groups->getDefaultGroup());
165 360
                if ($configurator->prefix !== '') {
166 358
                    $group->setPrefix($configurator->prefix);
167
                }
168 360
                $group->addRoute($name, $route);
169
            }
170
171 360
            if ($name === RoutingConfigurator::DEFAULT_ROUTE_NAME) {
172 358
                $this->setDefault($route);
173
            }
174
        }
175
    }
176
177
    /**
178
     * Find route matched for given request.
179
     */
180 66
    protected function matchRoute(ServerRequestInterface $request, ?string &$routeName = null): ?RouteInterface
181
    {
182 66
        foreach ($this->routes as $name => $route) {
183
            // Matched route will return new route instance with matched parameters
184 62
            $matched = $route->match($request);
185
186 62
            if ($matched !== null) {
187 41
                $routeName = $name;
188 41
                return $matched;
189
            }
190
        }
191
192 26
        if ($this->default !== null) {
193 15
            return $this->default->match($request);
194
        }
195
196
        // unable to match any route
197 11
        return null;
198
    }
199
200
    /**
201
     * Configure route with needed dependencies.
202
     */
203 474
    protected function configure(RouteInterface $route): RouteInterface
204
    {
205 474
        if ($route instanceof ContainerizedInterface && !$route->hasContainer()) {
206
            // isolating route in a given container
207 469
            $route = $route->withContainer($this->container);
208
        }
209
210
        try {
211 472
            $uriHandler = $route->getUriHandler();
212 447
        } catch (\Throwable) {
213 447
            $uriHandler = $this->uriHandler;
214
        }
215
216 472
        return $route->withUriHandler($uriHandler->withBasePath($this->basePath));
217
    }
218
219
    /**
220
     * Locates appropriate route by name. Support dynamic route allocation using following pattern:
221
     * Named route:   `name/controller:action`
222
     * Default route: `controller:action`
223
     * Only action:   `name/action`
224
     *
225
     * @throws UndefinedRouteException
226
     */
227 9
    protected function castRoute(string $route): RouteInterface
228
    {
229
        if (
230 9
            !\preg_match(
231 9
                '/^(?:(?P<name>[^\/]+)\/)?(?:(?P<controller>[^:]+):+)?(?P<action>[a-z_\-]+)$/i',
232 9
                $route,
233 9
                $matches,
234 9
            )
235
        ) {
236 1
            throw new UndefinedRouteException(
237 1
                "Unable to locate route or use default route with 'name/controller:action' pattern",
238 1
            );
239
        }
240
241
        /**
242
         * @var Matches $matches
243
         */
244 8
        if (!empty($matches['name'])) {
245 5
            $routeObject = $this->getRoute($matches['name']);
246 3
        } elseif ($this->default !== null) {
247 2
            $routeObject = $this->default;
248
        } else {
249 1
            throw new UndefinedRouteException(\sprintf('Unable to locate route candidate for `%s`', $route));
250
        }
251
252 7
        return $routeObject->withDefaults(
253 7
            [
254 7
                'controller' => $matches['controller'],
255 7
                'action' => $matches['action'],
256 7
            ],
257 7
        );
258
    }
259
}
260