Passed
Push — master ( 84220e...6f9ef0 )
by Divine Niiquaye
02:24
created

Router::resolveController()   B

Complexity

Conditions 11
Paths 7

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 11

Importance

Changes 0
Metric Value
cc 11
eloc 9
c 0
b 0
f 0
nc 7
nop 1
dl 0
loc 17
ccs 9
cts 9
cp 1
crap 11
rs 7.3166

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

384
            return $this->resolver->call(/** @scrutinizer ignore-type */ $handler, \array_merge($route->getArguments(), $arguments));
Loading history...
385 11
        };
386
    }
387
388
    /**
389
     * Marshals a route result based on the results of matching URL from set of routes.
390
     *
391
     * @param string[] $process
392
     *
393
     * @throws MethodNotAllowedException
394
     * @throws UriHandlerException
395
     * @throws RouteNotFoundException
396
     *
397
     * @return RouteInterface
398
     */
399 17
    private function marshalMatchedRoute(array $process): RouteInterface
400
    {
401 17
        foreach ($this->routes as $route) {
402
            // Let's match the routes
403 17
            $match      = $this->matcher->compileRoute($this->mergeAttributes($route));
404 17
            $parameters = $hostParameters = [];
405
406
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
407 17
            if (!$this->compareUri($match->getRegex(), $process[3], $parameters)) {
408 6
                continue;
409
            }
410
411 15
            $this->assertRoute($route, $match->getRegex(true), $hostParameters, $process);
412
413 11
            return $route->setArguments($this->mergeDefaults(
414 11
                \array_replace($parameters, $hostParameters) ?? $match->getVariables(),
415 11
                $route->getDefaults()
416
            ));
417
        }
418
419 2
        throw new  RouteNotFoundException(
420 2
            \sprintf(
421 2
                'Unable to find the controller for path "%s". The route is wrongly configured.',
422 2
                $process[3]
423
            )
424
        );
425
    }
426
427
    /**
428
     * Asserts the Route's method and domain scheme.
429
     *
430
     * @param RouteInterface           $route
431
     * @param string                   $domain
432
     * @param array<int|string,string> $parameters
433
     * @param array<int,mixed>         $attributes
434
     */
435 15
    private function assertRoute(RouteInterface $route, string $domain, array &$parameters, array $attributes): void
436
    {
437 15
        [$method, $scheme, $host, $path] = $attributes;
438 15
        $parameters                      = [];
439
440 15
        if (!$this->compareMethod($route->getMethods(), $method)) {
441 2
            throw new MethodNotAllowedException($route->getMethods(), $path, $method);
442
        }
443
444 13
        if (!$this->compareDomain($domain, $host, $parameters)) {
445 1
            throw new UriHandlerException(
446 1
                \sprintf('Unfortunately current domain "%s" is not allowed on requested uri [%s]', $host, $path),
447 1
                400
448
            );
449
        }
450
451 12
        if (!$this->compareScheme($route->getSchemes(), $scheme)) {
452 1
            throw new UriHandlerException(
453 1
                \sprintf('Unfortunately current scheme "%s" is not allowed on requested uri [%s]', $scheme, $path),
454 1
                400
455
            );
456
        }
457 11
    }
458
459
    /**
460
     * @param callable|object|string|string[] $controller
461
     *
462
     * @return callable|object|string|string[]
463
     */
464 8
    private function resolveController($controller)
465
    {
466 8
        if (null !== $this->namespace && (\is_string($controller) || !$controller instanceof Closure)) {
467
            if (
468 4
                \is_string($controller) &&
469 4
                !\class_exists($controller) &&
470 4
                false === \stripos($controller, $this->namespace)
471
            ) {
472 3
                $controller = \is_callable($controller) ? $controller : $this->namespace . $controller;
473
            }
474
475 4
            if (\is_array($controller) && (!\is_object($controller[0]) && !\class_exists($controller[0]))) {
476 1
                $controller[0] = $this->namespace . $controller[0];
477
            }
478
        }
479
480 8
        return $controller;
481
    }
482
}
483