Passed
Branch master (7bc80b)
by Divine Niiquaye
02:52
created

Router::generateUri()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

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