Passed
Push — master ( 7c5b4e...95d817 )
by Divine Niiquaye
23:25 queued 11:43
created

Router::generateUri()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2.032

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 2
b 0
f 0
nc 2
nop 3
dl 0
loc 9
ccs 4
cts 5
cp 0.8
crap 2.032
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\Exceptions\UrlGenerationException;
22
use Flight\Routing\Generator\GeneratedUri;
23
use Flight\Routing\Interfaces\{RouteCompilerInterface, RouteMatcherInterface, UrlGeneratorInterface};
24
use Laminas\Stratigility\Next;
25
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface, UriInterface};
26
use Psr\Http\Server\{MiddlewareInterface, RequestHandlerInterface};
27
use Symfony\Component\VarExporter\VarExporter;
28
29
/**
30
 * Aggregate routes for matching and Dispatching.
31
 *
32
 * @author Divine Niiquaye Ibok <[email protected]>
33
 */
34
class Router implements RouteMatcherInterface, RequestMethodInterface, MiddlewareInterface, UrlGeneratorInterface
35
{
36
    /**
37
     * Standard HTTP methods for browser requests.
38
     */
39
    public const HTTP_METHODS_STANDARD = [
40
        self::METHOD_HEAD,
41
        self::METHOD_GET,
42
        self::METHOD_POST,
43
        self::METHOD_PUT,
44
        self::METHOD_PATCH,
45
        self::METHOD_DELETE,
46
        self::METHOD_PURGE,
47
        self::METHOD_OPTIONS,
48
        self::METHOD_TRACE,
49
        self::METHOD_CONNECT,
50
    ];
51
52
    private ?RouteCompilerInterface $compiler;
53
    private ?RouteMatcherInterface $matcher = null;
54
    private ?\SplQueue $pipeline = null;
55
    private string $matcherClass = RouteMatcher::class;
56
    private ?string $cacheData;
57
58
    /** @var array<string,array<int,MiddlewareInterface>> */
59
    private array $middlewares = [];
60
61
    /** @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...
62
    private $collection;
63
64
    /**
65
     * @param string|null $cache file path to store compiled routes
66
     */
67 92
    public function __construct(RouteCompilerInterface $compiler = null, string $cache = null)
68
    {
69 92
        $this->compiler = $compiler;
70 92
        $this->cacheData = $cache;
71
    }
72
73
    /**
74
     * Set a route collection instance into Router in order to use addRoute method.
75
     *
76
     * @param string|null $cache file path to store compiled routes
77
     *
78
     * @return static
79
     */
80 86
    public static function withCollection(RouteCollection $collection = null, RouteCompilerInterface $compiler = null, string $cache = null)
81
    {
82 86
        $new = new static($compiler, $cache);
83 86
        $new->collection = $collection ?? new RouteCollection();
84
85 86
        return $new;
86
    }
87
88
    /**
89
     * This method works only if withCollection method is used.
90
     */
91 78
    public function addRoute(Route ...$routes): void
92
    {
93 78
        $this->getCollection()->routes($routes, false);
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99 2
    public function match(string $method, UriInterface $uri): ?Route
100
    {
101 2
        return $this->getMatcher()->match($method, $uri);
102
    }
103
104
    /**
105
     * {@inheritdoc}
106
     */
107 7
    public function matchRequest(ServerRequestInterface $request): ?Route
108
    {
109 7
        return $this->getMatcher()->matchRequest($request);
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115 7
    public function generateUri(string $routeName, array $parameters = [], int $referenceType = GeneratedUri::ABSOLUTE_PATH): GeneratedUri
116
    {
117 7
        $matcher = $this->getMatcher();
118
119 7
        if (!$matcher instanceof UrlGeneratorInterface) {
120
            throw new UrlGenerationException(\sprintf('The route matcher does not support using the %s implementation', UrlGeneratorInterface::class));
121
        }
122
123 7
        return $matcher->generateUri($routeName, $parameters, $referenceType);
124
    }
125
126
    /**
127
     * Attach middleware to the pipeline.
128
     */
129 56
    public function pipe(MiddlewareInterface ...$middlewares): void
130
    {
131 56
        if (null === $this->pipeline) {
132 56
            $this->pipeline = new \SplQueue();
133
        }
134
135 56
        foreach ($middlewares as $middleware) {
136 56
            $this->pipeline->enqueue($middleware);
137
        }
138
    }
139
140
    /**
141
     * Attach a name to a group of middlewares.
142
     */
143 4
    public function pipes(string $name, MiddlewareInterface ...$middlewares): void
144
    {
145 4
        $this->middlewares[$name] = $middlewares;
146
    }
147
148
    /**
149
     * Sets the RouteCollection instance associated with this Router.
150
     *
151
     * @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...
152
     */
153 4
    public function setCollection(callable $routeDefinitionCallback): void
154
    {
155 4
        $this->collection = $routeDefinitionCallback;
156
    }
157
158
    /**
159
     *  Get the RouteCollection instance associated with this Router.
160
     */
161 91
    public function getCollection(): RouteCollection
162
    {
163 91
        if (\is_callable($collection = $this->collection)) {
164 3
            $collection($collection = new RouteCollection());
165 88
        } elseif (null !== $collection) {
166 87
            return $this->collection;
167
        }
168
169 5
        return $this->collection = $collection ?? new RouteCollection();
170
    }
171
172
    /**
173
     * Set where cached data will be stored.
174
     *
175
     * @param string $cache file path to store compiled routes
176
     */
177
    public function setCache(string $cache): void
178
    {
179
        $this->cacheData = $cache;
180
    }
181
182
    /**
183
     * If RouteCollection's data has been cached.
184
     */
185 3
    public function isCached(): bool
186
    {
187 3
        return ($cache = $this->cacheData) && \file_exists($cache);
188
    }
189
190
    /**
191
     * Set a matcher class associated with this Router.
192
     */
193
    public function setMatcher(string $matcherClass): void
194
    {
195
        if (!\is_subclass_of($matcherClass, RouteMatcherInterface::class)) {
196
            throw new \InvalidArgumentException(\sprintf('"%s" must be a subclass of "%s".', $matcherClass, RouteMatcherInterface::class));
197
        }
198
        $this->matcherClass = $matcherClass;
199
    }
200
201
    /**
202
     * Gets the Route matcher instance associated with this Router.
203
     */
204 92
    public function getMatcher(): RouteMatcherInterface
205
    {
206 92
        return $this->matcher ??= $this->cacheData ? $this->getCachedData($this->cacheData) : new $this->matcherClass($this->getCollection(), $this->compiler);
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212 77
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
213
    {
214 77
        $route = $this->getMatcher()->matchRequest($request);
215
216 73
        if (null !== $route) {
217 50
            foreach ($route->getPiped() as $middleware) {
218 2
                if (isset($this->middlewares[$middleware])) {
219 2
                    $this->pipe(...$this->middlewares[$middleware]);
220
                }
221
            }
222
        }
223
224 73
        if (null !== $this->pipeline) {
225 56
            $handler = new Next($this->pipeline, $handler);
226
        }
227
228 73
        return $handler->handle($request->withAttribute(Route::class, $route));
229
    }
230
231 2
    protected function getCachedData(string $cache): RouteMatcherInterface
232
    {
233 2
        $cachedData = @include $cache;
234
235 2
        if (!$cachedData instanceof RouteMatcherInterface) {
236 1
            $cachedData = new $this->matcherClass($this->getCollection(), $this->compiler);
237 1
            $dumpData = \class_exists(VarExporter::class) ? VarExporter::export($cachedData) : "\unserialize(<<<'SERIALIZED'\n" . \serialize($cachedData) . "\nSERIALIZED)";
238
239 1
            if (!\is_dir($directory = \dirname($cache))) {
240
                @\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

240
                /** @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...
241
            }
242
243 1
            \file_put_contents($cache, "<?php // auto generated: AVOID MODIFYING\n\nreturn " . $dumpData . ";\n");
244
245 1
            if (\function_exists('opcache_invalidate') && \filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) {
246 1
                @\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

246
                /** @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...
247
            }
248 1
            $cachedData = require $cache;
249
        }
250
251 2
        return $cachedData;
252
    }
253
}
254