Passed
Push — master ( cfe0c1...d1cf98 )
by Divine Niiquaye
02:27
created

Router   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 399
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 118
c 1
b 0
f 0
dl 0
loc 399
ccs 127
cts 127
cp 1
rs 8.8798
wmc 44

16 Methods

Rating   Name   Duplication   Size   Complexity  
A handle() 0 18 3
A match() 0 18 1
A getAllowedMethods() 0 11 3
A setNamespace() 0 3 1
A getRoutes() 0 3 1
A getRoute() 0 7 2
A __construct() 0 12 1
A generateUri() 0 28 5
A mergeMiddlewares() 0 5 1
A addParameters() 0 10 3
A addRoute() 0 12 3
A getMiddlewares() 0 3 1
A assertRoute() 0 20 4
A addMiddleware() 0 15 6
A marshalMatchedRoute() 0 24 3
A generateResponse() 0 25 6

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

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