Passed
Push — master ( bb07ef...c3db73 )
by Anatoly
03:56 queued 01:05
created

Router::getMatchedRoute()   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 1
Bugs 0 Features 1
Metric Value
eloc 1
c 1
b 0
f 1
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 0
crap 1
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\Exception\InvalidArgumentException;
23
use Sunrise\Http\Router\Exception\MethodNotAllowedException;
24
use Sunrise\Http\Router\Exception\PageNotFoundException;
25
use Sunrise\Http\Router\Exception\RouteNotFoundException;
26
use Sunrise\Http\Router\Loader\LoaderInterface;
27
use Sunrise\Http\Router\RequestHandler\CallableRequestHandler;
28
use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler;
29
30
/**
31
 * Import functions
32
 */
33
use function array_flip;
34
use function array_keys;
35
use function array_values;
36
use function get_class;
37
use function spl_object_hash;
38
use function sprintf;
39
40
/**
41
 * Router
42
 */
43
class Router implements MiddlewareInterface, RequestHandlerInterface, RequestMethodInterface
44
{
45
46
    /**
47
     * Server Request attribute name for routing error instance
48
     *
49
     * @var string
50
     */
51
    public const ATTR_NAME_FOR_ROUTING_ERROR = '@routing-error';
52
53
    /**
54
     * Global patterns
55
     *
56
     * @var array<string, string>
57
     *
58
     * @since 2.9.0
59
     */
60
    public static $patterns = [
61
        '@slug' => '[0-9a-z-]+',
62
        '@uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
63
    ];
64
65
    /**
66
     * The router host table
67
     *
68
     * @var array<string, string[]>
69
     */
70
    private $hosts = [];
71
72
    /**
73
     * The router routes
74
     *
75
     * @var RouteInterface[]
76
     */
77
    private $routes = [];
78
79
    /**
80
     * The router middlewares
81
     *
82
     * @var MiddlewareInterface[]
83
     */
84
    private $middlewares = [];
85
86
    /**
87
     * The router's matched route
88
     *
89
     * @var RouteInterface|null
90
     */
91
    private $matchedRoute = null;
92
93
    /**
94
     * Gets the router host table
95
     *
96
     * @return array
97
     *
98
     * @since 2.6.0
99
     */
100 3
    public function getHosts() : array
101
    {
102 3
        return $this->hosts;
103
    }
104
105
    /**
106
     * Gets the router routes
107
     *
108
     * @return RouteInterface[]
109
     */
110 7
    public function getRoutes() : array
111
    {
112 7
        return array_values($this->routes);
113
    }
114
115
    /**
116
     * Gets the router middlewares
117
     *
118
     * @return MiddlewareInterface[]
119
     */
120 9
    public function getMiddlewares() : array
121
    {
122 9
        return array_values($this->middlewares);
123
    }
124
125
    /**
126
     * Gets the router's matched route
127
     *
128
     * @return RouteInterface|null
129
     */
130 2
    public function getMatchedRoute() : ?RouteInterface
131
    {
132 2
        return $this->matchedRoute;
133
    }
134
135
    /**
136
     * Adds the given patterns to the router
137
     *
138
     * ```php
139
     * $router->addPatterns([
140
     *   '@digit' => '\d+',
141
     *   '@word' => '\w+',
142
     * ]);
143
     *
144
     * $route->setPath('/{foo<@digit>}/{bar<@word>}');
145
     * ```
146
     *
147
     * @param array<string, string> $patterns
148
     *
149
     * @return void
150
     *
151
     * @since 2.11.0
152
     */
153 2
    public function addPatterns(array $patterns) : void
154
    {
155 2
        foreach ($patterns as $alias => $pattern) {
156 2
            self::$patterns[$alias] = $pattern;
157
        }
158 2
    }
159
160
    /**
161
     * Adds the given aliases for hostnames to the router's host table
162
     *
163
     * ```php
164
     * $router->addHosts([
165
     *   'local' => ['127.0.0.1', 'localhost'],
166
     * ]);
167
     *
168
     * $route->setHost('local');
169
     * ```
170
     *
171
     * @param array<string, string[]> $hosts
172
     *
173
     * @return void
174
     *
175
     * @since 2.11.0
176
     */
177 2
    public function addHosts(array $hosts) : void
178
    {
179 2
        foreach ($hosts as $alias => $hostnames) {
180 2
            $this->addHost($alias, ...$hostnames);
181
        }
182 2
    }
183
184
    /**
185
     * Adds the given alias for hostname(s) to the router's host table
186
     *
187
     * @param string $alias
188
     * @param string ...$hostnames
189
     *
190
     * @return void
191
     *
192
     * @since 2.6.0
193
     */
194 4
    public function addHost(string $alias, string ...$hostnames) : void
195
    {
196 4
        $this->hosts[$alias] = $hostnames;
197 4
    }
198
199
    /**
200
     * Adds the given route(s) to the router
201
     *
202
     * @param RouteInterface ...$routes
203
     *
204
     * @return void
205
     *
206
     * @throws InvalidArgumentException
207
     *         if one of the given routes already exists.
208
     */
209 25
    public function addRoute(RouteInterface ...$routes) : void
210
    {
211 25
        foreach ($routes as $route) {
212 25
            $name = $route->getName();
213 25
            if (isset($this->routes[$name])) {
214 1
                throw new InvalidArgumentException(sprintf(
215 1
                    'The route "%s" already exists.',
216 1
                    $name
217
                ));
218
            }
219
220 25
            $this->routes[$name] = $route;
221
        }
222 25
    }
223
224
    /**
225
     * Adds the given middleware(s) to the router
226
     *
227
     * @param MiddlewareInterface ...$middlewares
228
     *
229
     * @return void
230
     *
231
     * @throws InvalidArgumentException
232
     *         if one of the given middlewares already exists.
233
     */
234 7
    public function addMiddleware(MiddlewareInterface ...$middlewares) : void
235
    {
236 7
        foreach ($middlewares as $middleware) {
237 7
            $hash = spl_object_hash($middleware);
238 7
            if (isset($this->middlewares[$hash])) {
239 1
                throw new InvalidArgumentException(sprintf(
240 1
                    'The middleware "%s" already exists.',
241 1
                    get_class($middleware)
242
                ));
243
            }
244
245 7
            $this->middlewares[$hash] = $middleware;
246
        }
247 7
    }
248
249
    /**
250
     * Gets allowed methods
251
     *
252
     * @return string[]
253
     */
254 1
    public function getAllowedMethods() : array
255
    {
256 1
        $methods = [];
257 1
        foreach ($this->routes as $route) {
258 1
            foreach ($route->getMethods() as $method) {
259 1
                $methods[$method] = true;
260
            }
261
        }
262
263 1
        return array_keys($methods);
264
    }
265
266
    /**
267
     * Gets a route for the given name
268
     *
269
     * @param string $name
270
     *
271
     * @return RouteInterface
272
     *
273
     * @throws RouteNotFoundException
274
     */
275 3
    public function getRoute(string $name) : RouteInterface
276
    {
277 3
        if (!isset($this->routes[$name])) {
278 1
            throw new RouteNotFoundException(sprintf(
279 1
                'No route found for the name "%s".',
280 1
                $name
281
            ));
282
        }
283
284 2
        return $this->routes[$name];
285
    }
286
287
    /**
288
     * Generates a URI for the given named route
289
     *
290
     * @param string $name
291
     * @param array $attributes
292
     * @param bool $strict
293
     *
294
     * @return string
295
     *
296
     * @throws RouteNotFoundException
297
     *         If the given named route wasn't found.
298
     *
299
     * @throws Exception\InvalidAttributeValueException
300
     *         It can be thrown in strict mode, if an attribute value is not valid.
301
     *
302
     * @throws Exception\MissingAttributeValueException
303
     *         If a required attribute value is not given.
304
     */
305 1
    public function generateUri(string $name, array $attributes = [], bool $strict = false) : string
306
    {
307 1
        $route = $this->getRoute($name);
308
309 1
        $attributes += $route->getAttributes();
310
311 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

311
        return /** @scrutinizer ignore-call */ path_build($route->getPath(), $attributes, $strict);
Loading history...
312
    }
313
314
    /**
315
     * Looks for a route that matches the given request
316
     *
317
     * @param ServerRequestInterface $request
318
     *
319
     * @return RouteInterface
320
     *
321
     * @throws MethodNotAllowedException
322
     * @throws PageNotFoundException
323
     */
324 15
    public function match(ServerRequestInterface $request) : RouteInterface
325
    {
326 15
        $requestHost = $request->getUri()->getHost();
327 15
        $requestPath = $request->getUri()->getPath();
328 15
        $requestMethod = $request->getMethod();
329 15
        $allowedMethods = [];
330
331 15
        foreach ($this->routes as $route) {
332 15
            if (!$this->compareHosts($route->getHost(), $requestHost)) {
333 1
                continue;
334
            }
335
336
            // https://github.com/sunrise-php/http-router/issues/50
337
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
338 15
            if (!path_match($route->getPath(), $requestPath, $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

338
            if (!/** @scrutinizer ignore-call */ path_match($route->getPath(), $requestPath, $attributes)) {
Loading history...
339 12
                continue;
340
            }
341
342 11
            $routeMethods = array_flip($route->getMethods());
343 11
            $allowedMethods += $routeMethods;
344
345 11
            if (!isset($routeMethods[$requestMethod])) {
346 4
                continue;
347
            }
348
349 7
            return $route->withAddedAttributes($attributes);
350
        }
351
352 9
        if (!empty($allowedMethods)) {
353 4
            throw new MethodNotAllowedException('Method Not Allowed', [
354 4
                'method' => $requestMethod,
355 4
                'allowed' => array_keys($allowedMethods),
356
            ]);
357
        }
358
359 5
        throw new PageNotFoundException('Page Not Found');
360
    }
361
362
    /**
363
     * Runs the router
364
     *
365
     * @param ServerRequestInterface $request
366
     *
367
     * @return ResponseInterface
368
     *
369
     * @since 2.8.0
370
     */
371 3
    public function run(ServerRequestInterface $request) : ResponseInterface
372
    {
373
        // lazy resolving of the given request...
374 3
        $routing = new CallableRequestHandler(function (ServerRequestInterface $request) : ResponseInterface {
375 3
            $route = $this->match($request);
376 1
            $this->matchedRoute = $route;
377 1
            return $route->handle($request);
378 3
        });
379
380 3
        $middlewares = $this->getMiddlewares();
381 3
        if (empty($middlewares)) {
382 1
            return $routing->handle($request);
383
        }
384
385 2
        $handler = new QueueableRequestHandler($routing);
386 2
        $handler->add(...$middlewares);
387
388 2
        return $handler->handle($request);
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     */
394 8
    public function handle(ServerRequestInterface $request) : ResponseInterface
395
    {
396 8
        $route = $this->match($request);
397 4
        $this->matchedRoute = $route;
398
399 4
        $middlewares = $this->getMiddlewares();
400 4
        if (empty($middlewares)) {
401 2
            return $route->handle($request);
402
        }
403
404 2
        $handler = new QueueableRequestHandler($route);
405 2
        $handler->add(...$middlewares);
406
407 2
        return $handler->handle($request);
408
    }
409
410
    /**
411
     * {@inheritdoc}
412
     */
413 3
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
414
    {
415
        try {
416 3
            return $this->handle($request);
417 2
        } catch (MethodNotAllowedException|PageNotFoundException $e) {
418 2
            $request = $request->withAttribute(self::ATTR_NAME_FOR_ROUTING_ERROR, $e);
419
420 2
            return $handler->handle($request);
421
        }
422
    }
423
424
    /**
425
     * Loads routes through the given loaders
426
     *
427
     * @param LoaderInterface ...$loaders
428
     *
429
     * @return void
430
     */
431 3
    public function load(LoaderInterface ...$loaders) : void
432
    {
433 3
        foreach ($loaders as $loader) {
434 3
            $this->addRoute(...$loader->load()->all());
435
        }
436 3
    }
437
438
    /**
439
     * Compares the given route host and the given request host
440
     *
441
     * @param string|null $routeHost
442
     * @param string $requestHost
443
     *
444
     * @return bool
445
     */
446 15
    private function compareHosts(?string $routeHost, string $requestHost) : bool
447
    {
448 15
        if (null === $routeHost || $requestHost === $routeHost) {
449 15
            return true;
450
        }
451
452 1
        if (!empty($this->hosts[$routeHost])) {
453 1
            foreach ($this->hosts[$routeHost] as $hostname) {
454 1
                if ($hostname === $requestHost) {
455 1
                    return true;
456
                }
457
            }
458
        }
459
460 1
        return false;
461
    }
462
}
463