Passed
Pull Request — master (#26)
by Dmitriy
06:22 queued 03:41
created

UrlMatcher::loadConfig()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3.576

Importance

Changes 0
Metric Value
cc 3
eloc 4
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 8
ccs 3
cts 5
cp 0.6
crap 3.576
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router\FastRoute;
6
7
use FastRoute\Dispatcher;
8
use FastRoute\Dispatcher\GroupCountBased;
9
use FastRoute\RouteParser\Std as RouteParser;
10
use FastRoute\DataGenerator\GroupCountBased as RouteGenerator;
11
use FastRoute\RouteCollector;
12
use Psr\Http\Message\ServerRequestInterface;
13
use Psr\SimpleCache\CacheInterface;
14
use Yiisoft\Http\Method;
15
use Yiisoft\Router\Route;
16
use Yiisoft\Router\MatchingResult;
17
use Yiisoft\Router\UrlMatcherInterface;
18
use Yiisoft\Router\RouteCollectionInterface;
19
20
use function array_merge;
21
use function array_reduce;
22
use function array_unique;
23
24
final class UrlMatcher implements UrlMatcherInterface
25
{
26
    /**
27
     * @const string Configuration key used to set the cache file path
28
     */
29
    public const CONFIG_CACHE_KEY = 'cache_key';
30
31
    /**
32
     * @const string Configuration key used to set the cache file path
33
     */
34
    private string $cacheKey = 'routes-cache';
35
36
    /**
37
     * Cache generated route data?
38
     *
39
     * @var bool
40
     */
41
    private bool $cacheEnabled = false;
42
43
    /**
44
     * @var callable A factory callback that can return a dispatcher.
45
     */
46
    private $dispatcherCallback;
47
48
    /**
49
     * Cached data used by the dispatcher.
50
     *
51
     * @var array
52
     */
53
    private array $dispatchData = [];
54
55
    /**
56
     * True if cache is enabled and valid dispatch data has been loaded from
57
     * cache.
58
     *
59
     * @var bool
60
     */
61
    private bool $hasCache = false;
62
    private ?CacheInterface $cache = null;
63
64
    private RouteCollector $fastRouteCollector;
65
    private RouteCollectionInterface $routeCollection;
66
    private ?Route $currentRoute = null;
67
    private bool $hasInjectedRoutes = false;
68
69
    /**
70
     * Last matched request
71
     *
72
     * @var ServerRequestInterface|null
73
     */
74
    private ?ServerRequestInterface $request = null;
75
76
    /**
77
     * Constructor
78
     *
79
     * Accepts optionally a FastRoute RouteCollector and a callable factory
80
     * that can return a FastRoute dispatcher.
81
     *
82
     * If either is not provided defaults will be used:
83
     *
84
     * - A RouteCollector instance will be created composing a RouteParser and
85
     *   RouteGenerator.
86
     * - A callable that returns a GroupCountBased dispatcher will be created.
87
     *
88
     * @param null|RouteCollector $fastRouteCollector If not provided, a default
89
     *     implementation will be used.
90
     * @param null|callable $dispatcherFactory Callable that will return a
91
     *     FastRoute dispatcher.
92
     * @param array $config Array of custom configuration options.
93
     */
94 41
    public function __construct(
95
        RouteCollectionInterface $routeCollection,
96
        CacheInterface $cache = null,
97
        array $config = null,
98
        RouteCollector $fastRouteCollector = null,
99
        callable $dispatcherFactory = null
100
    ) {
101 41
        if (null === $fastRouteCollector) {
102 41
            $fastRouteCollector = $this->createRouteCollector();
103
        }
104 41
        $this->routeCollection = $routeCollection;
105 41
        $this->fastRouteCollector = $fastRouteCollector;
106 41
        $this->dispatcherCallback = $dispatcherFactory;
107 41
        $this->loadConfig($config);
108 41
        $this->cache = $cache;
109 41
        $this->cacheEnabled = ($cache === null ? false : true);
110
111 41
        $this->loadDispatchData();
112
    }
113
114 22
    public function match(ServerRequestInterface $request): MatchingResult
115
    {
116 22
        $this->request = $request;
117
118 22
        if (!$this->hasCache && !$this->hasInjectedRoutes) {
119 22
            $this->injectRoutes();
120
        }
121
122 22
        $dispatchData = $this->getDispatchData();
123 22
        $path = rawurldecode($request->getUri()->getPath());
124 22
        $method = $request->getMethod();
125 22
        $result = $this->getDispatcher($dispatchData)->dispatch($method, $path);
126
127 22
        return $result[0] !== Dispatcher::FOUND
128 2
            ? $this->marshalFailedRoute($result)
129 22
            : $this->marshalMatchedRoute($result, $method);
130
    }
131
132
    /**
133
     * Returns the current Route object
134
     * @return Route|null current route
135
     */
136 1
    public function getCurrentRoute(): ?Route
137
    {
138 1
        return $this->currentRoute;
139
    }
140
141
    /**
142
     * Returns last matched Request
143
     * @return ServerRequestInterface|null current route
144
     */
145 16
    public function getLastMatchedRequest(): ?ServerRequestInterface
146
    {
147 16
        return $this->request;
148
    }
149
150
    /**
151
     * @return RouteCollectionInterface collection of routes
152
     */
153 27
    public function getRouteCollection(): RouteCollectionInterface
154
    {
155 27
        return $this->routeCollection;
156
    }
157
158
    /**
159
     * Load configuration parameters
160
     *
161
     * @param null|array $config Array of custom configuration options.
162
     */
163 41
    private function loadConfig(array $config = null): void
164
    {
165 41
        if (null === $config) {
166 41
            return;
167
        }
168
169
        if (isset($config[self::CONFIG_CACHE_KEY])) {
170
            $this->cacheKey = (bool)$config[self::CONFIG_CACHE_KEY];
171
        }
172
    }
173
174
    /**
175
     * Retrieve the dispatcher instance.
176
     *
177
     * Uses the callable factory in $dispatcherCallback, passing it $data
178
     * (which should be derived from the router's getData() method); this
179
     * approach is done to allow testing against the dispatcher.
180
     *
181
     * @param array|object $data Data from RouteCollector::getData()
182
     * @return Dispatcher
183
     */
184 22
    private function getDispatcher($data): Dispatcher
185
    {
186 22
        if (!$this->dispatcherCallback) {
187 22
            $this->dispatcherCallback = $this->createDispatcherCallback();
188
        }
189
190 22
        $factory = $this->dispatcherCallback;
191
192 22
        return $factory($data);
193
    }
194
195
    /**
196
     * Create a default FastRoute Collector instance
197
     */
198 41
    private function createRouteCollector(): RouteCollector
199
    {
200 41
        return new RouteCollector(new RouteParser(), new RouteGenerator());
201
    }
202
203
    /**
204
     * Return a default implementation of a callback that can return a Dispatcher.
205
     */
206 22
    private function createDispatcherCallback(): callable
207
    {
208
        return static function ($data) {
209 22
            return new GroupCountBased($data);
210 22
        };
211
    }
212
213
    /**
214
     * Marshal a routing failure result.
215
     *
216
     * If the failure was due to the HTTP method, passes the allowed HTTP
217
     * methods to the factory.
218
     * @param array $result
219
     * @return MatchingResult
220
     */
221 2
    private function marshalFailedRoute(array $result): MatchingResult
222
    {
223 2
        $resultCode = $result[0];
224 2
        if ($resultCode === Dispatcher::METHOD_NOT_ALLOWED) {
225 1
            return MatchingResult::fromFailure($result[1]);
226
        }
227
228 1
        return MatchingResult::fromFailure(Method::ANY);
229
    }
230
231
    /**
232
     * Marshals a route result based on the results of matching and the current HTTP method.
233
     * @param array $result
234
     * @param string $method
235
     * @return MatchingResult
236
     */
237 20
    private function marshalMatchedRoute(array $result, string $method): MatchingResult
238
    {
239 20
        [, $name, $parameters] = $result;
240
241 20
        $route = $this->routeCollection->getRoute($name);
242 20
        if (!in_array($method, $route->getMethods(), true)) {
243 1
            $result[1] = $route->getPattern();
244 1
            return $this->marshalMethodNotAllowedResult($result);
245
        }
246
247 19
        $parameters = array_merge($route->getDefaults(), $parameters);
248 19
        $this->currentRoute = $route;
249
250 19
        return MatchingResult::fromSuccess($route, $parameters);
251
    }
252
253 1
    private function marshalMethodNotAllowedResult(array $result): MatchingResult
254
    {
255 1
        $path = $result[1];
256
257 1
        $allowedMethods = array_unique(
258 1
            array_reduce(
259 1
                $this->routeCollection->getRoutes(),
260
                static function ($allowedMethods, Route $route) use ($path) {
261 1
                    if ($path !== $route->getPattern()) {
262 1
                        return $allowedMethods;
263
                    }
264
265 1
                    return array_merge($allowedMethods, $route->getMethods());
266 1
                },
267 1
                []
268
            )
269
        );
270
271 1
        return MatchingResult::fromFailure($allowedMethods);
272
    }
273
274
    /**
275
     * Inject routes into the underlying router
276
     */
277 22
    private function injectRoutes(): void
278
    {
279 22
        foreach ($this->routeCollection->getRoutes() as $index => $route) {
280
            /** @var Route $route */
281 22
            $this->fastRouteCollector->addRoute($route->getMethods(), $route->getPattern(), $route->getName());
282
        }
283 22
        $this->hasInjectedRoutes = true;
284
    }
285
286
    /**
287
     * Get the dispatch data either from cache or freshly generated by the
288
     * FastRoute data generator.
289
     *
290
     * If caching is enabled, store the freshly generated data to file.
291
     */
292 22
    private function getDispatchData(): array
293
    {
294 22
        if ($this->hasCache) {
295
            return $this->dispatchData;
296
        }
297
298 22
        $dispatchData = (array)$this->fastRouteCollector->getData();
299
300 22
        if ($this->cacheEnabled) {
301 1
            $this->cacheDispatchData($dispatchData);
302
        }
303
304 22
        return $dispatchData;
305
    }
306
307
    /**
308
     * Load dispatch data from cache
309
     * @throws \RuntimeException If the cache file contains invalid data
310
     * @throws \Psr\SimpleCache\InvalidArgumentException
311
     */
312 41
    private function loadDispatchData(): void
313
    {
314 41
        if ($this->cacheEnabled && $this->cache->has($this->cacheKey)) {
0 ignored issues
show
Bug introduced by
The method has() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

314
        if ($this->cacheEnabled && $this->cache->/** @scrutinizer ignore-call */ has($this->cacheKey)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
315
            $dispatchData = $this->cache->get($this->cacheKey);
316
317
            $this->hasCache = true;
318
            $this->dispatchData = $dispatchData;
319
            return;
320
        }
321
322 41
        $this->hasCache = false;
323
    }
324
325
    /**
326
     * Save dispatch data to cache
327
     * @param array $dispatchData
328
     * @return int|false bytes written to file or false if error
329
     * @throws \RuntimeException If the cache directory does not exist.
330
     * @throws \RuntimeException If the cache directory is not writable.
331
     * @throws \RuntimeException If the cache file exists but is not writable
332
     * @throws \Psr\SimpleCache\InvalidArgumentException
333
     */
334 1
    private function cacheDispatchData(array $dispatchData): void
335
    {
336 1
        $this->cache->set($this->cacheKey, $dispatchData);
337
    }
338
}
339