Passed
Pull Request — master (#44)
by Dmitriy
167:02 queued 155:13
created

UrlMatcher::marshalFailedRoute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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

320
        $this->cache->/** @scrutinizer ignore-call */ 
321
                      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...
321
    }
322
}
323