Passed
Push — 5.x ( 1ca894...8bdbf0 )
by Phil
22:54
created

Router::collectGroupRoutes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
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 1
cts 1
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
{
21
    use MiddlewareAwareTrait;
22
    use RouteCollectionTrait;
23
    use StrategyAwareTrait;
24
25
    protected const IDENTIFIER_SEPARATOR = "\t";
26
27
    /**
28
     * @var RouteGroup[]
29
     */
30
    protected $groups = [];
31
32
    /**
33
     * @var Route[]
34
     */
35
    protected $namedRoutes = [];
36
37
    /**
38
     * @var array
39
     */
40
    protected $patternMatchers = [
41
        '/{(.+?):number}/'        => '{$1:[0-9]+}',
42
        '/{(.+?):word}/'          => '{$1:[a-zA-Z]+}',
43
        '/{(.+?):alphanum_dash}/' => '{$1:[a-zA-Z0-9-_]+}',
44
        '/{(.+?):slug}/'          => '{$1:[a-z0-9-]+}',
45
        '/{(.+?):uuid}/'          => '{$1:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}+}'
46
    ];
47
48
    /**
49
     * @var RouteCollector
50
     */
51
    protected $routeCollector;
52
53
    /**
54
     * @var Route[]
55
     */
56
    protected $routes = [];
57
58
    /**
59
     * @var bool
60
     */
61
    protected $routesPrepared = false;
62
63
    /**
64
     * @var array
65
     */
66
    protected $routesData = [];
67
68 66
    public function __construct(?RouteCollector $routeCollector = null)
69
    {
70 66
        $this->routeCollector = $routeCollector ?? new RouteCollector(
71 66
            new RouteParser\Std(),
72 66
            new DataGenerator\GroupCountBased()
73
        );
74 66
    }
75
76 3
    public function addPatternMatcher(string $alias, string $regex): self
77
    {
78 3
        $pattern = '/{(.+?):' . $alias . '}/';
79 3
        $regex = '{$1:' . $regex . '}';
80 3
        $this->patternMatchers[$pattern] = $regex;
81 3
        return $this;
82
    }
83
84 12
    public function group(string $prefix, callable $group): RouteGroup
85
    {
86 12
        $group = new RouteGroup($prefix, $group, $this);
87 12
        $this->groups[] = $group;
88 12
        return $group;
89
    }
90
91 51
    public function dispatch(ServerRequestInterface $request): ResponseInterface
92
    {
93 51
        if (false === $this->routesPrepared) {
94 48
            $this->prepareRoutes($request);
95
        }
96
97
        /** @var Dispatcher $dispatcher */
98 51
        $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

98
        $dispatcher = (new Dispatcher($this->routesData))->setStrategy(/** @scrutinizer ignore-type */ $this->getStrategy());
Loading history...
99
100 51
        foreach ($this->getMiddlewareStack() as $middleware) {
101 3
            if (is_string($middleware)) {
102 3
                $dispatcher->lazyMiddleware($middleware);
103 3
                continue;
104
            }
105
106 3
            $dispatcher->middleware($middleware);
107
        }
108
109 51
        return $dispatcher->dispatchRequest($request);
110
    }
111
112 6
    public function getNamedRoute(string $name): Route
113
    {
114 6
        if (!$this->routesPrepared) {
115
            $this->collectGroupRoutes();
116 6
        }
117 3
118
        $this->buildNameIndex();
119
120 3
        if (isset($this->namedRoutes[$name])) {
121
            return $this->namedRoutes[$name];
122
        }
123 3
124
        throw new InvalidArgumentException(sprintf('No route of the name (%s) exists', $name));
125 3
    }
126
127
    public function handle(ServerRequestInterface $request): ResponseInterface
128 51
    {
129
        return $this->dispatch($request);
130 51
    }
131 51
132
    public function map(string $method, string $path, $handler): Route
133 51
    {
134
        $path  = sprintf('/%s', ltrim($path, '/'));
135 51
        $route = new Route($method, $path, $handler);
136
137
        $this->routes[] = $route;
138 51
139
        return $route;
140 51
    }
141 33
142
    public function prepareRoutes(ServerRequestInterface $request): void
143
    {
144 51
        if ($this->getStrategy() === null) {
145 51
            $this->setStrategy(new ApplicationStrategy());
146
        }
147 51
148 51
        $this->processGroups($request);
149
        $this->buildNameIndex();
150
151 51
        $routes = array_merge(array_values($this->routes), array_values($this->namedRoutes));
152 45
        $options = [];
153 36
154
        /** @var Route $route */
155
        foreach ($routes as $route) {
156 45
            if ($route->getStrategy() === null) {
157
                $route->setStrategy($this->getStrategy());
158
            }
159 45
160 36
            $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);
161
162
            // global strategy must be an OPTIONS handler to automatically generate OPTIONS route
163
            if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
164 9
                continue;
165 9
            }
166
167
            // need a messy but useful identifier to determine what methods to respond with on OPTIONS
168 9
            $identifier = $route->getScheme() . static::IDENTIFIER_SEPARATOR . $route->getHost()
169
                . static::IDENTIFIER_SEPARATOR . $route->getPort() . static::IDENTIFIER_SEPARATOR . $route->getPath();
170
171
            // if there is a defined OPTIONS route, do not generate one
172
            if ('OPTIONS' === $route->getMethod()) {
173 9
                unset($options[$identifier]);
174 9
                continue;
175
            }
176
177 9
            if (!isset($options[$identifier])) {
178
                $options[$identifier] = [];
179
            }
180 51
181
            $options[$identifier][] = $route->getMethod();
182 51
        }
183 51
184 51
        $this->buildOptionsRoutes($options);
185
186 57
        $this->routesPrepared = true;
187
        $this->routesData = $this->routeCollector->getData();
188 57
    }
189 48
190 3
    protected function buildNameIndex(): void
191 3
    {
192
        foreach ($this->routes as $key => $route) {
193
            if ($route->getName() !== null) {
194 57
                unset($this->routes[$key]);
195
                $this->namedRoutes[$route->getName()] = $route;
196 51
            }
197
        }
198 51
    }
199 39
200
    protected function buildOptionsRoutes(array $options): void
201
    {
202
        if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
203 12
            return;
204
        }
205 12
206 9
        /** @var OptionsHandlerInterface $strategy */
207 9
        $strategy = $this->getStrategy();
208
209 9
        foreach ($options as $identifier => $methods) {
210
            [$scheme, $host, $port, $path] = explode(static::IDENTIFIER_SEPARATOR, $identifier);
211
            $route = new Route('OPTIONS', $path, $strategy->getOptionsCallable($methods));
212
213 9
            if (!empty($scheme)) {
214
                $route->setScheme($scheme);
215
            }
216
217 9
            if (!empty($host)) {
218
                $route->setHost($host);
219
            }
220
221 9
            if (!empty($port)) {
222
                $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

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