Passed
Push — 5.x ( b66c10...1ca894 )
by Phil
01:52 queued 11s
created

Router   A

Complexity

Total Complexity 35

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Test Coverage

Coverage 94.95%

Importance

Changes 5
Bugs 1 Features 0
Metric Value
wmc 35
eloc 102
c 5
b 1
f 0
dl 0
loc 243
ccs 94
cts 99
cp 0.9495
rs 9.6

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A dispatch() 0 19 4
A addPatternMatcher() 0 6 1
A group() 0 5 1
A map() 0 8 1
A handle() 0 3 1
A parseRoutePath() 0 3 1
A buildNameIndex() 0 6 3
B prepareRoutes() 0 46 7
A getNamedRoute() 0 13 3
A collectGroupRoutes() 0 4 2
A processGroups() 0 16 4
A buildOptionsRoutes() 0 26 6
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 69
    public function __construct(?RouteCollector $routeCollector = null)
69
    {
70 69
        $this->routeCollector = $routeCollector ?? new RouteCollector(
71 69
            new RouteParser\Std(),
72 69
            new DataGenerator\GroupCountBased()
73
        );
74 69
    }
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 15
    public function group(string $prefix, callable $group): RouteGroup
85
    {
86 15
        $group = new RouteGroup($prefix, $group, $this);
87 15
        $this->groups[] = $group;
88 15
        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 9
    public function getNamedRoute(string $name): Route
113
    {
114 9
        if (!$this->routesPrepared) {
115 9
            $this->collectGroupRoutes();
116
        }
117
118 9
        $this->buildNameIndex();
119
120 9
        if (isset($this->namedRoutes[$name])) {
121 6
            return $this->namedRoutes[$name];
122
        }
123
124 3
        throw new InvalidArgumentException(sprintf('No route of the name (%s) exists', $name));
125
    }
126
127 3
    public function handle(ServerRequestInterface $request): ResponseInterface
128
    {
129 3
        return $this->dispatch($request);
130
    }
131
132 54
    public function map(string $method, string $path, $handler): Route
133
    {
134 54
        $path  = sprintf('/%s', ltrim($path, '/'));
135 54
        $route = new Route($method, $path, $handler);
136
137 54
        $this->routes[] = $route;
138
139 54
        return $route;
140
    }
141
142 51
    public function prepareRoutes(ServerRequestInterface $request): void
143
    {
144 51
        if ($this->getStrategy() === null) {
145 33
            $this->setStrategy(new ApplicationStrategy());
146
        }
147
148 51
        $this->processGroups($request);
149 51
        $this->buildNameIndex();
150
151 51
        $routes = array_merge(array_values($this->routes), array_values($this->namedRoutes));
152 51
        $options = [];
153
154
        /** @var Route $route */
155 51
        foreach ($routes as $key => $route) {
156 45
            if ($route->getStrategy() === null) {
157 36
                $route->setStrategy($this->getStrategy());
158
            }
159
160 45
            $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 45
            if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
164 36
                continue;
165
            }
166
167
            // need a messy but useful identifier for to determine what methods to respond with on OPTIONS
168 9
            $identifier = $route->getScheme() . static::IDENTIFIER_SEPARATOR . $route->getHost()
169 9
                . static::IDENTIFIER_SEPARATOR . $route->getPort() . static::IDENTIFIER_SEPARATOR . $route->getPath();
170
171
            // if there is a defined OPTIONS route, do not generate one
172 9
            if ('OPTIONS' === $route->getMethod()) {
173
                unset($options[$identifier]);
174
                continue;
175
            }
176
177 9
            if (!isset($options[$identifier])) {
178 9
                $options[$identifier] = [];
179
            }
180
181 9
            $options[$identifier][] = $route->getMethod();
182
        }
183
184 51
        $this->buildOptionsRoutes($options);
185
186 51
        $this->routesPrepared = true;
187 51
        $this->routesData = $this->routeCollector->getData();
188 51
    }
189
190 60
    protected function buildNameIndex(): void
191
    {
192 60
        foreach ($this->routes as $key => $route) {
193 51
            if ($route->getName() !== null) {
194 6
                unset($this->routes[$key]);
195 6
                $this->namedRoutes[$route->getName()] = $route;
196
            }
197
        }
198 60
    }
199
200 51
    protected function buildOptionsRoutes(array $options): void
201
    {
202 51
        if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
203 39
            return;
204
        }
205
206
        /** @var OptionsHandlerInterface $strategy */
207 12
        $strategy = $this->getStrategy();
208
209 12
        foreach ($options as $identifier => $methods) {
210 9
            [$scheme, $host, $port, $path] = explode(static::IDENTIFIER_SEPARATOR, $identifier);
211 9
            $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
            }
224
225 9
            $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);
226
        }
227 12
    }
228
229 9
    protected function collectGroupRoutes(): void
230
    {
231 9
        foreach ($this->groups as $key => $group) {
232 3
            $group();
233
        }
234 9
    }
235
236 51
    protected function processGroups(ServerRequestInterface $request): void
237
    {
238 51
        $activePath = $request->getUri()->getPath();
239
240 51
        foreach ($this->groups as $key => $group) {
241
            // we want to determine if we are technically in a group even if the
242
            // route is not matched so exceptions are handled correctly
243
            if (
244 9
                $group->getStrategy() !== null
245 9
                && strncmp($activePath, $group->getPrefix(), strlen($group->getPrefix())) === 0
246
            ) {
247 6
                $this->setStrategy($group->getStrategy());
248
            }
249
250 9
            unset($this->groups[$key]);
251 9
            $group();
252
        }
253 51
    }
254
255 45
    protected function parseRoutePath(string $path): string
256
    {
257 45
        return preg_replace(array_keys($this->patternMatchers), array_values($this->patternMatchers), $path);
258
    }
259
}
260