Test Failed
Pull Request — master (#36)
by Divine Niiquaye
10:46 queued 08:19
created

Router::getCachedData()   A

Complexity

Conditions 6
Paths 9

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.0208

Importance

Changes 9
Bugs 0 Features 0
Metric Value
cc 6
eloc 11
nc 9
nop 1
dl 0
loc 21
ccs 11
cts 12
cp 0.9167
crap 6.0208
rs 9.2222
c 9
b 0
f 0
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\Interfaces\{RouteCompilerInterface, RouteMatcherInterface, UrlGeneratorInterface};
23
use Laminas\Stratigility\Next;
24
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface, UriInterface};
25
use Psr\Http\Server\{MiddlewareInterface, RequestHandlerInterface};
26
27
/**
28
 * Aggregate routes for matching and Dispatching.
29
 *
30
 * @author Divine Niiquaye Ibok <[email protected]>
31
 */
32
class Router implements RouteMatcherInterface, RequestMethodInterface, MiddlewareInterface, UrlGeneratorInterface
33
{
34
    use Traits\CacheTrait, Traits\ResolverTrait;
35
36
    /** @var array<int,string> Default methods for route. */
37
    public const DEFAULT_METHODS = [self::METHOD_GET, self::METHOD_HEAD];
38
39
    /**
40
     * Standard HTTP methods for browser requests.
41
     */
42
    public const HTTP_METHODS_STANDARD = [
43
        self::METHOD_HEAD,
44
        self::METHOD_GET,
45
        self::METHOD_POST,
46
        self::METHOD_PUT,
47
        self::METHOD_PATCH,
48
        self::METHOD_DELETE,
49
        self::METHOD_PURGE,
50
        self::METHOD_OPTIONS,
51
        self::METHOD_TRACE,
52
        self::METHOD_CONNECT,
53
    ];
54
55
    private RouteCompilerInterface $compiler;
56
    private ?\SplQueue $pipeline = null;
57
    private \Closure|RouteCollection|null $collection = null;
58
59
    /** @var array<string,array<int,MiddlewareInterface>> */
60
    private array $middlewares = [];
61
62
    /**
63
     * @param null|string $cache file path to store compiled routes
64
     */
65
    public function __construct(RouteCompilerInterface $compiler = null, string $cache = null)
66
    {
67 92
        $this->cache = $cache;
68
        $this->compiler = $compiler ?? new RouteCompiler();
69 92
    }
70 92
71
    /**
72
     * Set a route collection instance into Router in order to use addRoute method.
73
     *
74
     * @param null|string $cache file path to store compiled routes
75
     */
76
    public static function withCollection(
77
        \Closure|RouteCollection $collection = null,
78
        RouteCompilerInterface $compiler = null,
79
        string $cache = null
80 86
    ): static {
81
        $new = new static($compiler, $cache);
82 86
        $new->collection = $collection;
83 86
84
        return $new;
85 86
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function match(string $method, UriInterface $uri): ?array
91 78
    {
92
        return $this->optimized[$method.$uri->__toString()] ??= [$this, $this->cache ? 'resolveCache' : 'resolveRoute'](
93 78
            \rtrim(\rawurldecode($uri->getPath()), '/') ?: '/',
94
            $method,
95
            $uri
96
        );
97
    }
98
99 2
    /**
100
     * {@inheritdoc}
101 2
     */
102
    public function matchRequest(ServerRequestInterface $request): ?array
103
    {
104
        $requestUri = $request->getUri();
105
        $pathInfo = $request->getServerParams()['PATH_INFO'] ?? '';
106
107 7
        // Resolve request path to match sub-directory or /index.php/path
108
        if ('' !== $pathInfo && $pathInfo !== $requestUri->getPath()) {
109 7
            $requestUri = $requestUri->withPath($pathInfo);
110
        }
111
112
        return $this->match($request->getMethod(), $requestUri);
113
    }
114
115 7
    /**
116
     * {@inheritdoc}
117 7
     */
118
    public function generateUri(string $routeName, array $parameters = [], int $referenceType = RouteUri::ABSOLUTE_PATH): RouteUri
119 7
    {
120
        if (empty($matchedRoute = &$this->optimized[$routeName] ?? null)) {
121
            foreach ($this->getCollection()->getRoutes() as $route) {
122
                if (isset($route['name']) && $route['name'] === $routeName) {
123 7
                    $matchedRoute = $route;
124
                    break;
125
                }
126
            }
127
        }
128
129 56
        if (!isset($matchedRoute)) {
130
            throw new UrlGenerationException(\sprintf('Route "%s" does not exist.', $routeName));
131 56
        }
132 56
133
        return $this->compiler->generateUri($matchedRoute, $parameters, $referenceType)
134
            ?? throw new UrlGenerationException(\sprintf('%s::generateUri() not implemented in compiler.', $this->compiler::class));
135 56
    }
136 56
137
    /**
138
     * Attach middleware to the pipeline.
139
     */
140
    public function pipe(MiddlewareInterface ...$middlewares): void
141
    {
142
        if (null === $this->pipeline) {
143 4
            $this->pipeline = new \SplQueue();
144
        }
145 4
146
        foreach ($middlewares as $middleware) {
147
            $this->pipeline->enqueue($middleware);
148
        }
149
    }
150
151
    /**
152
     * Attach a name to a group of middlewares.
153 4
     */
154
    public function pipes(string $name, MiddlewareInterface ...$middlewares): void
155 4
    {
156
        $this->middlewares[$name] = $middlewares;
157
    }
158
159
    /**
160
     * Sets the RouteCollection instance associated with this Router.
161 91
     *
162
     * @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...
163 91
     */
164 3
    public function setCollection(callable $routeDefinitionCallback): void
165 88
    {
166 87
        $this->collection = $routeDefinitionCallback;
167
    }
168
169 5
    /**
170
     *  Get the RouteCollection instance associated with this Router.
171
     */
172
    public function getCollection(): RouteCollection
173
    {
174
        if ($this->cache) {
175
            return $this->optimized[2] ?? $this->doCache();
176
        }
177
178
        if ($this->collection instanceof \Closure) {
179
            ($this->collection)($this->collection = new RouteCollection());
180
        }
181
182
        return $this->collection ??= new RouteCollection();
0 ignored issues
show
Bug Best Practice introduced by
The expression return AssignCoalesceNode could return the type Closure which is incompatible with the type-hinted return Flight\Routing\RouteCollection. Consider adding an additional type-check to rule them out.
Loading history...
183
    }
184
185 3
    /**
186
     * Set a route compiler instance into Router.
187 3
     */
188
    public function setCompiler(RouteCompiler $compiler): void
189
    {
190
        $this->compiler = $compiler;
191
    }
192
193
    public function getCompiler(): RouteCompilerInterface
194
    {
195
        return $this->compiler;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
202
    {
203
        $route = $this->matchRequest($request);
204 92
205
        if (null !== $route) {
206 92
            foreach ($route['middlewares'] ?? [] as $a => $b) {
207
                if (isset($this->middlewares[$a])) {
208
                    $this->pipe(...$this->middlewares[$a]);
209
                }
210
            }
211
        }
212 77
213
        if (!empty($this->pipeline)) {
214 77
            $handler = new Next($this->pipeline, $handler);
215
        }
216 73
217 50
        return $handler->handle($request->withAttribute(self::class, $route));
218 2
    }
219
}
220