Completed
Pull Request — master (#283)
by Phil
04:05 queued 40s
created

Router::prepareRoutes()   B

Complexity

Conditions 7
Paths 18

Size

Total Lines 46
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 7.0283

Importance

Changes 0
Metric Value
eloc 23
c 0
b 0
f 0
dl 0
loc 46
ccs 22
cts 24
cp 0.9167
rs 8.6186
cc 7
nc 18
nop 1
crap 7.0283
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
        $this->buildNameIndex();
115
116 6
        if (isset($this->namedRoutes[$name])) {
117 3
            return $this->namedRoutes[$name];
118
        }
119
120 3
        throw new InvalidArgumentException(sprintf('No route of the name (%s) exists', $name));
121
    }
122
123
    public function handle(ServerRequestInterface $request): ResponseInterface
124
    {
125
        return $this->dispatch($request);
126
    }
127
128 51
    public function map(string $method, string $path, $handler): Route
129
    {
130 51
        $path  = sprintf('/%s', ltrim($path, '/'));
131 51
        $route = new Route($method, $path, $handler);
132
133 51
        $this->routes[] = $route;
134
135 51
        return $route;
136
    }
137
138 51
    public function prepareRoutes(ServerRequestInterface $request): void
139
    {
140 51
        if ($this->getStrategy() === null) {
141 33
            $this->setStrategy(new ApplicationStrategy());
142
        }
143
144 51
        $this->processGroups($request);
145 51
        $this->buildNameIndex();
146
147 51
        $routes = array_merge(array_values($this->routes), array_values($this->namedRoutes));
148 51
        $options = [];
149
150
        /** @var Route $route */
151 51
        foreach ($routes as $key => $route) {
152 45
            if ($route->getStrategy() === null) {
153 36
                $route->setStrategy($this->getStrategy());
154
            }
155
156 45
            $this->routeCollector->addRoute($route->getMethod(), $this->parseRoutePath($route->getPath()), $route);
157
158
            // global strategy must be an OPTIONS handler to automatically generate OPTIONS route
159 45
            if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
160 36
                continue;
161
            }
162
163
            // need a messy but useful identifier for 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 51
        $this->buildOptionsRoutes($options);
181
182 51
        $this->routesPrepared = true;
183 51
        $this->routesData = $this->routeCollector->getData();
184 51
    }
185
186 57
    protected function buildNameIndex(): void
187
    {
188 57
        foreach ($this->routes as $key => $route) {
189 48
            if ($route->getName() !== null) {
190 3
                unset($this->routes[$key]);
191 3
                $this->namedRoutes[$route->getName()] = $route;
192
            }
193
        }
194 57
    }
195
196 51
    protected function buildOptionsRoutes(array $options): void
197
    {
198 51
        if (!($this->getStrategy() instanceof OptionsHandlerInterface)) {
199 39
            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 12
    }
224
225 51
    protected function processGroups(ServerRequestInterface $request): void
226
    {
227 51
        $activePath = $request->getUri()->getPath();
228
229 51
        foreach ($this->groups as $key => $group) {
230
            // we want to determine if we are technically in a group even if the
231
            // route is not matched so exceptions are handled correctly
232
            if (
233 9
                $group->getStrategy() !== null
234 9
                && strncmp($activePath, $group->getPrefix(), strlen($group->getPrefix())) === 0
235
            ) {
236 6
                $this->setStrategy($group->getStrategy());
237
            }
238
239 9
            unset($this->groups[$key]);
240 9
            $group();
241
        }
242 51
    }
243
244 45
    protected function parseRoutePath(string $path): string
245
    {
246 45
        return preg_replace(array_keys($this->patternMatchers), array_values($this->patternMatchers), $path);
247
    }
248
}
249