Passed
Pull Request — master (#831)
by Maxim
15:41
created

Router::import()   B

Complexity

Conditions 9
Paths 33

Size

Total Lines 29
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 9.0164

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 16
c 3
b 0
f 0
dl 0
loc 29
ccs 16
cts 17
cp 0.9412
rs 8.0555
cc 9
nc 33
nop 1
crap 9.0164
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 381
    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 381
        $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 324
    public function setRoute(string $name, RouteInterface $route): void
96
    {
97
        // each route must inherit basePath prefix
98 324
        $this->routes[$name] = $this->configure($route);
99
    }
100
101 273
    public function setDefault(RouteInterface $route): void
102
    {
103 273
        $this->default = $this->configure($route);
104
    }
105
106 22
    public function getRoute(string $name): RouteInterface
107
    {
108 22
        if (isset($this->routes[$name])) {
109 18
            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 273
    public function import(RoutingConfigurator $routes): void
136
    {
137
        /** @var GroupRegistry $groups */
138 273
        $groups = $this->container->get(GroupRegistry::class);
139
140 273
        foreach ($routes->getCollection() as $name => $configurator) {
141 267
            $target = $configurator->target;
142 267
            if ($configurator->core !== null && $target instanceof AbstractTarget) {
143 267
                $target = $target->withCore($configurator->core);
144
            }
145
146 267
            $route = new Route(\ltrim($configurator->pattern, '/'), $target, $configurator->defaults);
147
148 267
            if ($configurator->middleware !== null) {
149 267
                $route = $route->withMiddleware(...$configurator->middleware);
150
            }
151
152 267
            if ($configurator->methods !== null) {
153
                $route = $route->withVerbs(...$configurator->methods);
154
            }
155
156 267
            if (!isset($this->routes[$name]) && $name !== RoutingConfigurator::DEFAULT_ROUTE_NAME) {
157 267
                $group = $groups->getGroup($configurator->group ?? $groups->getDefaultGroup());
158 267
                $group->setPrefix($configurator->prefix);
159 267
                $group->addRoute($name, $route);
160
            }
161
162 267
            if ($name === RoutingConfigurator::DEFAULT_ROUTE_NAME) {
163 267
                $this->setDefault($route);
164
            }
165
        }
166
    }
167
168
    /**
169
     * Find route matched for given request.
170
     */
171 67
    protected function matchRoute(ServerRequestInterface $request, string &$routeName = null): ?RouteInterface
172
    {
173 67
        foreach ($this->routes as $name => $route) {
174
            // Matched route will return new route instance with matched parameters
175 63
            $matched = $route->match($request);
176
177 63
            if ($matched !== null) {
178 42
                $routeName = $name;
179 42
                return $matched;
180
            }
181
        }
182
183 26
        if ($this->default !== null) {
184 15
            return $this->default->match($request);
185
        }
186
187
        // unable to match any route
188 11
        return null;
189
    }
190
191
    /**
192
     * Configure route with needed dependencies.
193
     */
194 329
    protected function configure(RouteInterface $route): RouteInterface
195
    {
196 329
        if ($route instanceof ContainerizedInterface && !$route->hasContainer()) {
197
            // isolating route in a given container
198 324
            $route = $route->withContainer($this->container);
199
        }
200
201
        try {
202 327
            $uriHandler = $route->getUriHandler();
203 312
        } catch (\Throwable) {
204 312
            $uriHandler = $this->uriHandler;
205
        }
206
207 327
        return $route->withUriHandler($uriHandler->withBasePath($this->basePath));
208
    }
209
210
    /**
211
     * Locates appropriate route by name. Support dynamic route allocation using following pattern:
212
     * Named route:   `name/controller:action`
213
     * Default route: `controller:action`
214
     * Only action:   `name/action`
215
     *
216
     * @throws UndefinedRouteException
217
     */
218 9
    protected function castRoute(string $route): RouteInterface
219
    {
220
        if (
221 9
            !\preg_match(
222
                '/^(?:(?P<name>[^\/]+)\/)?(?:(?P<controller>[^:]+):+)?(?P<action>[a-z_\-]+)$/i',
223
                $route,
224
                $matches
225
            )
226
        ) {
227 1
            throw new UndefinedRouteException(
228
                "Unable to locate route or use default route with 'name/controller:action' pattern"
229
            );
230
        }
231
232 8
        if (!empty($matches['name'])) {
233 5
            $routeObject = $this->getRoute($matches['name']);
234 3
        } elseif ($this->default !== null) {
235 2
            $routeObject = $this->default;
236
        } else {
237 1
            throw new UndefinedRouteException(\sprintf('Unable to locate route candidate for `%s`', $route));
238
        }
239
240 7
        return $routeObject->withDefaults(
241
            [
242 7
                'controller' => $matches['controller'],
243 7
                'action' => $matches['action'],
244
            ]
245
        );
246
    }
247
}
248