Passed
Pull Request — master (#87)
by Sergei
02:18
created

UrlMatcher::marshalFailedRoute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 8
ccs 5
cts 5
cp 1
crap 2
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 RuntimeException;
15
use Yiisoft\Http\Method;
16
use Yiisoft\Router\MatchingResult;
17
use Yiisoft\Router\RouteCollectionInterface;
18
use Yiisoft\Router\UrlMatcherInterface;
19
20
use function array_merge;
21
22
final class UrlMatcher implements UrlMatcherInterface
23
{
24
    /**
25
     * Configuration key used to set the cache file path
26
     */
27
    public const CONFIG_CACHE_KEY = 'cache_key';
28
29
    /**
30
     * Configuration key used to set the cache file path
31
     */
32
    private string $cacheKey = 'routes-cache';
33
34
    /**
35
     * @var callable A factory callback that can return a dispatcher.
36
     */
37
    private $dispatcherCallback;
38
39
    /**
40
     * @var array Cached data used by the dispatcher.
41
     */
42
    private array $dispatchData = [];
43
44
    /**
45
     * @var bool Whether cache is enabled and valid dispatch data has been loaded from cache.
46
     */
47
    private bool $hasCache = false;
48
49
    private ?CacheInterface $cache;
50
51
    private RouteCollector $fastRouteCollector;
52
    private RouteCollectionInterface $routeCollection;
53
    private bool $hasInjectedRoutes = false;
54
55
    /**
56
     * Accepts optionally a FastRoute RouteCollector and a callable factory
57
     * that can return a FastRoute dispatcher.
58
     *
59
     * If either is not provided defaults will be used:
60
     *
61
     * - A RouteCollector instance will be created composing a RouteParser and
62
     *   RouteGenerator.
63
     * - A callable that returns a GroupCountBased dispatcher will be created.
64
     *
65
     * @param RouteCollector|null $fastRouteCollector If not provided, a default
66
     *     implementation will be used.
67
     * @param callable|null $dispatcherFactory Callable that will return a
68
     *     FastRoute dispatcher.
69
     * @param array|null $config Array of custom configuration options.
70
     */
71 22
    public function __construct(
72
        RouteCollectionInterface $routeCollection,
73
        CacheInterface $cache = null,
74
        ?array $config = null,
75
        RouteCollector $fastRouteCollector = null,
76
        callable $dispatcherFactory = null
77
    ) {
78 22
        $fastRouteCollector = $fastRouteCollector ?? $this->createRouteCollector();
79 22
        $this->routeCollection = $routeCollection;
80 22
        $this->fastRouteCollector = $fastRouteCollector;
81 22
        $this->dispatcherCallback = $dispatcherFactory;
82 22
        $this->loadConfig($config);
83 22
        $this->cache = $cache;
84
85 22
        $this->loadDispatchData();
86 22
    }
87
88 22
    public function match(ServerRequestInterface $request): MatchingResult
89
    {
90 22
        if (!$this->hasCache && !$this->hasInjectedRoutes) {
91 21
            $this->injectRoutes();
92
        }
93
94 22
        $dispatchData = $this->getDispatchData();
95 22
        $path = urldecode($request->getUri()->getPath());
96 22
        $method = $request->getMethod();
97 22
        $result = $this->getDispatcher($dispatchData)->dispatch($method, $request->getUri()->getHost() . $path);
98
99 22
        return $result[0] !== Dispatcher::FOUND
100 8
            ? $this->marshalFailedRoute($result)
101 22
            : $this->marshalMatchedRoute($result);
102
    }
103
104
    /**
105
     * Load configuration parameters
106
     *
107
     * @param array|null $config Array of custom configuration options.
108
     */
109 22
    private function loadConfig(?array $config): void
110
    {
111 22
        if ($config === null) {
0 ignored issues
show
introduced by
The condition $config === null is always false.
Loading history...
112 1
            return;
113
        }
114
115 21
        if (isset($config[self::CONFIG_CACHE_KEY])) {
116 21
            $this->cacheKey = (string) $config[self::CONFIG_CACHE_KEY];
117
        }
118 21
    }
119
120
    /**
121
     * Retrieve the dispatcher instance.
122
     *
123
     * Uses the callable factory in $dispatcherCallback, passing it $data
124
     * (which should be derived from the router's getData() method); this
125
     * approach is done to allow testing against the dispatcher.
126
     *
127
     * @param array|object $data Data from {@see RouteCollector::getData()}
128
     *
129
     * @return Dispatcher
130
     */
131 22
    private function getDispatcher($data): Dispatcher
132
    {
133 22
        if (!$this->dispatcherCallback) {
134 22
            $this->dispatcherCallback = $this->createDispatcherCallback();
135
        }
136
137 22
        $factory = $this->dispatcherCallback;
138
139 22
        return $factory($data);
140
    }
141
142
    /**
143
     * Create a default FastRoute Collector instance
144
     */
145 22
    private function createRouteCollector(): RouteCollector
146
    {
147 22
        return new RouteCollector(new RouteParser(), new RouteGenerator());
148
    }
149
150
    /**
151
     * Return a default implementation of a callback that can return a Dispatcher.
152
     */
153 22
    private function createDispatcherCallback(): callable
154
    {
155 22
        return static function ($data) {
156 22
            return new GroupCountBased($data);
157 22
        };
158
    }
159
160
    /**
161
     * Marshal a routing failure result.
162
     *
163
     * If the failure was due to the HTTP method, passes the allowed HTTP
164
     * methods to the factory.
165
     *
166
     * @param array $result
167
     *
168
     * @return MatchingResult
169
     */
170 8
    private function marshalFailedRoute(array $result): MatchingResult
171
    {
172 8
        $resultCode = $result[0];
173 8
        if ($resultCode === Dispatcher::METHOD_NOT_ALLOWED) {
174 3
            return MatchingResult::fromFailure($result[1]);
175
        }
176
177 5
        return MatchingResult::fromFailure(Method::ALL);
178
    }
179
180
    /**
181
     * Marshals a route result based on the results of matching.
182
     *
183
     * @param array $result
184
     *
185
     * @return MatchingResult
186
     */
187 14
    private function marshalMatchedRoute(array $result): MatchingResult
188
    {
189 14
        [, $name, $arguments] = $result;
190
191 14
        $route = $this->routeCollection->getRoute($name);
192
193 14
        $arguments = array_merge($route->getData('defaults'), $arguments);
194
195 14
        return MatchingResult::fromSuccess($route, $arguments);
196
    }
197
198
    /**
199
     * Inject routes into the underlying router
200
     */
201 21
    private function injectRoutes(): void
202
    {
203 21
        foreach ($this->routeCollection->getRoutes() as $route) {
204 20
            if (!$route->getData('hasMiddlewares')) {
205 1
                continue;
206
            }
207 19
            $hostPattern = $route->getData('host') ?? '{_host:[a-zA-Z0-9\.\-]*}';
208 19
            $methods = $route->getData('methods');
209 19
            $this->fastRouteCollector->addRoute(
210 19
                $methods,
211 19
                $hostPattern . $route->getData('pattern'),
212 19
                $route->getData('name')
213
            );
214
        }
215 21
        $this->hasInjectedRoutes = true;
216 21
    }
217
218
    /**
219
     * Get the dispatch data either from cache or freshly generated by the
220
     * FastRoute data generator.
221
     *
222
     * If caching is enabled, store the freshly generated data to file.
223
     */
224 22
    private function getDispatchData(): array
225
    {
226 22
        if ($this->hasCache) {
227 1
            return $this->dispatchData;
228
        }
229
230 21
        $dispatchData = (array)$this->fastRouteCollector->getData();
231
232 21
        if ($this->cache !== null) {
233 2
            $this->cacheDispatchData($dispatchData);
234
        }
235
236 21
        return $dispatchData;
237
    }
238
239
    /**
240
     * Load dispatch data from cache
241
     *
242
     * @throws RuntimeException If the cache file contains invalid data
243
     */
244 22
    private function loadDispatchData(): void
245
    {
246 22
        if ($this->cache !== null && $this->cache->has($this->cacheKey)) {
247 1
            $dispatchData = $this->cache->get($this->cacheKey);
248
249 1
            $this->hasCache = true;
250 1
            $this->dispatchData = $dispatchData;
251 1
            return;
252
        }
253
254 21
        $this->hasCache = false;
255 21
    }
256
257
    /**
258
     * Save dispatch data to cache
259
     *
260
     * @param array $dispatchData
261
     *
262
     * @throws RuntimeException If the cache directory does not exist.
263
     * @throws RuntimeException If the cache directory is not writable.
264
     * @throws RuntimeException If the cache file exists but is not writable
265
     */
266 2
    private function cacheDispatchData(array $dispatchData): void
267
    {
268 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

268
        $this->cache->/** @scrutinizer ignore-call */ 
269
                      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...
269 2
    }
270
}
271