Passed
Pull Request — master (#29)
by Rustam
13:21
created

UrlMatcher::loadDispatchData()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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

272
        $requestHost = $this->request->/** @scrutinizer ignore-call */ getUri()->getHost();

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...
273 4
        $allowedHost = parse_url($route->getHost(), PHP_URL_HOST);
274
275 4
        return $requestHost === $allowedHost;
276
    }
277
278
    /**
279
     * Inject routes into the underlying router
280
     */
281 23
    private function injectRoutes(): void
282
    {
283 23
        foreach ($this->routeCollection->getRoutes() as $index => $route) {
284
            /** @var Route $route */
285 23
            $this->fastRouteCollector->addRoute($route->getMethods(), $route->getPattern(), $route->getName());
286
        }
287 23
        $this->hasInjectedRoutes = true;
288
    }
289
290
    /**
291
     * Get the dispatch data either from cache or freshly generated by the
292
     * FastRoute data generator.
293
     *
294
     * If caching is enabled, store the freshly generated data to file.
295
     */
296 24
    private function getDispatchData(): array
297
    {
298 24
        if ($this->hasCache) {
299 1
            return $this->dispatchData;
300
        }
301
302 23
        $dispatchData = (array)$this->fastRouteCollector->getData();
303
304 23
        if ($this->cache !== null) {
305 2
            $this->cacheDispatchData($dispatchData);
306
        }
307
308 23
        return $dispatchData;
309
    }
310
311
    /**
312
     * Load dispatch data from cache
313
     * @throws \RuntimeException If the cache file contains invalid data
314
     * @throws \Psr\SimpleCache\InvalidArgumentException
315
     */
316 43
    private function loadDispatchData(): void
317
    {
318 43
        if ($this->cache !== null && $this->cache->has($this->cacheKey)) {
319 1
            $dispatchData = $this->cache->get($this->cacheKey);
320
321 1
            $this->hasCache = true;
322 1
            $this->dispatchData = $dispatchData;
323 1
            return;
324
        }
325
326 42
        $this->hasCache = false;
327
    }
328
329
    /**
330
     * Save dispatch data to cache
331
     * @param array $dispatchData
332
     * @throws \RuntimeException If the cache directory does not exist.
333
     * @throws \RuntimeException If the cache directory is not writable.
334
     * @throws \RuntimeException If the cache file exists but is not writable
335
     * @throws \Psr\SimpleCache\InvalidArgumentException
336
     */
337 2
    private function cacheDispatchData(array $dispatchData): void
338
    {
339 2
        $this->cache->set($this->cacheKey, $dispatchData);
0 ignored issues
show
Bug introduced by
The method set() 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

339
        $this->cache->/** @scrutinizer ignore-call */ 
340
                      set($this->cacheKey, $dispatchData);

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...
340
    }
341
}
342