Router::match()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 2
dl 0
loc 26
ccs 11
cts 11
cp 1
crap 3
rs 9.504
c 0
b 0
f 0
1
<?php declare(strict_types=1);
2
3
namespace Limoncello\Core\Routing;
4
5
/**
6
 * Copyright 2015-2020 [email protected]
7
 *
8
 * Licensed under the Apache License, Version 2.0 (the "License");
9
 * you may not use this file except in compliance with the License.
10
 * You may obtain a copy of the License at
11
 *
12
 * http://www.apache.org/licenses/LICENSE-2.0
13
 *
14
 * Unless required by applicable law or agreed to in writing, software
15
 * distributed under the License is distributed on an "AS IS" BASIS,
16
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
 * See the License for the specific language governing permissions and
18
 * limitations under the License.
19
 */
20
21
use FastRoute\DataGenerator;
22
use FastRoute\Dispatcher;
23
use FastRoute\RouteCollector;
24
use FastRoute\RouteParser\Std;
25
use Limoncello\Common\Reflection\ClassIsTrait;
26
use Limoncello\Contracts\Routing\DispatcherInterface;
27
use Limoncello\Contracts\Routing\GroupInterface;
28
use Limoncello\Contracts\Routing\RouteInterface;
29
use Limoncello\Contracts\Routing\RouterInterface;
30
use LogicException;
31
use Psr\Http\Message\ServerRequestInterface;
32
use function assert;
33
use function array_key_exists;
34
use function array_merge;
35
use function strlen;
36
37
/**
38
 * @package Limoncello\Core
39
 */
40
class Router implements RouterInterface
41
{
42
    use ClassIsTrait;
43
44
    /**
45
     * @var false|array
46
     */
47
    private $cachedRoutes = false;
48
49
    /**
50
     * @var string
51
     */
52
    private $generatorClass;
53
54
    /**
55
     * @var string
56
     */
57
    private $dispatcherClass;
58
59
    /**
60
     * @var DispatcherInterface
61
     */
62
    private $dispatcher;
63
64
    /**
65
     * @param string $generatorClass
66
     * @param string $dispatcherClass
67
     */
68 16
    public function __construct(string $generatorClass, string $dispatcherClass)
69
    {
70 16
        assert(static::classImplements($generatorClass, DataGenerator::class));
71 16
        assert(static::classImplements($dispatcherClass, Dispatcher::class));
72
73 16
        $this->generatorClass  = $generatorClass;
74 16
        $this->dispatcherClass = $dispatcherClass;
75
    }
76
77
    /**
78
     * @inheritdoc
79
     *
80 15
     * PHPMD sees `out` parameters from functions as undefined.
81
     * @SuppressWarnings(PHPMD.UndefinedVariable)
82 15
     */
83
    public function getCachedRoutes(GroupInterface $group): array
84 15
    {
85 15
        $collector = $this->createRouteCollector();
86 15
87 15
        $routeIndex         = 0;
88
        $allRoutesInfo      = [];
89 15
        $namedRouteUriPaths = [];
90 15
        foreach ($group->getRoutes() as $route) {
91 15
            /** @var RouteInterface $route */
92 15
            $allRoutesInfo[] = [
93 15
                $route->getHandler(),
94
                $route->getMiddleware(),
95
                $route->getContainerConfigurators(),
96 15
                $route->getRequestFactory(),
97 15
            ];
98 7
99 7
            $routeName = $route->getName();
100 7
            if (empty($routeName) === false) {
101
                assert(
102 7
                    $this->checkRouteNameIsUnique($route, $namedRouteUriPaths, $uriPath, $otherUri) === true,
103
                    "Route name `$routeName` from `$uriPath` has already been used for `$otherUri`."
104
                );
105 15
                $namedRouteUriPaths[$routeName] = $route->getUriPath();
106
            }
107 15
108
            $collector->addRoute($route->getMethod(), $route->getUriPath(), $routeIndex);
109
110 15
            $routeIndex++;
111
        }
112
113
        return [$collector->getData(), $allRoutesInfo, $namedRouteUriPaths];
114
    }
115
116 15
    /**
117
     * @inheritdoc
118 15
     */
119 15
    public function loadCachedRoutes(array $cachedRoutes): void
120
    {
121 15
        $this->cachedRoutes = $cachedRoutes;
122 15
        list($collectorData) = $cachedRoutes;
123
124
        $this->dispatcher = $this->createDispatcher();
125
        $this->dispatcher->setData($collectorData);
126
    }
127
128 14
    /**
129
     * @inheritdoc
130 14
     */
131
    public function match(string $method, string $uriPath): array
132 13
    {
133
        $this->checkRoutesLoaded();
134
135
        $result = $this->dispatcher->dispatchRequest($method, $uriPath);
136 13
137
        // Array contains matching result code, allowed methods list, handler parameters list, handler,
138 13
        // middleware list, container configurators list, custom request factory.
139 11
        list ($dispatchResult) = $result;
140 11
        switch ($dispatchResult) {
141 11
            case DispatcherInterface::ROUTE_FOUND:
142
                list (, $routeIndex, $handlerParams) = $result;
143 11
                list(, $allRoutesInfo) = $this->cachedRoutes;
144
                $routeInfo = $allRoutesInfo[$routeIndex];
145 6
146 5
                return array_merge([self::MATCH_FOUND, null, $handlerParams], $routeInfo);
147
148 5
            case DispatcherInterface::ROUTE_METHOD_NOT_ALLOWED:
149
                list (, $allowedMethods) = $result;
150
151 5
                return [self::MATCH_METHOD_NOT_ALLOWED, $allowedMethods, null, null, null, null, null];
152
153
            default:
154
                return [self::MATCH_NOT_FOUND, null, null, null, null, null, null];
155
        }
156
    }
157
158 5
    /**
159
     * @inheritdoc
160 5
     */
161
    public function getUriPath(string $routeName): ?string
162 5
    {
163
        $this->checkRoutesLoaded();
164 5
165
        list(, , $namedRouteUriPaths) = $this->cachedRoutes;
166 5
167
        $result = array_key_exists($routeName, $namedRouteUriPaths) === true ? $namedRouteUriPaths[$routeName] : null;
168
169
        return $result;
170
    }
171
172 1
    /**
173
     * @inheritdoc
174
     */
175
    public function get(
176
        string $hostUri,
177
        string $routeName,
178 1
        array $placeholders = [],
179 1
        array $queryParams = []
180 1
    ): string {
181
        $path = $this->getUriPath($routeName);
182 1
        $path = $path === null ? $path : $this->replacePlaceholders($path, $placeholders);
183
        $url  = empty($queryParams) === true ? "$hostUri$path" : "$hostUri$path?" . http_build_query($queryParams);
184
185
        return $url;
186
    }
187
188 1
    /**
189
     * @inheritdoc
190 1
     */
191 1
    public function getHostUri(ServerRequestInterface $request): string
192 1
    {
193 1
        $uri       = $request->getUri();
194 1
        $uriScheme = $uri->getScheme();
195
        $uriHost   = $uri->getHost();
196 1
        $uriPort   = $uri->getPort();
197
        $hostUri   = empty($uriPort) === true ? "$uriScheme://$uriHost" : "$uriScheme://$uriHost:$uriPort";
198
199
        return $hostUri;
200
    }
201
202 15
    /**
203
     * @return RouteCollector
204 15
     */
205
    protected function createRouteCollector(): RouteCollector
206
    {
207
        return new RouteCollector(new Std(), new $this->generatorClass);
208
    }
209
210 15
    /**
211
     * @return DispatcherInterface
212 15
     */
213
    protected function createDispatcher(): DispatcherInterface
214
    {
215
        return new $this->dispatcherClass;
216
    }
217
218
    /**
219
     * @param string $path
220
     * @param array  $placeholders
221
     *
222
     * @return string
223 1
     *
224
     * @SuppressWarnings(PHPMD.ElseExpression)
225 1
     */
226 1
    private function replacePlaceholders(string $path, array $placeholders): string
227 1
    {
228 1
        $result            = '';
229 1
        $inPlaceholder     = false;
230 1
        $curPlaceholder    = null;
231 1
        $inPlaceholderName = false;
232
        $pathLength        = strlen($path);
233 1
        for ($index = 0; $index < $pathLength; ++$index) {
234 1
            $character = $path[$index];
235 1
            switch ($character) {
236 1
                case '{':
237 1
                    assert($inPlaceholder === false, 'Nested placeholders (e.g. `{{}}}` are not allowed.');
238 1
                    $inPlaceholder     = true;
239 1
                    $inPlaceholderName = true;
240 1
                    break;
241
                case '}':
242 1
                    $result .= array_key_exists($curPlaceholder, $placeholders) === true ?
243 1
                        $placeholders[$curPlaceholder] : '{' . $curPlaceholder . '}';
244 1
245 1
                    $inPlaceholder     = false;
246
                    $curPlaceholder    = null;
247 1
                    $inPlaceholderName = false;
248 1
                    break;
249
                default:
250 1
                    if ($inPlaceholder === false) {
251 1
                        $result .= $character;
252 1
                    } else {
253 1
                        if ($character === ':') {
254
                            $inPlaceholderName = false;
255
                        } elseif ($inPlaceholderName === true) {
256 1
                            $curPlaceholder .= $character;
257
                        }
258
                    }
259
                    break;
260 1
            }
261
        }
262
263
        return $result;
264
    }
265
266 15
    /**
267
     * @return void
268 15
     */
269 1
    private function checkRoutesLoaded(): void
270
    {
271
        if ($this->cachedRoutes === false) {
272
            throw new LogicException('Routes are not loaded yet.');
273
        }
274
    }
275
276
    /**
277
     * @param RouteInterface $route
278
     * @param array          $namedRouteUriPaths
279
     * @param null|string    $url
280
     * @param null|string    $otherUrl
281 7
     *
282
     * @return bool
283
     */
284
    private function checkRouteNameIsUnique(
285
        RouteInterface $route,
286
        array $namedRouteUriPaths,
287
        ?string &$url,
288 7
        ?string &$otherUrl
289
    ): bool {
290 7
        // check is really simple, the main purpose of the method is to prepare data for assert
291 7
        $isUnique = array_key_exists($route->getName(), $namedRouteUriPaths) === false;
292
293 7
        $url      = $isUnique === true ? null : $route->getUriPath();
294
        $otherUrl = $isUnique === true ? null : $namedRouteUriPaths[$route->getName()];
295
296
        return $isUnique;
297
    }
298
}
299