Issues (120)

src/Router.php (1 issue)

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 \Closure|RouteCollection|null $collection
73
     * @param null|string $cache file path to store compiled routes
74
     */
75 73
    public static function withCollection(
76
        \Closure|RouteCollection|null $collection = null,
77
        RouteCompilerInterface $compiler = null,
78
        string $cache = null
79
    ): static {
80 73
        $new = new static($compiler, $cache);
81 73
        $new->collection = $collection;
82
83 73
        return $new;
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89 76
    public function match(string $method, UriInterface $uri): ?array
90
    {
91 76
        return $this->optimized[$method.$uri->__toString()] ??= $this->{$this->cache ? 'resolveCache' : 'resolveRoute'}(
92 76
            \rtrim(\rawurldecode($uri->getPath()), '/') ?: '/',
93
            $method,
94
            $uri
95
        );
96
    }
97
98
    /**
99
     * {@inheritdoc}
100
     */
101 71
    public function matchRequest(ServerRequestInterface $request): ?array
102
    {
103 71
        $requestUri = $request->getUri();
104 71
        $pathInfo = $request->getServerParams()['PATH_INFO'] ?? '';
105
106
        // Resolve request path to match sub-directory or /index.php/path
107 71
        if ('' !== $pathInfo && $pathInfo !== $requestUri->getPath()) {
108 1
            $requestUri = $requestUri->withPath($pathInfo);
109
        }
110
111 71
        return $this->match($request->getMethod(), $requestUri);
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117 7
    public function generateUri(string $routeName, array $parameters = [], int $referenceType = RouteUri::ABSOLUTE_PATH): RouteUri
118
    {
119 7
        if (empty($matchedRoute = &$this->optimized[$routeName] ?? null)) {
120 7
            foreach ($this->getCollection()->getRoutes() as $route) {
121 6
                if (isset($route['name']) && $route['name'] === $routeName) {
122 6
                    $matchedRoute = $route;
123 6
                    break;
124
                }
125
            }
126
        }
127
128 7
        if (!isset($matchedRoute)) {
129 1
            throw new UrlGenerationException(\sprintf('Route "%s" does not exist.', $routeName));
130
        }
131
132 6
        return $this->compiler->generateUri($matchedRoute, $parameters, $referenceType)
133 6
            ?? throw new UrlGenerationException(\sprintf('%s::generateUri() not implemented in compiler.', $this->compiler::class));
134
    }
135
136
    /**
137
     * Attach middleware to the pipeline.
138
     */
139 65
    public function pipe(MiddlewareInterface ...$middlewares): void
140
    {
141 65
        if (null === $this->pipeline) {
142 65
            $this->pipeline = new \SplQueue();
143
        }
144
145 65
        foreach ($middlewares as $middleware) {
146 65
            $this->pipeline->enqueue($middleware);
147
        }
148
    }
149
150
    /**
151
     * Attach a name to a group of middlewares.
152
     */
153 1
    public function pipes(string $name, MiddlewareInterface ...$middlewares): void
154
    {
155 1
        $this->middlewares[$name] = $middlewares;
156
    }
157
158
    /**
159
     * Sets the RouteCollection instance associated with this Router.
160
     *
161
     * @param \Closure|RouteCollection $collection
162
     */
163 3
    public function setCollection(\Closure|RouteCollection $collection): void
164
    {
165 3
        $this->collection = $collection;
166
    }
167
168
    /**
169
     *  Get the RouteCollection instance associated with this Router.
170
     */
171 77
    public function getCollection(): RouteCollection
172
    {
173 77
        if ($this->cache) {
174 4
            return $this->optimized[2] ?? $this->doCache();
175
        }
176
177 73
        if ($this->collection instanceof \Closure) {
178 1
            ($this->collection)($this->collection = new RouteCollection());
179
        }
180
181 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...
182
    }
183
184
    /**
185
     * Set a route compiler instance into Router.
186
     */
187 3
    public function setCompiler(RouteCompiler $compiler): void
188
    {
189 3
        $this->compiler = $compiler;
190
    }
191
192 2
    public function getCompiler(): RouteCompilerInterface
193
    {
194 2
        return $this->compiler;
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 65
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
201
    {
202 65
        $route = $this->matchRequest($request);
203
204 65
        if (null !== $route) {
205 47
            foreach ($route['middlewares'] ?? [] as $a => $b) {
206 1
                if (isset($this->middlewares[$a])) {
207 1
                    $this->pipe(...$this->middlewares[$a]);
208
                }
209
            }
210
        }
211
212 65
        if (!empty($this->pipeline)) {
213 65
            $handler = new Next($this->pipeline, $handler);
214
        }
215
216 65
        return $handler->handle($request->withAttribute(self::class, $route));
217
    }
218
}
219