Test Failed
Pull Request — master (#31)
by Divine Niiquaye
03:07
created

Router::setMatcher()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 7
ccs 2
cts 2
cp 1
crap 2
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Flight Routing.
7
 *
8
 * PHP version 7.4 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Flight\Routing;
19
20
use Fig\Http\Message\RequestMethodInterface;
21
use Flight\Routing\Generator\GeneratedUri;
22
use Flight\Routing\Interfaces\{RouteCompilerInterface, RouteMatcherInterface};
23
use Laminas\Stratigility\Next;
24
use Psr\Cache\CacheItemPoolInterface;
25
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface, UriInterface};
26
use Psr\Http\Server\{MiddlewareInterface, RequestHandlerInterface};
27
28
/**
29
 * Aggregate routes for matching and Dispatching.
30
 *
31
 * @author Divine Niiquaye Ibok <[email protected]>
32
 */
33
class Router implements RouteMatcherInterface, RequestMethodInterface, MiddlewareInterface
34
{
35
    /**
36
     * Standard HTTP methods for browser requests.
37
     */
38
    public const HTTP_METHODS_STANDARD = [
39
        self::METHOD_HEAD,
40
        self::METHOD_GET,
41
        self::METHOD_POST,
42
        self::METHOD_PUT,
43
        self::METHOD_PATCH,
44
        self::METHOD_DELETE,
45
        self::METHOD_PURGE,
46
        self::METHOD_OPTIONS,
47
        self::METHOD_TRACE,
48
        self::METHOD_CONNECT,
49
    ];
50
51
    private \SplQueue $pipeline;
52
    private ?RouteCompilerInterface $compiler;
53
    private ?RouteMatcherInterface $matcher = null;
54
    private string $matcherClass = RouteMatcher::class;
55
56
    /** @var RouteCollection|(callable(RouteCollection): void)|null */
0 ignored issues
show
Documentation Bug introduced by
The doc comment RouteCollection|(callabl...Collection): void)|null at position 3 could not be parsed: Expected ')' at position 3, but found 'callable'.
Loading history...
57
    private $collection;
58
59 73
    /** @var CacheItemPoolInterface|string|null */
60
    private $cacheData;
61
62
    /** @var array<string,array<int,MiddlewareInterface>> */
63
    private array $middlewares = [];
64
65 73
    /**
66 73
     * @param CacheItemPoolInterface|string|null $cache use file path or PSR-6 cache
67 73
     */
68
    public function __construct(RouteCompilerInterface $compiler = null, $cache = null)
69 73
    {
70 73
        $this->compiler = $compiler ?? new RouteCompiler();
71 73
        $this->pipeline = new \SplQueue();
72 73
        $this->cacheData = $cache;
73
    }
74
75
    /**
76
     * Set a route collection instance into Router in order to use addRoute method.
77
     *
78
     * @param CacheItemPoolInterface|string|null $cache use file path or PSR-6 cache
79
     *
80
     * @return static
81
     */
82
    public static function withCollection(RouteCollection $collection = null, RouteCompilerInterface $compiler = null, $cache = null)
83
    {
84
        $new = new static($compiler, $cache);
85
        $new->collection = $collection ?? new RouteCollection();
86
87
        return $new;
88 73
    }
89
90 73
    /**
91
     * This method works only if withCollection method is used.
92
     */
93
    public function addRoute(Route ...$routes): void
94
    {
95
        if ($this->collection instanceof RouteCollection) {
96
            $this->collection->routes($routes);
97
        }
98
    }
99
100 73
    /**
101
     * {@inheritdoc}
102 73
     */
103 73
    public function match(string $method, UriInterface $uri): ?Route
104 73
    {
105
        return $this->getMatcher()->match($method, $uri);
106
    }
107
108
    /**
109
     * {@inheritdoc}
110 73
     */
111
    public function matchRequest(ServerRequestInterface $request): ?Route
112
    {
113
        return $this->getMatcher()->matchRequest($request);
114
    }
115
116 73
    /**
117 3
     * {@inheritdoc}
118
     */
119
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
120
    {
121 73
        return $this->getMatcher()->generateUri($routeName, $parameters);
122 4
    }
123
124 73
    /**
125
     * Attach middleware to the pipeline.
126
     */
127
    public function pipe(MiddlewareInterface ...$middlewares): void
128
    {
129 59
        foreach ($middlewares as $middleware) {
130
            $this->pipeline->enqueue($middleware);
131 59
        }
132 3
    }
133
134
    /**
135 56
     * Attach a name to a group of middlewares.
136
     */
137
    public function pipes(string $name, MiddlewareInterface ...$middlewares): void
138
    {
139
        $this->middlewares[$name] = $middlewares;
140
    }
141
142
    /**
143
     * Sets the RouteCollection instance associated with this Router.
144
     *
145 60
     * @param (callable(RouteCollection): void) $routeDefinitionCallback takes only one parameter of route collection
0 ignored issues
show
Documentation Bug introduced by
The doc comment (callable(RouteCollection): void) at position 1 could not be parsed: Expected ')' at position 1, but found 'callable'.
Loading history...
146
     */
147 60
    public function setCollection(callable $routeDefinitionCallback): void
148 60
    {
149 29
        $this->collection = $routeDefinitionCallback;
150
    }
151
152 60
    /**
153 1
     *  Get the RouteCollection instance associated with this Router.
154 1
     */
155
    public function getCollection(): RouteCollection
156
    {
157
        if (\is_callable($collection = $this->collection)) {
158 60
            $collection($collection = new RouteCollection());
159
        } elseif (null === $collection) {
160 60
            throw new \RuntimeException(\sprintf('Did you forget to set add the route collection with the "%s".', __CLASS__ . '::setCollection'));
161
        }
162
163
        return $this->collection = $collection;
164
    }
165
166
    /**
167 24
     * Set where cached data will be stored.
168
     *
169 24
     * @param CacheItemPoolInterface|string $cache use file path or PSR-6 cache
170 2
     */
171
    public function setCache($cache): void
172
    {
173 24
        $this->cacheData = $cache;
174 24
    }
175
176
    /**
177
     * If RouteCollection's data has been cached.
178
     */
179 12
    public function isCached(): bool
180
    {
181 12
        if (null === $cache = $this->cacheData) {
182
            return false;
183
        }
184
185
        return ($cache instanceof CacheItemPoolInterface && $cache->hasItem(__FILE__)) || \file_exists($cache);
0 ignored issues
show
Bug introduced by
It seems like $cache can also be of type Psr\Cache\CacheItemPoolInterface; however, parameter $filename of file_exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

185
        return ($cache instanceof CacheItemPoolInterface && $cache->hasItem(__FILE__)) || \file_exists(/** @scrutinizer ignore-type */ $cache);
Loading history...
186
    }
187
188
    /**
189 4
     * Sets the route matcher class to use.
190
     */
191 4
    public function setMatcher(string $matcherClass): void
192
    {
193 4
        if (!\is_subclass_of($matcherClass, RouteMatcherInterface::class)) {
194
            throw new \InvalidArgumentException(\sprintf('"%s" must be a subclass of "%s".', $matcherClass, RouteMatcherInterface::class));
195
        }
196
197
        $this->matcherClass = $matcherClass;
198
    }
199
200
    /**
201
     * Gets the Route matcher instance associated with this Router.
202
     */
203 55
    public function getMatcher(): RouteMatcherInterface
204
    {
205
        return $this->matcher ??= $this->cacheData ? $this->getCachedData($this->cacheData) : new $this->matcherClass($this->getCollection(), $this->compiler);
206 55
    }
207
208 50
    /**
209 4
     * {@inheritdoc}
210 4
     */
211 4
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
212 4
    {
213
        $route = $this->getMatcher()->matchRequest($request);
214
215
        if (null !== $route) {
216
            foreach ($route->getPiped() as $middleware) {
217 46
                foreach ($this->middlewares[$middleware] ?? [] as $pipedMiddleware) {
218
                    $this->pipeline->enqueue($pipedMiddleware);
219 46
                }
220 3
            }
221
        }
222
223 46
        return (new Next($this->pipeline, $handler))->handle($request->withAttribute(Route::class, $route));
224
    }
225
226
    /**
227
     * @param CacheItemPoolInterface|string $cache
228
     */
229 48
    protected function getCachedData($cache): RouteMatcherInterface
230
    {
231
        if ($cache instanceof CacheItemPoolInterface) {
232
            $cachedData = ($cacheItem = $cache->getItem(__FILE__))->get();
233 48
234
            if (!$cachedData instanceof RouteMatcherInterface) {
235
                $cache->deleteItem(__FILE__);
236
                $cache->save($cacheItem->set(new $this->matcherClass($this->getCollection(), $this->compiler)));
237 48
            }
238 41
239
            return $cacheItem->get();
240 41
        }
241 31
242 31
        $cachedData = @include $cache;
243 30
244 4
        if (!$cachedData instanceof RouteMatcherInterface) {
245
            $dumpData = "<?php // auto generated: AVOID MODIFYING\n\n \$data = ";
246
            $dumpData .= \var_export([
247 30
                'compiled' => $this->compiler->build($c = $this->getCollection()),
0 ignored issues
show
Bug introduced by
The method build() 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

247
                'compiled' => $this->compiler->/** @scrutinizer ignore-call */ build($c = $this->getCollection()),

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...
248 31
                'routes' => $c->getRoutes(),
249 31
            ], true);
250
251
            if (!\is_dir($directory = \dirname($cache))) {
252
                @\mkdir($directory, 0775, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

252
                /** @scrutinizer ignore-unhandled */ @\mkdir($directory, 0775, true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
253
            }
254 41
            \file_put_contents($cache, $dumpData . ";\nreturn \\" . $this->matcherClass . "::__set_state(\$data);\n");
255
256
            if (\function_exists('opcache_invalidate') && \filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
257 39
                @\opcache_invalidate($cache, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for opcache_invalidate(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

257
                /** @scrutinizer ignore-unhandled */ @\opcache_invalidate($cache, true);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
258
            }
259 39
260 39
            return require $cache;
261
        }
262
263
        return $cachedData;
264
    }
265
}
266