Passed
Push — master ( e57067...b75601 )
by Alexander
02:44 queued 10s
created

UrlMatcher::marshalMatchedRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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

278
        $this->cache->/** @scrutinizer ignore-call */ 
279
                      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...
279 2
    }
280
}
281