Test Failed
Push — master ( 17fbcb...a3ed53 )
by Divine Niiquaye
11:03
created

Router::setCache()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
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
    /** @var array<string,MiddlewareInterface[]> */
52
    private array $middlewares = [];
53
54
    private \SplQueue $pipeline;
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
    private ?RouteCompilerInterface $compiler;
60
61
    private ?RouteMatcherInterface $matcher = null;
62
63
    /** @var CacheItemPoolInterface|string|null */
64
    private $cacheData;
65 73
66 73
    /**
67 73
     * @param CacheItemPoolInterface|string|null $cache use file path or PSR-6 cache
68
     */
69 73
    public function __construct(RouteCompilerInterface $compiler = null, $cache = null)
70 73
    {
71 73
        $this->compiler = $compiler;
72 73
        $this->pipeline = new \SplQueue();
73
        $this->cacheData = $cache;
74
    }
75
76
    /**
77
     * Set a route collection instance into Router in order to use addRoute method.
78
     *
79
     * @param CacheItemPoolInterface|string|null $cache use file path or PSR-6 cache
80
     *
81
     * @return static
82
     */
83
    public static function withCollection(RouteCollection $collection = null, RouteCompilerInterface $compiler = null, $cache = null)
84
    {
85
        $new = new static($compiler, $cache);
86
        $new->collection = $collection ?? new RouteCollection();
87
88 73
        return $new;
89
    }
90 73
91
    /**
92
     * This method works only if withCollection method is used.
93
     */
94
    public function addRoute(Route ...$routes): void
95
    {
96
        if ($this->collection instanceof RouteCollection) {
97
            $this->collection->routes($routes);
98
        }
99
    }
100 73
101
    /**
102 73
     * {@inheritdoc}
103 73
     */
104 73
    public function match(string $method, UriInterface $uri): ?Route
105
    {
106
        return $this->getMatcher()->match($method, $uri);
107
    }
108
109
    /**
110 73
     * {@inheritdoc}
111
     */
112
    public function matchRequest(ServerRequestInterface $request): ?Route
113
    {
114
        return $this->getMatcher()->matchRequest($request);
115
    }
116 73
117 3
    /**
118
     * {@inheritdoc}
119
     */
120
    public function generateUri(string $routeName, array $parameters = []): GeneratedUri
121 73
    {
122 4
        return $this->getMatcher()->generateUri($routeName, $parameters);
123
    }
124 73
125
    /**
126
     * Attach middleware to the pipeline.
127
     */
128
    public function pipe(MiddlewareInterface ...$middlewares): void
129 59
    {
130
        foreach ($middlewares as $middleware) {
131 59
            $this->pipeline->enqueue($middleware);
132 3
        }
133
    }
134
135 56
    /**
136
     * Attach a name to a group of middlewares.
137
     */
138
    public function pipes(string $name, MiddlewareInterface ...$middlewares): void
139
    {
140
        $this->middlewares[$name] = $middlewares;
141
    }
142
143
    /**
144
     * Sets the RouteCollection instance associated with this Router.
145 60
     *
146
     * @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...
147 60
     */
148 60
    public function setCollection(callable $routeDefinitionCallback): void
149 29
    {
150
        $this->collection = $routeDefinitionCallback;
151
    }
152 60
153 1
    /**
154 1
     *  Get the RouteCollection instance associated with this Router.
155
     */
156
    public function getCollection(): RouteCollection
157
    {
158 60
        if (\is_callable($collection = $this->collection)) {
159
            $collection($collection = new RouteCollection());
160 60
        } elseif (null === $collection) {
161
            throw new \RuntimeException(\sprintf('Did you forget to set add the route collection with the "%s".', __CLASS__ . '::setCollection'));
162
        }
163
164
        return $this->collection = $collection;
165
    }
166
167 24
    /**
168
     * Set where cached data will be stored.
169 24
     *
170 2
     * @param CacheItemPoolInterface|string $cache use file path or PSR-6 cache
171
     *
172
     * @return void
173 24
     */
174 24
    public function setCache($cache): void
175
    {
176
        $this->cacheData = $cache;
177
    }
178
179 12
    /**
180
     * If RouteCollection's data has been cached.
181 12
     */
182
    public function isCached(): bool
183
    {
184
        if (null === $cache = $this->cacheData) {
185
            return false;
186
        }
187
188
        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

188
        return ($cache instanceof CacheItemPoolInterface && $cache->hasItem(__FILE__)) || \file_exists(/** @scrutinizer ignore-type */ $cache);
Loading history...
189 4
    }
190
191 4
    /**
192
     * Gets the Route matcher instance associated with this Router.
193 4
     */
194
    public function getMatcher(): RouteMatcherInterface
195
    {
196
        return $this->matcher
197
            ?? $this->matcher = (
198
                $this->cacheData
199
                    ? $this->getCachedData($this->cacheData)
200
                    : new RouteMatcher($this->getCollection(), $this->compiler)
201
            );
202
    }
203 55
204
    /**
205
     * {@inheritdoc}
206 55
     */
207
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
208 50
    {
209 4
        $route = $this->getMatcher()->matchRequest($request);
210 4
211 4
        if (null !== $route) {
212 4
            foreach ($route->getPiped() as $middleware) {
213
                foreach ($this->middlewares[$middleware] ?? [] as $pipedMiddleware) {
214
                    $this->pipeline->enqueue($pipedMiddleware);
215
                }
216
            }
217 46
        }
218
219 46
        return (new Next($this->pipeline, $handler))->handle($request->withAttribute(Route::class, $route));
220 3
    }
221
222
    /**
223 46
     * @param CacheItemPoolInterface|string $cache
224
     */
225
    protected function getCachedData($cache): RouteMatcherInterface
226
    {
227
        if ($cache instanceof CacheItemPoolInterface) {
228
            $cachedData = $cache->getItem(__FILE__)->get();
229 48
230
            if (!$cachedData instanceof RouteMatcherInterface) {
231
                $cache->deleteItem(__FILE__);
232
                $cache->save($cache->getItem(__FILE__)->set($cachedData = new RouteMatcher($this->getCollection(), $this->compiler)));
233 48
            }
234
235
            return $cachedData;
236
        }
237 48
238 41
        $cachedData = @include $cache;
239
240 41
        if (!$cachedData instanceof RouteMatcherInterface) {
241 31
            $dumpData = "<<<'SERIALIZED'\n" . \serialize($cachedData = new RouteMatcher($this->getCollection(), $this->compiler)) . "\nSERIALIZED";
242 31
243 30
            if (!\is_dir($directory = \dirname($cache))) {
244 4
                @\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

244
                /** @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...
245
            }
246
247 30
            \file_put_contents($cache, "<?php // auto generated: AVOID MODIFYING\n\nreturn \unserialize(" . $dumpData . ");\n");
248 31
249 31
            if (\function_exists('opcache_invalidate') && \filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
250
                @\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

250
                /** @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...
251
            }
252
        }
253
254 41
        return $cachedData;
255
    }
256
}
257