Passed
Push — master ( 9f07a5...afc511 )
by Anatoly
03:40 queued 01:13
created

Router   B

Complexity

Total Complexity 49

Size/Duplication

Total Lines 506
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 19
Bugs 0 Features 2
Metric Value
wmc 49
eloc 119
c 19
b 0
f 2
dl 0
loc 506
ccs 123
cts 123
cp 1
rs 8.48

21 Methods

Rating   Name   Duplication   Size   Complexity  
A getEventDispatcher() 0 3 1
A getMatchedRoute() 0 3 1
A run() 0 25 3
A addHost() 0 3 1
A process() 0 8 2
A addPatterns() 0 4 2
A getRoute() 0 10 2
A resolveHostname() 0 11 4
A generateUri() 0 7 1
A addMiddleware() 0 12 3
B match() 0 37 6
A getAllowedMethods() 0 10 3
A getMiddlewares() 0 8 2
A load() 0 4 2
A setEventDispatcher() 0 3 1
A getRoutes() 0 8 2
A getRoutesByHostname() 0 14 4
A getHosts() 0 3 1
A addRoute() 0 12 3
A handle() 0 20 3
A addHosts() 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 declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Fenric <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Fenric
8
 * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-router
10
 */
11
12
namespace Sunrise\Http\Router;
13
14
/**
15
 * Import classes
16
 */
17
use Fig\Http\Message\RequestMethodInterface;
18
use Psr\Http\Message\ResponseInterface;
19
use Psr\Http\Message\ServerRequestInterface;
20
use Psr\Http\Server\MiddlewareInterface;
21
use Psr\Http\Server\RequestHandlerInterface;
22
use Sunrise\Http\Router\Event\RouteEvent;
23
use Sunrise\Http\Router\Exception\InvalidArgumentException;
24
use Sunrise\Http\Router\Exception\MethodNotAllowedException;
25
use Sunrise\Http\Router\Exception\PageNotFoundException;
26
use Sunrise\Http\Router\Exception\RouteNotFoundException;
27
use Sunrise\Http\Router\Loader\LoaderInterface;
28
use Sunrise\Http\Router\RequestHandler\CallableRequestHandler;
29
use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler;
30
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
31
32
/**
33
 * Import functions
34
 */
35
use function Sunrise\Http\Router\path_build;
0 ignored issues
show
introduced by
The function Sunrise\Http\Router\path_build was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
36
use function Sunrise\Http\Router\path_match;
0 ignored issues
show
introduced by
The function Sunrise\Http\Router\path_match was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
37
use function array_keys;
38
use function get_class;
39
use function spl_object_hash;
40
use function sprintf;
41
42
/**
43
 * Router
44
 */
45
class Router implements MiddlewareInterface, RequestHandlerInterface, RequestMethodInterface
46
{
47
48
    /**
49
     * Server Request attribute name for routing error instance
50
     *
51
     * @var string
52
     */
53
    public const ATTR_NAME_FOR_ROUTING_ERROR = '@routing-error';
54
55
    /**
56
     * Global patterns
57
     *
58
     * @var array<string, string>
59
     *
60
     * @since 2.9.0
61
     */
62
    public static $patterns = [
63
        '@slug' => '[0-9a-z-]+',
64
        '@uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
65
    ];
66
67
    /**
68
     * The router's host table
69
     *
70
     * @var array<string, string[]>
71
     *      The key is a host alias and values are hostnames.
72
     */
73
    private $hosts = [];
74
75
    /**
76
     * The router's routes
77
     *
78
     * @var array<string, RouteInterface>
79
     *      The key is a route name.
80
     */
81
    private $routes = [];
82
83
    /**
84
     * The router's middlewares
85
     *
86
     * @var array<string, MiddlewareInterface>
87
     *      The keys is an object hash.
88
     */
89
    private $middlewares = [];
90
91
    /**
92
     * The router's matched route
93
     *
94
     * @var RouteInterface|null
95
     */
96
    private $matchedRoute = null;
97
98
    /**
99
     * The router's event dispatcher
100
     *
101
     * @var EventDispatcherInterface|null
102
     *
103
     * @since 2.13.0
104
     */
105
    private $eventDispatcher = null;
106
107
    /**
108
     * Gets the router's host table
109
     *
110
     * @return array<string, string[]>
111
     *
112
     * @since 2.6.0
113
     */
114 3
    public function getHosts() : array
115
    {
116 3
        return $this->hosts;
117
    }
118
119
    /**
120
     * Resolves the given hostname
121
     *
122
     * @param string $hostname
123
     *
124
     * @return string|null
125
     *
126
     * @since 2.14.0
127
     */
128 19
    public function resolveHostname(string $hostname) : ?string
129
    {
130 19
        foreach ($this->hosts as $alias => $hostnames) {
131 3
            foreach ($hostnames as $value) {
132 3
                if ($hostname === $value) {
133 3
                    return $alias;
134
                }
135
            }
136
        }
137
138 19
        return null;
139
    }
140
141
    /**
142
     * Gets all routes
143
     *
144
     * @return RouteInterface[]
145
     */
146 7
    public function getRoutes() : array
147
    {
148 7
        $routes = [];
149 7
        foreach ($this->routes as $route) {
150 5
            $routes[] = $route;
151
        }
152
153 7
        return $routes;
154
    }
155
156
    /**
157
     * Gets routes by the given hostname
158
     *
159
     * @param string $hostname
160
     *
161
     * @return RouteInterface[]
162
     *
163
     * @since 2.14.0
164
     */
165 18
    public function getRoutesByHostname(string $hostname) : array
166
    {
167
        // the hostname's alias.
168 18
        $alias = $this->resolveHostname($hostname);
169
170 18
        $routes = [];
171 18
        foreach ($this->routes as $route) {
172 18
            $host = $route->getHost();
173 18
            if ($host === null || $host === $alias) {
174 18
                $routes[] = $route;
175
            }
176
        }
177
178 18
        return $routes;
179
    }
180
181
    /**
182
     * Gets the router's middlewares
183
     *
184
     * @return MiddlewareInterface[]
185
     */
186 11
    public function getMiddlewares() : array
187
    {
188 11
        $middlewares = [];
189 11
        foreach ($this->middlewares as $middleware) {
190 6
            $middlewares[] = $middleware;
191
        }
192
193 11
        return $middlewares;
194
    }
195
196
    /**
197
     * Gets the router's matched route
198
     *
199
     * @return RouteInterface|null
200
     */
201 2
    public function getMatchedRoute() : ?RouteInterface
202
    {
203 2
        return $this->matchedRoute;
204
    }
205
206
    /**
207
     * Gets the router's event dispatcher
208
     *
209
     * @return EventDispatcherInterface|null
210
     *
211
     * @since 2.13.0
212
     */
213 2
    public function getEventDispatcher() : ?EventDispatcherInterface
214
    {
215 2
        return $this->eventDispatcher;
216
    }
217
218
    /**
219
     * Adds the given patterns to the router
220
     *
221
     * ```php
222
     * $router->addPatterns([
223
     *   '@digit' => '\d+',
224
     *   '@word' => '\w+',
225
     * ]);
226
     * ```
227
     *
228
     * ```php
229
     * $route->setPath('/{foo<@digit>}/{bar<@word>}');
230
     * ```
231
     *
232
     * @param array<string, string> $patterns
233
     *
234
     * @return void
235
     *
236
     * @since 2.11.0
237
     */
238 2
    public function addPatterns(array $patterns) : void
239
    {
240 2
        foreach ($patterns as $alias => $pattern) {
241 2
            self::$patterns[$alias] = $pattern;
242
        }
243
    }
244
245
    /**
246
     * Adds aliases for hostnames to the router's host table
247
     *
248
     * ```php
249
     * $router->addHosts([
250
     *   'local' => ['127.0.0.1', 'localhost'],
251
     * ]);
252
     * ```
253
     *
254
     * ```php
255
     * // will be available at 127.0.0.1
256
     * $route->setHost('local');
257
     * ```
258
     *
259
     * @param array<string, string[]> $hosts
260
     *
261
     * @return void
262
     *
263
     * @since 2.11.0
264
     */
265 2
    public function addHosts(array $hosts) : void
266
    {
267 2
        foreach ($hosts as $alias => $hostnames) {
268 2
            $this->addHost($alias, ...$hostnames);
269
        }
270
    }
271
272
    /**
273
     * Adds the given alias for the given hostname(s) to the router's host table
274
     *
275
     * @param string $alias
276
     * @param string ...$hostnames
277
     *
278
     * @return void
279
     *
280
     * @since 2.6.0
281
     */
282 6
    public function addHost(string $alias, string ...$hostnames) : void
283
    {
284 6
        $this->hosts[$alias] = $hostnames;
285
    }
286
287
    /**
288
     * Adds the given route(s) to the router
289
     *
290
     * @param RouteInterface ...$routes
291
     *
292
     * @return void
293
     *
294
     * @throws InvalidArgumentException
295
     *         if one of the given routes already exists.
296
     */
297 28
    public function addRoute(RouteInterface ...$routes) : void
298
    {
299 28
        foreach ($routes as $route) {
300 28
            $name = $route->getName();
301 28
            if (isset($this->routes[$name])) {
302 1
                throw new InvalidArgumentException(sprintf(
303
                    'The route "%s" already exists.',
304
                    $name
305
                ));
306
            }
307
308 28
            $this->routes[$name] = $route;
309
        }
310
    }
311
312
    /**
313
     * Adds the given middleware(s) to the router
314
     *
315
     * @param MiddlewareInterface ...$middlewares
316
     *
317
     * @return void
318
     *
319
     * @throws InvalidArgumentException
320
     *         if one of the given middlewares already exists.
321
     */
322 7
    public function addMiddleware(MiddlewareInterface ...$middlewares) : void
323
    {
324 7
        foreach ($middlewares as $middleware) {
325 7
            $hash = spl_object_hash($middleware);
326 7
            if (isset($this->middlewares[$hash])) {
327 1
                throw new InvalidArgumentException(sprintf(
328
                    'The middleware "%s" already exists.',
329 1
                    get_class($middleware)
330
                ));
331
            }
332
333 7
            $this->middlewares[$hash] = $middleware;
334
        }
335
    }
336
337
    /**
338
     * Sets the given event dispatcher to the router
339
     *
340
     * @param EventDispatcherInterface|null $eventDispatcher
341
     *
342
     * @return void
343
     *
344
     * @since 2.13.0
345
     */
346 4
    public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher) : void
347
    {
348 4
        $this->eventDispatcher = $eventDispatcher;
349
    }
350
351
    /**
352
     * Gets allowed methods
353
     *
354
     * @return string[]
355
     */
356 1
    public function getAllowedMethods() : array
357
    {
358 1
        $methods = [];
359 1
        foreach ($this->routes as $route) {
360 1
            foreach ($route->getMethods() as $method) {
361 1
                $methods[$method] = true;
362
            }
363
        }
364
365 1
        return array_keys($methods);
366
    }
367
368
    /**
369
     * Gets a route for the given name
370
     *
371
     * @param string $name
372
     *
373
     * @return RouteInterface
374
     *
375
     * @throws RouteNotFoundException
376
     */
377 3
    public function getRoute(string $name) : RouteInterface
378
    {
379 3
        if (!isset($this->routes[$name])) {
380 1
            throw new RouteNotFoundException(sprintf(
381
                'No route found for the name "%s".',
382
                $name
383
            ));
384
        }
385
386 2
        return $this->routes[$name];
387
    }
388
389
    /**
390
     * Generates a URI for the given named route
391
     *
392
     * @param string $name
393
     * @param array $attributes
394
     * @param bool $strict
395
     *
396
     * @return string
397
     *
398
     * @throws RouteNotFoundException
399
     *         If the given named route wasn't found.
400
     *
401
     * @throws Exception\InvalidAttributeValueException
402
     *         It can be thrown in strict mode, if an attribute value is not valid.
403
     *
404
     * @throws Exception\MissingAttributeValueException
405
     *         If a required attribute value is not given.
406
     */
407 1
    public function generateUri(string $name, array $attributes = [], bool $strict = false) : string
408
    {
409 1
        $route = $this->getRoute($name);
410
411 1
        $attributes += $route->getAttributes();
412
413 1
        return path_build($route->getPath(), $attributes, $strict);
0 ignored issues
show
Bug introduced by
The function path_build was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

413
        return /** @scrutinizer ignore-call */ path_build($route->getPath(), $attributes, $strict);
Loading history...
414
    }
415
416
    /**
417
     * Looks for a route that matches the given request
418
     *
419
     * @param ServerRequestInterface $request
420
     *
421
     * @return RouteInterface
422
     *
423
     * @throws MethodNotAllowedException
424
     * @throws PageNotFoundException
425
     */
426 17
    public function match(ServerRequestInterface $request) : RouteInterface
427
    {
428 17
        $currentHost = $request->getUri()->getHost();
429 17
        $currentPath = $request->getUri()->getPath();
430 17
        $currentMethod = $request->getMethod();
431 17
        $allowedMethods = [];
432
433 17
        $routes = $this->getRoutesByHostname($currentHost);
434
435 17
        foreach ($routes as $route) {
436
            // https://github.com/sunrise-php/http-router/issues/50
437
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
438 17
            if (!path_match($route->getPath(), $currentPath, $attributes)) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $attributes seems to be never defined.
Loading history...
Bug introduced by
The function path_match was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

438
            if (!/** @scrutinizer ignore-call */ path_match($route->getPath(), $currentPath, $attributes)) {
Loading history...
439 13
                continue;
440
            }
441
442 13
            $routeMethods = [];
443 13
            foreach ($route->getMethods() as $routeMethod) {
444 13
                $routeMethods[$routeMethod] = true;
445 13
                $allowedMethods[$routeMethod] = true;
446
            }
447
448 13
            if (!isset($routeMethods[$currentMethod])) {
449 4
                continue;
450
            }
451
452 9
            return $route->withAddedAttributes($attributes);
453
        }
454
455 8
        if (!empty($allowedMethods)) {
456 4
            throw new MethodNotAllowedException('Method Not Allowed', [
457
                'method' => $currentMethod,
458 4
                'allowed' => array_keys($allowedMethods),
459
            ]);
460
        }
461
462 4
        throw new PageNotFoundException('Page Not Found');
463
    }
464
465
    /**
466
     * Runs the router
467
     *
468
     * @param ServerRequestInterface $request
469
     *
470
     * @return ResponseInterface
471
     *
472
     * @since 2.8.0
473
     */
474 4
    public function run(ServerRequestInterface $request) : ResponseInterface
475
    {
476
        // lazy resolving of the given request...
477 4
        $routing = new CallableRequestHandler(function (ServerRequestInterface $request) : ResponseInterface {
478 4
            $route = $this->match($request);
479 2
            $this->matchedRoute = $route;
480
481 2
            if (isset($this->eventDispatcher)) {
482 1
                $event = new RouteEvent($route, $request);
483 1
                $this->eventDispatcher->dispatch($event, RouteEvent::NAME);
0 ignored issues
show
Bug introduced by
The method dispatch() does not exist on null. ( Ignorable by Annotation )

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

483
                $this->eventDispatcher->/** @scrutinizer ignore-call */ 
484
                                        dispatch($event, RouteEvent::NAME);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with Sunrise\Http\Router\Event\RouteEvent::NAME. ( Ignorable by Annotation )

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

483
                $this->eventDispatcher->/** @scrutinizer ignore-call */ 
484
                                        dispatch($event, RouteEvent::NAME);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
484 1
                $request = $event->getRequest();
485
            }
486
487 2
            return $route->handle($request);
488
        });
489
490 4
        $middlewares = $this->getMiddlewares();
491 4
        if (empty($middlewares)) {
492 2
            return $routing->handle($request);
493
        }
494
495 2
        $handler = new QueueableRequestHandler($routing);
496 2
        $handler->add(...$middlewares);
497
498 2
        return $handler->handle($request);
499
    }
500
501
    /**
502
     * {@inheritdoc}
503
     */
504 9
    public function handle(ServerRequestInterface $request) : ResponseInterface
505
    {
506 9
        $route = $this->match($request);
507 5
        $this->matchedRoute = $route;
508
509 5
        if (isset($this->eventDispatcher)) {
510 1
            $event = new RouteEvent($route, $request);
511 1
            $this->eventDispatcher->dispatch($event, RouteEvent::NAME);
0 ignored issues
show
Unused Code introduced by
The call to Symfony\Contracts\EventD...erInterface::dispatch() has too many arguments starting with Sunrise\Http\Router\Event\RouteEvent::NAME. ( Ignorable by Annotation )

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

511
            $this->eventDispatcher->/** @scrutinizer ignore-call */ 
512
                                    dispatch($event, RouteEvent::NAME);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
512 1
            $request = $event->getRequest();
513
        }
514
515 5
        $middlewares = $this->getMiddlewares();
516 5
        if (empty($middlewares)) {
517 3
            return $route->handle($request);
518
        }
519
520 2
        $handler = new QueueableRequestHandler($route);
521 2
        $handler->add(...$middlewares);
522
523 2
        return $handler->handle($request);
524
    }
525
526
    /**
527
     * {@inheritdoc}
528
     */
529 3
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
530
    {
531
        try {
532 3
            return $this->handle($request);
533 2
        } catch (MethodNotAllowedException|PageNotFoundException $e) {
534 2
            $request = $request->withAttribute(self::ATTR_NAME_FOR_ROUTING_ERROR, $e);
535
536 2
            return $handler->handle($request);
537
        }
538
    }
539
540
    /**
541
     * Loads routes through the given loaders
542
     *
543
     * @param LoaderInterface ...$loaders
544
     *
545
     * @return void
546
     */
547 3
    public function load(LoaderInterface ...$loaders) : void
548
    {
549 3
        foreach ($loaders as $loader) {
550 3
            $this->addRoute(...$loader->load()->all());
551
        }
552
    }
553
}
554