Passed
Pull Request — 5.x (#311)
by
unknown
01:35
created

Router::handle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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

99
        $dispatcher = (new Dispatcher($this->routesData))->setStrategy(/** @scrutinizer ignore-type */ $this->getStrategy());
Loading history...
100
101 69
        foreach ($this->getMiddlewareStack() as $middleware) {
102 3
            if (is_string($middleware)) {
103 3
                $dispatcher->lazyMiddleware($middleware);
104 3
                continue;
105
            }
106
107 3
            $dispatcher->middleware($middleware);
108
        }
109
110 69
        return $dispatcher->dispatchRequest($request);
111
    }
112
113 9
    public function getNamedRoute(string $name): Route
114
    {
115 9
        if (!$this->routesPrepared) {
116 9
            $this->collectGroupRoutes();
117
        }
118
119 9
        $this->buildNameIndex();
120
121 9
        if (isset($this->namedRoutes[$name])) {
122 6
            return $this->namedRoutes[$name];
123
        }
124
125 3
        throw new InvalidArgumentException(sprintf('No route of the name (%s) exists', $name));
126
    }
127
128 18
    public function handle(ServerRequestInterface $request): ResponseInterface
129
    {
130 18
        return $this->dispatch($request);
131
    }
132
133 72
    public function map(string $method, string $path, $handler): Route
134
    {
135 72
        $path  = sprintf('/%s', ltrim($path, '/'));
136 72
        $route = new Route($method, $path, $handler);
137
138 72
        $this->routes[] = $route;
139
140 72
        return $route;
141
    }
142
143 69
    public function prepareRoutes(ServerRequestInterface $request): void
144
    {
145 69
        if ($this->getStrategy() === null) {
146 45
            $this->setStrategy(new ApplicationStrategy());
147
        }
148
149 69
        $this->processGroups($request);
150 69
        $this->buildNameIndex();
151
152 69
        $routes = array_merge(array_values($this->routes), array_values($this->namedRoutes));
153 69
        $options = [];
154
155
        /** @var Route $route */
156 69
        foreach ($routes as $route) {
157
            // this allows for the same route to be mapped across different routes/hosts etc
158 63
            if (false === $this->isExtraConditionMatch($route, $request)) {
159 12
                continue;
160
            }
161
162 54
            if ($route->getStrategy() === null) {
163 45
                $route->setStrategy($this->getStrategy());
164
            }
165
166 54
            $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);
167
168
            // global strategy must be an OPTIONS handler to automatically generate OPTIONS route
169 54
            if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
170 39
                continue;
171
            }
172
173
            // need a messy but useful identifier to determine what methods to respond with on OPTIONS
174 15
            $identifier = $route->getScheme() . static::IDENTIFIER_SEPARATOR . $route->getHost()
175 15
                . static::IDENTIFIER_SEPARATOR . $route->getPort() . static::IDENTIFIER_SEPARATOR . $route->getPath();
176
177
            // if there is a defined OPTIONS route, do not generate one
178 15
            if ('OPTIONS' === $route->getMethod()) {
179 3
                unset($options[$identifier]);
180 3
                continue;
181
            }
182
183 12
            if (!isset($options[$identifier])) {
184 12
                $options[$identifier] = [];
185
            }
186
187 12
            $options[$identifier][] = $route->getMethod();
188
        }
189
190 69
        $this->buildOptionsRoutes($options);
191
192 69
        $this->routesPrepared = true;
193 69
        $this->routesData = $this->routeCollector->getData();
194 69
    }
195
196 78
    protected function buildNameIndex(): void
197
    {
198 78
        foreach ($this->routes as $key => $route) {
199 69
            if ($route->getName() !== null) {
200 6
                unset($this->routes[$key]);
201 6
                $this->namedRoutes[$route->getName()] = $route;
202
            }
203
        }
204 78
    }
205
206 69
    protected function buildOptionsRoutes(array $options): void
207
    {
208 69
        if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
209 51
            return;
210
        }
211
212
        /** @var OptionsHandlerInterface $strategy */
213 18
        $strategy = $this->getStrategy();
214
215 18
        foreach ($options as $identifier => $methods) {
216 12
            [$scheme, $host, $port, $path] = explode(static::IDENTIFIER_SEPARATOR, $identifier);
217 12
            $route = new Route('OPTIONS', $path, $strategy->getOptionsCallable($methods));
218
219 12
            if (!empty($scheme)) {
220 3
                $route->setScheme($scheme);
221
            }
222
223 12
            if (!empty($host)) {
224 3
                $route->setHost($host);
225
            }
226
227 12
            if (!empty($port)) {
228 3
                $route->setPort((int) $port);
229
            }
230
231 12
            $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);
232
        }
233 18
    }
234
235 9
    protected function collectGroupRoutes(): void
236
    {
237 9
        foreach ($this->groups as $group) {
238 3
            $group();
239
        }
240 9
    }
241
242 69
    protected function processGroups(ServerRequestInterface $request): void
243
    {
244 69
        $activePath = $request->getUri()->getPath();
245
246 69
        foreach ($this->groups as $key => $group) {
247
            // we want to determine if we are technically in a group even if the
248
            // route is not matched so exceptions are handled correctly
249
            if (
250 9
                $group->getStrategy() !== null
251 9
                && strncmp($activePath, $group->getPrefix(), strlen($group->getPrefix())) === 0
252
            ) {
253 6
                $this->setStrategy($group->getStrategy());
254
            }
255
256 9
            unset($this->groups[$key]);
257 9
            $group();
258
        }
259 69
    }
260
261 54
    protected function parseRoutePath(string $path): string
262
    {
263 54
        return preg_replace(array_keys($this->patternMatchers), array_values($this->patternMatchers), $path);
264
    }
265
}
266