Router::collectGroupRoutes()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 2
c 1
b 1
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 0
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace League\Route;
6
7
use FastRoute\{DataGenerator, RouteCollector, RouteParser};
8
use InvalidArgumentException;
9
use League\Route\Middleware\{MiddlewareAwareInterface, MiddlewareAwareTrait};
10
use League\Route\Strategy\{ApplicationStrategy, OptionsHandlerInterface, StrategyAwareInterface, StrategyAwareTrait};
11
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface};
12
use Psr\Http\Server\RequestHandlerInterface;
13
use RuntimeException;
14
15
class Router implements
16
    MiddlewareAwareInterface,
17
    RouteCollectionInterface,
18
    StrategyAwareInterface,
19
    RequestHandlerInterface,
20
    RouteConditionHandlerInterface
21
{
22
    use MiddlewareAwareTrait;
23
    use RouteCollectionTrait;
24
    use RouteConditionHandlerTrait;
25
    use StrategyAwareTrait;
26
27
    protected const IDENTIFIER_SEPARATOR = "\t";
28
29
    /**
30
     * @var RouteGroup[]
31
     */
32
    protected array $groups = [];
33
34
    /**
35
     * @var Route[]
36
     */
37
    protected array $namedRoutes = [];
38
39
    protected array $patternMatchers = [
40
        '/{(.+?):number}/'        => '{$1:[0-9]+}',
41
        '/{(.+?):word}/'          => '{$1:[a-zA-Z]+}',
42
        '/{(.+?):alphanum_dash}/' => '{$1:[a-zA-Z0-9-_]+}',
43
        '/{(.+?):slug}/'          => '{$1:[a-z0-9-]+}',
44
        '/{(.+?):uuid}/'          => '{$1:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}+}'
45
    ];
46
47
    /**
48
     * @var Route[]
49
     */
50
    protected array $routes = [];
51
52
    protected bool $routesPrepared = false;
53
54
    protected array $routesData = [];
55
56 72
    public function __construct(protected ?RouteCollector $routeCollector = null)
57
    {
58 72
        $this->routeCollector = $this->routeCollector ?? new RouteCollector(
59 72
            new RouteParser\Std(),
60 72
            new DataGenerator\GroupCountBased()
61 72
        );
62
    }
63
64 3
    public function addPatternMatcher(string $alias, string $regex): self
65
    {
66 3
        $pattern = '/{(.+?):' . $alias . '}/';
67 3
        $regex = '{$1:' . $regex . '}';
68 3
        $this->patternMatchers[$pattern] = $regex;
69 3
        return $this;
70
    }
71
72 15
    public function group(string $prefix, callable $group): RouteGroup
73
    {
74 15
        $group = new RouteGroup($prefix, $group, $this);
75 15
        $this->groups[] = $group;
76 15
        return $group;
77
    }
78
79 54
    public function dispatch(ServerRequestInterface $request): ResponseInterface
80
    {
81 54
        if (false === $this->routesPrepared) {
82 51
            $this->prepareRoutes($request);
83
        }
84
85
        /** @var Dispatcher $dispatcher */
86 54
        $dispatcher = (new Dispatcher($this->routesData))->setStrategy($this->getStrategy());
0 ignored issues
show
Bug introduced by
It seems like $this->getStrategy() can also be of type null; however, parameter $strategy of League\Route\Dispatcher::setStrategy() does only seem to accept League\Route\Strategy\StrategyInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

86
        $dispatcher = (new Dispatcher($this->routesData))->setStrategy(/** @scrutinizer ignore-type */ $this->getStrategy());
Loading history...
87
88 54
        foreach ($this->getMiddlewareStack() as $middleware) {
89 3
            if (is_string($middleware)) {
90 3
                $dispatcher->lazyMiddleware($middleware);
91 3
                continue;
92
            }
93
94 3
            $dispatcher->middleware($middleware);
95
        }
96
97 54
        return $dispatcher->dispatchRequest($request);
98
    }
99
100 9
    public function getNamedRoute(string $name): Route
101
    {
102 9
        if (!$this->routesPrepared) {
103 9
            $this->collectGroupRoutes();
104
        }
105
106 9
        $this->buildNameIndex();
107
108 9
        if (isset($this->namedRoutes[$name])) {
109 6
            return $this->namedRoutes[$name];
110
        }
111
112 3
        throw new InvalidArgumentException(sprintf('No route of the name (%s) exists', $name));
113
    }
114
115 3
    public function handle(ServerRequestInterface $request): ResponseInterface
116
    {
117 3
        return $this->dispatch($request);
118
    }
119
120 57
    public function map(
121
        string|array $method,
122
        string $path,
123
        callable|array|string|RequestHandlerInterface $handler
124
    ): Route {
125 57
        $path = sprintf('/%s', ltrim($path, '/'));
126 57
        $route = new Route($method, $path, $handler);
127
128 57
        $this->routes[] = $route;
129
130 57
        return $route;
131
    }
132
133 54
    public function prepareRoutes(ServerRequestInterface $request): void
134
    {
135 54
        if ($this->getStrategy() === null) {
136 36
            $this->setStrategy(new ApplicationStrategy());
137
        }
138
139 54
        $this->processGroups($request);
140 54
        $this->buildNameIndex();
141
142 54
        $routes = array_merge(array_values($this->routes), array_values($this->namedRoutes));
143 54
        $options = [];
144
145
        /** @var Route $route */
146 54
        foreach ($routes as $route) {
147
            // this allows for the same route to be mapped across different routes/hosts etc
148 48
            if (false === $this->isExtraConditionMatch($route, $request)) {
149 12
                continue;
150
            }
151
152 39
            if ($route->getStrategy() === null) {
153 30
                $route->setStrategy($this->getStrategy());
154
            }
155
156 39
            $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);
0 ignored issues
show
Bug introduced by
The method addRoute() 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

156
            $this->routeCollector->/** @scrutinizer ignore-call */ 
157
                                   addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);

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...
157
158
            // global strategy must be an OPTIONS handler to automatically generate OPTIONS route
159 39
            if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
160 30
                continue;
161
            }
162
163
            // need a messy but useful identifier to determine what methods to respond with on OPTIONS
164 9
            $identifier = $route->getScheme() . static::IDENTIFIER_SEPARATOR . $route->getHost()
165 9
                . static::IDENTIFIER_SEPARATOR . $route->getPort() . static::IDENTIFIER_SEPARATOR . $route->getPath();
166
167
            // if there is a defined OPTIONS route, do not generate one
168 9
            if ('OPTIONS' === $route->getMethod()) {
169
                unset($options[$identifier]);
170
                continue;
171
            }
172
173 9
            if (!isset($options[$identifier])) {
174 9
                $options[$identifier] = [];
175
            }
176
177 9
            $options[$identifier][] = $route->getMethod();
178
        }
179
180 54
        $this->buildOptionsRoutes($options);
181
182 54
        $this->routesPrepared = true;
183 54
        $this->routesData = $this->routeCollector->getData();
184
    }
185
186 63
    protected function buildNameIndex(): void
187
    {
188 63
        foreach ($this->routes as $key => $route) {
189 54
            if ($route->getName() !== null) {
190 6
                unset($this->routes[$key]);
191 6
                $this->namedRoutes[$route->getName()] = $route;
192
            }
193
        }
194
    }
195
196 54
    protected function buildOptionsRoutes(array $options): void
197
    {
198 54
        if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
199 42
            return;
200
        }
201
202
        /** @var OptionsHandlerInterface $strategy */
203 12
        $strategy = $this->getStrategy();
204
205 12
        foreach ($options as $identifier => $methods) {
206 9
            [$scheme, $host, $port, $path] = explode(static::IDENTIFIER_SEPARATOR, $identifier);
207 9
            $route = new Route('OPTIONS', $path, $strategy->getOptionsCallable($methods));
208
209 9
            if (!empty($scheme)) {
210
                $route->setScheme($scheme);
211
            }
212
213 9
            if (!empty($host)) {
214
                $route->setHost($host);
215
            }
216
217 9
            if (!empty($port)) {
218
                $route->setPort($port);
0 ignored issues
show
Bug introduced by
$port of type string is incompatible with the type integer expected by parameter $port of League\Route\Route::setPort(). ( Ignorable by Annotation )

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

218
                $route->setPort(/** @scrutinizer ignore-type */ $port);
Loading history...
219
            }
220
221 9
            $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);
222
        }
223
    }
224
225 9
    protected function collectGroupRoutes(): void
226
    {
227 9
        foreach ($this->groups as $group) {
228 3
            $group();
229
        }
230
    }
231
232 54
    protected function processGroups(ServerRequestInterface $request): void
233
    {
234 54
        $activePath = $request->getUri()->getPath();
235
236 54
        foreach ($this->groups as $key => $group) {
237
            // we want to determine if we are technically in a group even if the
238
            // route is not matched so exceptions are handled correctly
239
            if (
240 9
                $group->getStrategy() !== null
241 9
                && strncmp($activePath, $group->getPrefix(), strlen($group->getPrefix())) === 0
242
            ) {
243 6
                $this->setStrategy($group->getStrategy());
244
            }
245
246 9
            unset($this->groups[$key]);
247 9
            $group();
248
        }
249
    }
250
251 39
    protected function parseRoutePath(string $path): string
252
    {
253 39
        return preg_replace(array_keys($this->patternMatchers), array_values($this->patternMatchers), $path);
254
    }
255
}
256