Passed
Push — master ( 8df7ad...310ad9 )
by Divine Niiquaye
07:56
created

Router::setNamespace()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
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 Biurad\Annotations\LoaderInterface;
21
use Closure;
22
use DivineNii\Invoker\Interfaces\InvokerInterface;
23
use DivineNii\Invoker\Invoker;
24
use Flight\Routing\Exceptions\DuplicateRouteException;
25
use Flight\Routing\Exceptions\MethodNotAllowedException;
26
use Flight\Routing\Exceptions\RouteNotFoundException;
27
use Flight\Routing\Exceptions\UriHandlerException;
28
use Flight\Routing\Exceptions\UrlGenerationException;
29
use Flight\Routing\Interfaces\RouteCollectionInterface;
30
use Flight\Routing\Interfaces\RouteInterface;
31
use Flight\Routing\Interfaces\RouteListenerInterface;
32
use Flight\Routing\Interfaces\RouteMatcherInterface;
33
use Psr\Http\Message\ResponseFactoryInterface;
34
use Psr\Http\Message\ResponseInterface;
35
use Psr\Http\Message\ServerRequestInterface;
36
use Psr\Http\Message\UriFactoryInterface;
37
use Psr\Http\Message\UriInterface;
38
use Psr\Http\Server\RequestHandlerInterface;
39
40
/**
41
 * Router
42
 */
43
class Router implements RequestHandlerInterface
44
{
45 1
    use Traits\ResolveTrait;
46 1
    use Traits\ValidationTrait;
47 1
    use Traits\MiddlewareTrait;
48
49
    public const TYPE_REQUIREMENT = 1;
50
51
    public const TYPE_DEFAULT = 0;
52
53
    /** @var RouteMatcherInterface */
54
    private $matcher;
55
56
    /** @var ResponseFactoryInterface */
57
    private $responseFactory;
58
59
    /** @var UriFactoryInterface */
60
    private $uriFactory;
61
62
    /** @var string */
63
    private $namespace;
64
65
    /** @var RouteInterface[] */
66
    private $routes = [];
67
68
    /** @var RouteListenerInterface[] */
69
    private $listeners = [];
70
71
    /** @var array<int,array<string,mixed>> */
72
    private $attributes = [];
73
74
    /** @var null|DebugRoute */
75
    private $profiler;
76
77 77
    public function __construct(
78
        ResponseFactoryInterface $responseFactory,
79
        UriFactoryInterface $uriFactory,
80
        ?RouteMatcherInterface $matcher = null,
81
        ?InvokerInterface $resolver = null,
82
        bool $profileRoutes = false
83
    ) {
84 77
        $this->matcher = $matcher ?? new Services\SimpleRouteMatcher();
85
86 77
        $this->uriFactory      = $uriFactory;
87 77
        $this->responseFactory = $responseFactory;
88 77
        $this->resolver        = $resolver ?? new Invoker();
89
90 77
        if (false !== $profileRoutes) {
91 1
            $this->profiler = new DebugRoute();
92
        }
93 77
    }
94
95
    /**
96
     * Gets the router routes
97
     *
98
     * @return RouteInterface[]
99
     */
100 5
    public function getRoutes(): array
101
    {
102 5
        return \array_values($this->routes);
103
    }
104
105
    /**
106
     * Set Namespace for route handlers/controllers
107
     *
108
     * @param string $namespace
109
     */
110 4
    public function setNamespace(string $namespace): void
111
    {
112 4
        $this->namespace = \rtrim($namespace, '\\/') . '\\';
113 4
    }
114
115
    /**
116
     * Adds the given route(s) to the router
117
     *
118
     * @param RouteInterface ...$routes
119
     *
120
     * @throws DuplicateRouteException
121
     */
122 61
    public function addRoute(RouteInterface ...$routes): void
123
    {
124 61
        foreach ($routes as $route) {
125 61
            $name = $route->getName();
126
127 61
            if (isset($this->routes[$name])) {
128 1
                throw new DuplicateRouteException(
129 1
                    \sprintf('A route with the name "%s" already exists.', $name)
130
                );
131
            }
132
133 61
            if ($this->profiler instanceof DebugRoute) {
134 1
                $this->profiler->addProfile(new DebugRoute($name, $route));
135
            }
136
137 61
            $this->routes[$name] = $route;
138
        }
139 61
    }
140
141
    /**
142
     * Adds the given route(s) listener to the router
143
     *
144
     * @param RouteListenerInterface ...$listeners
145
     */
146 1
    public function addRouteListener(RouteListenerInterface ...$listeners): void
147
    {
148 1
        foreach ($listeners as $listener) {
149 1
            $this->listeners[] = $listener;
150
        }
151 1
    }
152
153
    /**
154
     * Adds parameters.
155
     *
156
     * This method implements a fluent interface.
157
     *
158
     * @param array<string,mixed> $parameters The parameters
159
     * @param int                 $type
160
     */
161 1
    public function addParameters(array $parameters, int $type = self::TYPE_REQUIREMENT): void
162
    {
163 1
        foreach ($parameters as $key => $regex) {
164 1
            if (self::TYPE_DEFAULT === $type) {
165 1
                $this->attributes[self::TYPE_DEFAULT] = [$key => $regex];
166
167 1
                continue;
168
            }
169
170 1
            $this->attributes[self::TYPE_REQUIREMENT] = [$key => $regex];
171
        }
172 1
    }
173
174
    /**
175
     * Gets allowed methods
176
     *
177
     * @return string[]
178
     */
179 1
    public function getAllowedMethods(): array
180
    {
181 1
        $methods = [];
182
183 1
        foreach ($this->routes as $route) {
184 1
            foreach ($route->getMethods() as $method) {
185 1
                $methods[$method] = true;
186
            }
187
        }
188
189 1
        return \array_keys($methods);
190
    }
191
192
    /**
193
     * Gets a route for the given name
194
     *
195
     * @param string $name
196
     *
197
     * @throws RouteNotFoundException
198
     *
199
     * @return RouteInterface
200
     */
201 6
    public function getRoute(string $name): RouteInterface
202
    {
203 6
        if (!isset($this->routes[$name])) {
204 2
            throw new RouteNotFoundException(\sprintf('No route found for the name "%s".', $name));
205
        }
206
207 4
        return $this->routes[$name];
208
    }
209
210
    /**
211
     * Get the profiled routes
212
     *
213
     * @return null|DebugRoute
214
     */
215 1
    public function getProfile(): ?DebugRoute
216
    {
217 1
        return $this->profiler;
218
    }
219
220
    /**
221
     * Generate a URI from the named route.
222
     *
223
     * Takes the named route and any parameters, and attempts to generate a
224
     * URI from it. Additional router-dependent query may be passed.
225
     *
226
     * Once there are no missing parameters in the URI we will encode
227
     * the URI and prepare it for returning to the user. If the URI is supposed to
228
     * be absolute, we will return it as-is. Otherwise we will remove the URL's root.
229
     *
230
     * @param string                       $routeName   route name
231
     * @param array<string,string>         $parameters  key => value option pairs to pass to the
232
     *                                                  router for purposes of generating a URI; takes precedence over options
233
     *                                                  present in route used to generate URI
234
     * @param array<int|string,int|string> $queryParams Optional query string parameters
235
     *
236
     * @throws UrlGenerationException if the route name is not known
237
     *                                or a parameter value does not match its regex
238
     *
239
     * @return UriInterface of fully qualified URL for named route
240
     */
241 4
    public function generateUri(string $routeName, array $parameters = [], array $queryParams = []): UriInterface
242
    {
243
        try {
244 4
            $route = $this->getRoute($routeName);
245 1
        } catch (RouteNotFoundException $e) {
246 1
            throw new UrlGenerationException(
247 1
                \sprintf(
248 1
                    'Unable to generate a URL for the named route "%s" as such route does not exist.',
249 1
                    $routeName
250
                ),
251 1
                404
252
            );
253
        }
254
255 3
        $prefix  = '.'; // Append missing "." at the beginning of the $uri.
256
257
        // Making routing on sub-folders easier
258 3
        if (\strpos($createdUri = $this->matcher->buildPath($route, $parameters), '/') !== 0) {
259 3
            $prefix .= '/';
260
        }
261
262
        // Incase query is added to uri.
263 3
        if (!empty($queryParams)) {
264 1
            $createdUri .= '?' . \http_build_query($queryParams);
265
        }
266
267 3
        if (\strpos($createdUri, '://') === false) {
268 2
            $createdUri = $prefix . $createdUri;
269
        }
270
271 3
        return $this->uriFactory->createUri(\rtrim($createdUri, '/'));
272
    }
273
274
    /**
275
     * Looks for a route that matches the given request
276
     *
277
     * @param ServerRequestInterface $request
278
     *
279
     * @throws MethodNotAllowedException
280
     * @throws UriHandlerException
281
     * @throws RouteNotFoundException
282
     *
283
     * @return RouteHandler
284
     */
285 50
    public function match(ServerRequestInterface $request): RouteHandler
286
    {
287 50
        $requestUri  = $request->getUri();
288 50
        $requestPath = $this->resolvePath($request, $requestUri->getPath());
289
290
        // Get the request matching format.
291 50
        $route = $this->marshalMatchedRoute($request->getMethod(), $requestUri, $requestPath);
292
293 46
        if (!$route instanceof RouteInterface) {
294 3
            throw new RouteNotFoundException(
295 3
                \sprintf(
296 3
                    'Unable to find the controller for path "%s". The route is wrongly configured.',
297 3
                    $requestPath
298
                )
299
            );
300
        }
301
302 43
        $this->resolveListeners($request, $route);
303
304 43
        if ($this->profiler instanceof DebugRoute) {
305 1
            $this->profiler->setMatched($route->getName());
306
        }
307
308 43
        return new RouteHandler(
309 43
            Closure::fromCallable([$this, 'resolveRoute']),
310 43
            $this->responseFactory->createResponse()
311
        );
312
    }
313
314
    /**
315
     * {@inheritDoc}
316
     */
317 46
    public function handle(ServerRequestInterface $request): ResponseInterface
318
    {
319
        // Get the Route Handler ready for dispatching
320 46
        $routingResults = $this->match($request);
321
322
        try {
323 41
            if (\count($this->getMiddlewares()) > 0) {
324
                // This middleware is in the priority map; but, this is the first middleware we have
325
                // encountered from the map thus far. We'll save its current index plus its index
326
                // from the priority map so we can compare against them on the next iterations.
327 26
                return $this->pipeline()
328 23
                    ->process($request->withAttribute(Route::class, $this->route), $routingResults);
329
            }
330
331 15
            return $routingResults->handle($request);
332
        } finally {
333 41
            if ($this->profiler instanceof DebugRoute) {
334 41
                foreach ($this->profiler->getProfiles() as $profiler) {
335 1
                    $profiler->leave();
336
                }
337
            }
338
        }
339
    }
340
341
    /**
342
     * Load routes from annotation.
343
     *
344
     * @param LoaderInterface $loader
345
     */
346 15
    public function loadAnnotation(LoaderInterface $loader): void
347
    {
348 15
        foreach ($loader->load() as $annotation) {
349 4
            if ($annotation instanceof RouteCollectionInterface) {
350 4
                $this->addRoute(...$annotation);
351
            }
352
        }
353 4
    }
354
355
    /**
356
     * Marshals a route result based on the results of matching URL from set of routes.
357
     *
358
     * @param string $method
359
     * @param UriInterface $uri
360
     * @param string $path
361
     *
362
     * @throws MethodNotAllowedException
363
     * @throws UriHandlerException
364
     *
365
     * @return null|RouteInterface
366
     */
367 50
    private function marshalMatchedRoute(string $method, UriInterface $uri, string $path): ?RouteInterface
368
    {
369 50
        [$scheme, $host] = [$uri->getScheme(), $uri->getHost()];
370
371 50
        foreach ($this->routes as $route) {
372
            // Let's match the routes
373 49
            $match         = $this->matcher->compileRoute($this->mergeAttributes($route));
374 49
            $uriParameters = $hostParameters = [];
375
376
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
377 49
            if (!$this->compareUri($match->getRegex(), $path, $uriParameters)) {
378 13
                continue;
379
            }
380
381 47
            if (!$this->compareDomain($match->getRegex(true), $host, $hostParameters)) {
382 1
                throw new UriHandlerException(
383 1
                    \sprintf('Unfortunately current domain "%s" is not allowed on requested uri [%s]', $host, $path),
384 1
                    400
385
                );
386
            }
387
388 46
            $this->assertRoute($route, [$method, $scheme, $path]);
389 43
            $parameters = \array_replace($uriParameters, $hostParameters) ?? $match->getVariables();
390
391 43
            return $route->setArguments($this->mergeDefaults($parameters, $route->getDefaults()));
392
        }
393
394 3
        return null;
395
    }
396
}
397