Completed
Push — master ( 1c3045...113bf8 )
by Neomerx
12:22
created

Router::checkRouteNameIsUnique()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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