Test Failed
Push — master ( b83ac1...f35ed8 )
by Divine Niiquaye
03:05
created

Router   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 395
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 121
c 4
b 0
f 0
dl 0
loc 395
rs 8.8798
ccs 112
cts 112
cp 1
wmc 44

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getRoutes() 0 3 1
A handle() 0 20 2
A match() 0 33 6
A getAllowedMethods() 0 11 3
A setNamespace() 0 3 1
A assertRoute() 0 20 4
A getRoute() 0 7 2
A __construct() 0 12 1
A marshalMatchedRoute() 0 21 3
A generateResponse() 0 23 6
A generateUri() 0 30 5
A setProfiler() 0 3 1
A addParameters() 0 10 3
A addRoute() 0 15 4
A addRouteListener() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like Router often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Router, and based on these observations, apply Extract Interface, too.

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\RouteListenerInterface;
29
use Flight\Routing\Interfaces\RouteMatcherInterface;
30
use Psr\Container\ContainerInterface;
31
use Psr\Http\Message\ResponseFactoryInterface;
32
use Psr\Http\Message\ResponseInterface;
33
use Psr\Http\Message\ServerRequestInterface;
34
use Psr\Http\Message\UriFactoryInterface;
35
use Psr\Http\Message\UriInterface;
36
use Psr\Http\Server\RequestHandlerInterface;
37
38
/**
39
 * Router
40
 */
41
class Router implements RequestHandlerInterface
42 1
{
43
    use Traits\ValidationTrait;
44
45
    public const TYPE_REQUIREMENT = 1;
46
47
    public const TYPE_DEFAULT = 0;
48
49
    /** @var RouteMatcherInterface */
50
    private $matcher;
51
52
    /** @var InvokerInterface */
53
    private $resolver;
54
55
    /** @var callable */
56
    private $response;
57
58
    /** @var UriFactoryInterface */
59
    private $uriFactory;
60
61
    /** @var string */
62
    private $namespace;
63
64
    /** @var RouteInterface[] */
65
    private $routes = [];
66
67
    /** @var RouteListenerInterface[] */
68
    private $listeners = [];
69 56
70
    /** @var array<int,array<string,mixed>> */
71
    private $attributes = [];
72
73
    /** @var null|ProfileRoute */
74
    private $profiler;
75
76 56
    public function __construct(
77 56
        ResponseFactoryInterface $responseFactory,
78
        UriFactoryInterface $uriFactory,
79 56
        ?RouteMatcherInterface $matcher = null,
80 56
        ?InvokerInterface $resolver = null,
81 56
        ?ContainerInterface $container = null
82
    ) {
83
        $this->resolver  = $resolver ?? new Invoker([], $container);
84
        $this->matcher   = $matcher ?? new Services\SimpleRouteMatcher();
85
86
        $this->uriFactory      = $uriFactory;
87
        $this->response        = [$responseFactory, 'createResponse'];
88 1
    }
89
90 1
    /**
91
     * Gets the router routes
92
     *
93 4
     * @return RouteInterface[]
94
     */
95 4
    public function getRoutes(): array
96 4
    {
97
        return \array_values($this->routes);
98
    }
99
100
    /**
101
     * Set Namespace for route handlers/controllers
102
     *
103
     * @param string $namespace
104
     */
105 54
    public function setNamespace(string $namespace): void
106
    {
107 54
        $this->namespace = rtrim($namespace, '\\/') . '\\';
108 54
    }
109
110 54
    /**
111 1
     * @param ProfileRoute $profiler
112 1
     */
113
    public function setProfiler(?ProfileRoute $profiler = null): void
114
    {
115
        $this->profiler = $profiler ?? new ProfileRoute();
116 54
    }
117
118 54
    /**
119
     * Adds the given route(s) to the router
120
     *
121
     * @param RouteInterface ...$routes
122
     *
123
     * @throws DuplicateRouteException
124
     */
125
    public function addRoute(RouteInterface ...$routes): void
126
    {
127
        foreach ($routes as $route) {
128 1
            $name = $route->getName();
129
130 1
            if (isset($this->routes[$name])) {
131 1
                throw new DuplicateRouteException(
132 1
                    \sprintf('A route with the name "%s" already exists.', $name)
133
                );
134 1
            }
135
136
            $this->routes[$name] = $route;
137 1
138
            if (null !== $this->profiler) {
139 1
                $this->profiler->addProfile(new ProfileRoute($name, $route));
140
            }
141
        }
142
    }
143
144
    /**
145
     * Adds the given route(s) listener to the router
146 1
     *
147
     * @param RouteListenerInterface ...$listeners
148 1
     */
149
    public function addRouteListener(RouteListenerInterface ...$listeners): void
150 1
    {
151 1
        foreach ($listeners as $listener) {
152 1
            $this->listeners[] = $listener;
153
        }
154
    }
155
156 1
    /**
157
     * Adds parameters.
158
     *
159
     * This method implements a fluent interface.
160
     *
161
     * @param array<string,mixed> $parameters The parameters
162
     * @param int                 $type
163
     */
164
    public function addParameters(array $parameters, int $type = self::TYPE_REQUIREMENT): void
165
    {
166
        foreach ($parameters as $key => $regex) {
167
            if (self::TYPE_DEFAULT === $type) {
168 5
                $this->attributes[self::TYPE_DEFAULT] = [$key => $regex];
169
170 5
                continue;
171 2
            }
172
173
            $this->attributes[self::TYPE_REQUIREMENT] = [$key => $regex];
174 3
        }
175
    }
176
177
    /**
178
     * Gets allowed methods
179
     *
180
     * @return string[]
181
     */
182
    public function getAllowedMethods(): array
183
    {
184
        $methods = [];
185
186
        foreach ($this->routes as $route) {
187
            foreach ($route->getMethods() as $method) {
188
                $methods[$method] = true;
189
            }
190
        }
191
192
        return \array_keys($methods);
193
    }
194
195
    /**
196
     * Gets a route for the given name
197
     *
198 3
     * @param string $name
199
     *
200
     * @throws RouteNotFoundException
201 3
     *
202 1
     * @return RouteInterface
203 1
     */
204 1
    public function getRoute(string $name): RouteInterface
205 1
    {
206 1
        if (!isset($this->routes[$name])) {
207
            throw new RouteNotFoundException(\sprintf('No route found for the name "%s".', $name));
208 1
        }
209
210
        return $this->routes[$name];
211
    }
212 2
213 2
    /**
214
     * Generate a URI from the named route.
215
     *
216 2
     * Takes the named route and any parameters, and attempts to generate a
217 2
     * URI from it. Additional router-dependent query may be passed.
218
     *
219
     * Once there are no missing parameters in the URI we will encode
220
     * the URI and prepare it for returning to the user. If the URI is supposed to
221 2
     * be absolute, we will return it as-is. Otherwise we will remove the URL's root.
222 1
     *
223
     * @param string                       $routeName   route name
224
     * @param array<string,string>         $parameters  key => value option pairs to pass to the
225 2
     *                                                  router for purposes of generating a URI; takes precedence over options
226
     *                                                  present in route used to generate URI
227 2
     * @param array<int|string,int|string> $queryParams Optional query string parameters
228
     *
229
     * @throws UrlGenerationException if the route name is not known
230
     *                                or a parameter value does not match its regex
231
     *
232
     * @return UriInterface of fully qualified URL for named route
233
     */
234
    public function generateUri(string $routeName, array $parameters = [], array $queryParams = []): UriInterface
235
    {
236
        try {
237
            $route = $this->getRoute($routeName);
238
        } catch (RouteNotFoundException $e) {
239
            throw new UrlGenerationException(
240
                \sprintf(
241 47
                    'Unable to generate a URL for the named route "%s" as such route does not exist.',
242
                    $routeName
243 47
                ),
244 47
                404
245 47
            );
246
        }
247 47
248 47
        $prefix     = '.'; // Append missing "." at the beginning of the $uri.
249
        $createdUri = $this->matcher->buildPath($route, $parameters);
0 ignored issues
show
Unused Code introduced by
The assignment to $createdUri is dead and can be removed.
Loading history...
250
251
        // Making routing on sub-folders easier
252 47
        if (\strpos($createdUri = $this->matcher->buildPath($route, $parameters), '/') !== 0) {
253
            $prefix .= '/';
254 47
        }
255 47
256 47
        // Incase query is added to uri.
257 47
        if (!empty($queryParams)) {
258
            $createdUri .= '?' . \http_build_query($queryParams);
259
        }
260
261 43
        $createdUri = \rtrim(\strpos($createdUri, '://') !== false ? $createdUri : $prefix . $createdUri, '/');
262 41
263
        return $this->uriFactory->createUri($createdUri);
264 41
    }
265
266
    /**
267 2
     * Looks for a route that matches the given request
268 2
     *
269 2
     * @param ServerRequestInterface $request
270 2
     *
271
     * @throws MethodNotAllowedException
272
     * @throws UriHandlerException
273
     * @throws RouteNotFoundException
274
     *
275
     * @return RouteHandler
276
     */
277
    public function match(ServerRequestInterface &$request): RouteHandler
278 21
    {
279
        $requestUri  = $request->getUri();
280
        $basePath    = \dirname($request->getServerParams()['SCRIPT_NAME'] ?? '');
281 21
        $requestPath = \substr($requestUri->getPath(), \strlen($basePath)) ?: '/';
282
283
        if ('cli' === PHP_SAPI) {
284 17
            $requestPath = $requestUri->getPath();
285
        }
286
287 17
        // Get the request matching format.
288 17
        $route = $this->marshalMatchedRoute(
289
            [
290 17
                $request->getMethod(),
291
                $requestUri->getScheme(),
292
                $requestUri->getHost(),
293
                \rawurldecode(\strlen($requestPath) > 1 ? \rtrim($requestPath, '/') : $requestPath),
294 6
            ]
295
        );
296
297 11
        if ($route instanceof RouteInterface) {
298
            if (null !== $this->profiler) {
299
                $this->profiler->setMatched($route->getName());
300
            }
301
            $request = $request->withAttribute(Route::class, $route);
302
303
            return new RouteHandler($this->generateResponse($route), ($this->response)());
304
        }
305
306
        throw new RouteNotFoundException(
307 41
            \sprintf(
308
                'Unable to find the controller for path "%s". The route is wrongly configured.',
309 41
                $requestPath
310 34
            )
311 34
        );
312
    }
313 34
314 11
    /**
315
     * {@inheritDoc}
316
     */
317 23
    public function handle(ServerRequestInterface $request): ResponseInterface
318 41
    {
319
        // Get the Route Handler ready for dispatching
320
        $routingResults = $this->match($request);
321
322
        /** @var RouteInterface $route */
323
        $route = $request->getAttribute(Route::class);
324
325
        // Add Middlewares on route ...
326
        $pipeline = new RoutePipeline($this->resolver->getContainer());
327
        $pipeline->addMiddleware(...$route->getMiddlewares());
328
329
        if (\count($pipeline->getMiddlewares()) > 0) {
330
            // This middleware is in the priority map; but, this is the first middleware we have
331 47
            // encountered from the map thus far. We'll save its current index plus its index
332
            // from the priority map so we can compare against them on the next iterations.
333 47
            return $pipeline->process($request, $routingResults);
334
        }
335 47
336 47
        return $routingResults->handle($request);
337
    }
338
339 47
    /**
340 14
     * Generate the response so it can be served
341
     *
342
     * @param RouteInterface $route
343 45
     *
344
     * @return callable
345 41
     */
346 41
    private function generateResponse(RouteInterface $route): callable
347 41
    {
348
        return function (ServerRequestInterface $request, ResponseInterface $response) use ($route) {
349
            $handler   = $this->resolveController($request, $route);
350
            $arguments = [\get_class($request) => $request, \get_class($response) => $response];
351 2
352
            if ($handler instanceof ResponseInterface) {
353
                return $handler;
354
            }
355
356
            foreach ($this->listeners as $listener) {
357
                // Only allow default Invoker class
358
                if ($this->resolver instanceof Invoker) {
359
                    $listener->onRoute($request, $route, $this->resolver->getCallableResolver()->resolve($handler));
0 ignored issues
show
Bug introduced by
It seems like $handler can also be of type object; however, parameter $callable of DivineNii\Invoker\CallableResolver::resolve() does only seem to accept callable|string|string[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

359
                    $listener->onRoute($request, $route, $this->resolver->getCallableResolver()->resolve(/** @scrutinizer ignore-type */ $handler));
Loading history...
360
                }
361
            }
362 45
363
            try {
364 45
                return $this->resolver->call($handler, \array_merge($route->getArguments(), $arguments));
0 ignored issues
show
Bug introduced by
It seems like $handler can also be of type object; however, parameter $callable of DivineNii\Invoker\Interf...nvokerInterface::call() does only seem to accept callable|string|string[], maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

364
                return $this->resolver->call(/** @scrutinizer ignore-type */ $handler, \array_merge($route->getArguments(), $arguments));
Loading history...
365 45
            } finally {
366
                if (null !== $this->profiler) {
367 45
                    foreach ($this->profiler->getProfiles() as $profiler) {
368 2
                        $profiler->leave();
369
                    }
370
                }
371 43
            }
372 1
        };
373 1
    }
374 1
375
    /**
376
     * Marshals a route result based on the results of matching URL from set of routes.
377
     *
378 42
     * @param string[] $process
379 1
     *
380 1
     * @throws MethodNotAllowedException
381 1
     * @throws UriHandlerException
382
     *
383
     * @return null|RouteInterface
384 41
     */
385
    private function marshalMatchedRoute(array $process): ?RouteInterface
386
    {
387
        foreach ($this->routes as $route) {
388
            // Let's match the routes
389
            $match      = $this->matcher->compileRoute($this->mergeAttributes($route));
390
            $parameters = $hostParameters = [];
391
392
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
393
            if (!$this->compareUri($match->getRegex(), $process[3], $parameters)) {
394
                continue;
395
            }
396
397
            $this->assertRoute($route, $match->getRegex(true), $hostParameters, $process);
398
399
            return $route->setArguments($this->mergeDefaults(
400
                \array_replace($parameters, $hostParameters) ?? $match->getVariables(),
401
                $route->getDefaults()
402
            ));
403
        }
404
405
        return null;
406
    }
407
408
    /**
409
     * Asserts the Route's method and domain scheme.
410
     *
411
     * @param RouteInterface           $route
412
     * @param string                   $domain
413
     * @param array<int|string,string> $parameters
414
     * @param array<int,mixed>         $attributes
415
     */
416
    private function assertRoute(RouteInterface $route, string $domain, array &$parameters, array $attributes): void
417
    {
418
        [$method, $scheme, $host, $path] = $attributes;
419
        $parameters                      = [];
420
421
        if (!$this->compareMethod($route->getMethods(), $method)) {
422
            throw new MethodNotAllowedException($route->getMethods(), $path, $method);
423
        }
424
425
        if (!$this->compareDomain($domain, $host, $parameters)) {
426
            throw new UriHandlerException(
427
                \sprintf('Unfortunately current domain "%s" is not allowed on requested uri [%s]', $host, $path),
428
                400
429
            );
430
        }
431
432
        if (!$this->compareScheme($route->getSchemes(), $scheme)) {
433
            throw new UriHandlerException(
434
                \sprintf('Unfortunately current scheme "%s" is not allowed on requested uri [%s]', $scheme, $path),
435
                400
436
            );
437
        }
438
    }
439
}
440