Test Failed
Pull Request — master (#77)
by Rustam
07:27
created

UrlMatcher::getCurrentRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
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\Route;
18
use Yiisoft\Router\RouteCollectionInterface;
19
use Yiisoft\Router\RouteParametersInterface;
20
use Yiisoft\Router\CurrentRoute;
0 ignored issues
show
Bug introduced by
The type Yiisoft\Router\CurrentRoute was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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

314
        $this->cache->/** @scrutinizer ignore-call */ 
315
                      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...
315
    }
316
}
317