Passed
Push — master ( 56c5da...22541f )
by Divine Niiquaye
03:34
created

Router::getCollection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 6
ccs 4
cts 4
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.1 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 DivineNii\Invoker\Interfaces\InvokerInterface;
21
use DivineNii\Invoker\Invoker;
22
use Flight\Routing\Exceptions\DuplicateRouteException;
23
use Flight\Routing\Exceptions\MethodNotAllowedException;
24
use Flight\Routing\Exceptions\RouteNotFoundException;
25
use Flight\Routing\Exceptions\UriHandlerException;
26
use Flight\Routing\Exceptions\UrlGenerationException;
27
use Flight\Routing\Interfaces\RouteInterface;
28
use Flight\Routing\Interfaces\RouteListInterface;
29
use Flight\Routing\Interfaces\RouteMatcherInterface;
30
use Psr\Http\Message\ResponseFactoryInterface;
31
use Psr\Http\Message\ResponseInterface;
32
use Psr\Http\Message\ServerRequestInterface;
33
use Psr\Http\Message\UriFactoryInterface;
34
use Psr\Http\Message\UriInterface;
35
use Psr\Http\Server\RequestHandlerInterface;
36
37
/**
38
 * Router
39
 */
40
class Router implements RequestHandlerInterface
41
{
42
    use Traits\RouterTrait;
43
44
    public const TYPE_REQUIREMENT = 1;
45
46
    public const TYPE_DEFAULT = 0;
47
48
    public const TYPE_CACHE = 2;
49
50 77
    public function __construct(
51
        ResponseFactoryInterface $responseFactory,
52
        UriFactoryInterface $uriFactory,
53
        ?RouteMatcherInterface $matcher = null,
54
        ?InvokerInterface $resolver = null
55
    ) {
56 77
        $this->uriFactory      = $uriFactory;
57 77
        $this->responseFactory = $responseFactory;
58 77
        $this->resolver        = $resolver ?? new Invoker();
59 77
        $this->matcher         = $matcher ?? new Matchers\SimpleRouteMatcher();
60 77
    }
61
62
    /**
63
     * Adds the given route(s) to the router
64
     *
65
     * @param RouteInterface ...$routes
66
     *
67
     * @throws DuplicateRouteException
68
     */
69 62
    public function addRoute(RouteInterface ...$routes): void
70
    {
71 62
        foreach ($routes as $route) {
72 62
            $name = $route->getName();
73
74 62
            if (isset($this->routes[$name])) {
75 1
                throw new DuplicateRouteException(
76 1
                    \sprintf('A route with the name "%s" already exists.', $name)
77
                );
78
            }
79
80 62
            $this->routes[$name] = $this->mergeAttributes($route);
81
82 62
            if (null !== $this->debug) {
83 1
                $this->debug->addProfile(new DebugRoute($name, $route));
84
            }
85
        }
86 62
    }
87
88
    /**
89
     * Gets the router routes
90
     *
91
     * @return RouteListInterface
92
     */
93 57
    public function getCollection(): RouteListInterface
94
    {
95 57
        $collection = new RouteList();
96 57
        $collection->addForeach(...array_values($this->routes));
97
98 57
        return $collection;
99
    }
100
101
    /**
102
     * Generate a URI from the named route.
103
     *
104
     * Takes the named route and any parameters, and attempts to generate a
105
     * URI from it. Additional router-dependent query may be passed.
106
     *
107
     * Once there are no missing parameters in the URI we will encode
108
     * the URI and prepare it for returning to the user. If the URI is supposed to
109
     * be absolute, we will return it as-is. Otherwise we will remove the URL's root.
110
     *
111
     * @param string                       $routeName   route name
112
     * @param array<string,string>         $parameters  key => value option pairs to pass to the
113
     *                                                  router for purposes of generating a URI; takes precedence over options
114
     *                                                  present in route used to generate URI
115
     * @param array<int|string,int|string> $queryParams Optional query string parameters
116
     *
117
     * @throws UrlGenerationException if the route name is not known
118
     *                                or a parameter value does not match its regex
119
     *
120
     * @return UriInterface of fully qualified URL for named route
121
     */
122 4
    public function generateUri(string $routeName, array $parameters = [], array $queryParams = []): UriInterface
123
    {
124
        try {
125 4
            $route = $this->getRoute($routeName);
126 1
        } catch (RouteNotFoundException $e) {
127 1
            throw new UrlGenerationException(
128 1
                \sprintf(
129 1
                    'Unable to generate a URL for the named route "%s" as such route does not exist.',
130 1
                    $routeName
131
                ),
132 1
                404
133
            );
134
        }
135
136 3
        return $this->uriFactory->createUri($this->resolveUri($route, $parameters, $queryParams));
137
    }
138
139
    /**
140
     * Looks for a route that matches the given request
141
     *
142
     * @param ServerRequestInterface $request
143
     *
144
     * @throws MethodNotAllowedException
145
     * @throws UriHandlerException
146
     * @throws RouteNotFoundException
147
     *
148
     * @return RouteInterface
149
     */
150 51
    public function match(ServerRequestInterface $request): RouteInterface
151
    {
152
        // Get the request matching format.
153 51
        $route = $this->getMatcher()->match($this->getCollection(), $request);
154
155 47
        if (!$route instanceof RouteInterface) {
156 4
            throw new RouteNotFoundException(
157 4
                \sprintf(
158 4
                    'Unable to find the controller for path "%s". The route is wrongly configured.',
159 4
                    $request->getUri()->getPath()
160
                )
161
            );
162
        }
163
164 43
        $route->setArguments($this->mergeDefaults($route));
165
166 43
        if (null !== $this->debug) {
167 1
            $this->debug->setMatched(new DebugRoute($route->getName(), $route));
168
        }
169
170 43
        return $this->route = clone $route;
171
    }
172
173
    /**
174
     * {@inheritDoc}
175
     */
176 44
    public function handle(ServerRequestInterface $request): ResponseInterface
177
    {
178
        // Get the Route Handler ready for dispatching
179 44
        if (!$this->route instanceof RouteInterface) {
180 43
            $this->match($request);
181
        }
182
183 38
        $middlewareDispatcher = new Middlewares\MiddlewareDispatcher($this->resolver->getContainer());
184
185 38
        return $middlewareDispatcher->dispatch(
186 38
            $this->getMiddlewares(),
187 38
            new Handlers\CallbackHandler(
188 38
                function (ServerRequestInterface $request) use ($middlewareDispatcher): ResponseInterface {
189
                    try {
190 38
                        $route   = $request->getAttribute(Route::class, $this->route);
191 38
                        $handler = $this->resolveHandler($route);
192 38
                        $mididlewars = $this->resolveMiddlewares($route);
193
194 38
                        return $middlewareDispatcher->dispatch($mididlewars, $handler, $request);
195
                    } finally {
196 38
                        if (null !== $this->debug) {
197 38
                            foreach ($this->debug->getProfiles() as $profiler) {
198 1
                                $profiler->leave();
199
                            }
200
                        }
201
                    }
202 38
                }
203
            ),
204 38
            $request->withAttribute(Route::class, $this->route)
205
        );
206
    }
207
208
    /**
209
     * Gets the RouteMatcherInterface instance associated with this Router.
210
     *
211
     * @return RouteMatcherInterface
212
     */
213 54
    public function getMatcher(): RouteMatcherInterface
214
    {
215 54
        $cacheFile = $this->attributes[self::TYPE_CACHE] ?? '';
216
217 54
        if (is_null($this->debug) && (!empty($cacheFile) && !is_file($cacheFile))) {
218 1
            $this->generateCompiledRoutes($cacheFile);
219
        }
220
221 54
        if (file_exists($cacheFile)) {
222 2
            $this->matcher->warmCompiler($cacheFile);
223
        }
224
225 54
        return $this->matcher;
226
    }
227
}
228