Passed
Pull Request — master (#990)
by Maxim
11:14
created

Router::import()   B

Complexity

Conditions 10
Paths 65

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 10.0125

Importance

Changes 0
Metric Value
eloc 19
dl 0
loc 32
ccs 19
cts 20
cp 0.95
rs 7.6666
c 0
b 0
f 0
cc 10
nc 65
nop 1
crap 10.0125

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 430
    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 430
        $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 430
        $this->basePath = '/' . \ltrim($basePath, '/');
58
    }
59
60
    /**
61
     * @throws RouteNotFoundException
62
     * @throws RouterException
63
     */
64 69
    public function handle(ServerRequestInterface $request): ResponseInterface
65
    {
66 69
        $this->eventDispatcher?->dispatch(new Routing($request));
67
68 69
        return $this->tracer->trace(
69 69
            name: 'Routing',
70 69
            callback: function (SpanInterface $span) use ($request) {
71
                try {
72 69
                    $route = $this->matchRoute($request, $routeName);
73
                } catch (RouteException $e) {
74
                    throw new RouterException('Invalid route definition', $e->getCode(), $e);
75
                }
76
77 69
                if ($route === null) {
78 12
                    $this->eventDispatcher?->dispatch(new RouteNotFound($request));
79 12
                    throw new RouteNotFoundException($request->getUri());
80
                }
81
82 58
                $span
83 58
                    ->setAttribute('request.uri', (string)$request->getUri())
84 58
                    ->setAttribute('route.name', $routeName)
85 58
                    ->setAttribute('route.matches', $route->getMatches() ?? []);
86
87 58
                $request = $request
88 58
                    ->withAttribute(self::ROUTE_ATTRIBUTE, $route)
89 58
                    ->withAttribute(self::ROUTE_NAME, $routeName)
90 58
                    ->withAttribute(self::ROUTE_MATCHES, $route->getMatches() ?? []);
91
92 58
                $this->eventDispatcher?->dispatch(new RouteMatched($request, $route));
93
94 58
                return $route->handle($request);
95 69
            }
96 69
        );
97
    }
98
99 373
    public function setRoute(string $name, RouteInterface $route): void
100
    {
101
        // each route must inherit basePath prefix
102 373
        $this->routes[$name] = $this->configure($route);
103
    }
104
105 310
    public function setDefault(RouteInterface $route): void
106
    {
107 310
        $this->default = $this->configure($route);
108
    }
109
110 32
    public function getRoute(string $name): RouteInterface
111
    {
112 32
        if (isset($this->routes[$name])) {
113 28
            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 11
    public function uri(string $route, iterable $parameters = []): UriInterface
129
    {
130
        try {
131 11
            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 311
    public function import(RoutingConfigurator $routes): void
140
    {
141
        /** @var GroupRegistry $groups */
142 311
        $groups = $this->container->get(GroupRegistry::class);
143
144 311
        foreach ($routes->getCollection() as $name => $configurator) {
145 305
            $target = $configurator->target;
146 305
            if ($configurator->core !== null && $target instanceof AbstractTarget) {
147 304
                $target = $target->withCore($configurator->core);
148
            }
149
150 305
            $pattern = \str_starts_with($configurator->pattern, '//')
151 1
                ? $configurator->pattern
152 304
                : \ltrim($configurator->pattern, '/');
153 305
            $route = new Route($pattern, $target, $configurator->defaults);
154
155 305
            if ($configurator->middleware !== null) {
156 304
                $route = $route->withMiddleware(...$configurator->middleware);
157
            }
158
159 305
            if ($configurator->methods !== null) {
160
                $route = $route->withVerbs(...$configurator->methods);
161
            }
162
163 305
            if (!isset($this->routes[$name]) && $name !== RoutingConfigurator::DEFAULT_ROUTE_NAME) {
164 305
                $group = $groups->getGroup($configurator->group ?? $groups->getDefaultGroup());
165 305
                $group->setPrefix($configurator->prefix);
166 305
                $group->addRoute($name, $route);
167
            }
168
169 305
            if ($name === RoutingConfigurator::DEFAULT_ROUTE_NAME) {
170 304
                $this->setDefault($route);
171
            }
172
        }
173
    }
174
175
    /**
176
     * Find route matched for given request.
177
     */
178 69
    protected function matchRoute(ServerRequestInterface $request, string &$routeName = null): ?RouteInterface
179
    {
180 69
        foreach ($this->routes as $name => $route) {
181
            // Matched route will return new route instance with matched parameters
182 65
            $matched = $route->match($request);
183
184 65
            if ($matched !== null) {
185 44
                $routeName = $name;
186 44
                return $matched;
187
            }
188
        }
189
190 26
        if ($this->default !== null) {
191 15
            return $this->default->match($request);
192
        }
193
194
        // unable to match any route
195 11
        return null;
196
    }
197
198
    /**
199
     * Configure route with needed dependencies.
200
     */
201 378
    protected function configure(RouteInterface $route): RouteInterface
202
    {
203 378
        if ($route instanceof ContainerizedInterface && !$route->hasContainer()) {
204
            // isolating route in a given container
205 373
            $route = $route->withContainer($this->container);
206
        }
207
208
        try {
209 376
            $uriHandler = $route->getUriHandler();
210 351
        } catch (\Throwable) {
211 351
            $uriHandler = $this->uriHandler;
212
        }
213
214 376
        return $route->withUriHandler($uriHandler->withBasePath($this->basePath));
215
    }
216
217
    /**
218
     * Locates appropriate route by name. Support dynamic route allocation using following pattern:
219
     * Named route:   `name/controller:action`
220
     * Default route: `controller:action`
221
     * Only action:   `name/action`
222
     *
223
     * @throws UndefinedRouteException
224
     */
225 9
    protected function castRoute(string $route): RouteInterface
226
    {
227
        if (
228 9
            !\preg_match(
229 9
                '/^(?:(?P<name>[^\/]+)\/)?(?:(?P<controller>[^:]+):+)?(?P<action>[a-z_\-]+)$/i',
230 9
                $route,
231 9
                $matches
232 9
            )
233
        ) {
234 1
            throw new UndefinedRouteException(
235 1
                "Unable to locate route or use default route with 'name/controller:action' pattern"
236 1
            );
237
        }
238
239
        /**
240
         * @var Matches $matches
241
         */
242 8
        if (!empty($matches['name'])) {
243 5
            $routeObject = $this->getRoute($matches['name']);
244 3
        } elseif ($this->default !== null) {
245 2
            $routeObject = $this->default;
246
        } else {
247 1
            throw new UndefinedRouteException(\sprintf('Unable to locate route candidate for `%s`', $route));
248
        }
249
250 7
        return $routeObject->withDefaults(
251 7
            [
252 7
                'controller' => $matches['controller'],
253 7
                'action' => $matches['action'],
254 7
            ]
255 7
        );
256
    }
257
}
258