Passed
Push — master ( 07b020...fc35ef )
by Anatoly
04:14 queued 02:01
created

Router::addPatterns()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 2
c 0
b 0
f 0
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
cc 2
nc 2
nop 1
crap 2
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
     * Gets the router host table
88
     *
89
     * @return array
90
     *
91
     * @since 2.6.0
92
     */
93 3
    public function getHosts() : array
94
    {
95 3
        return $this->hosts;
96
    }
97
98
    /**
99
     * Gets the router routes
100
     *
101
     * @return RouteInterface[]
102
     */
103 6
    public function getRoutes() : array
104
    {
105 6
        return array_values($this->routes);
106
    }
107
108
    /**
109
     * Gets the router middlewares
110
     *
111
     * @return MiddlewareInterface[]
112
     */
113 9
    public function getMiddlewares() : array
114
    {
115 9
        return array_values($this->middlewares);
116
    }
117
118
    /**
119
     * Adds the given patterns to the router
120
     *
121
     * ```php
122
     * $router->addPatterns([
123
     *   '@digit' => '\d+',
124
     *   '@word' => '\w+',
125
     * ]);
126
     *
127
     * $route->setPath('/{foo<@digit>}/{bar<@word>}');
128
     * ```
129
     *
130
     * @param array<string, string> $patterns
131
     *
132
     * @return void
133
     *
134
     * @since 2.11.0
135
     */
136 2
    public function addPatterns(array $patterns) : void
137
    {
138 2
        foreach ($patterns as $alias => $pattern) {
139 2
            self::$patterns[$alias] = $pattern;
140
        }
141 2
    }
142
143
    /**
144
     * Adds the given aliases for hostnames to the router's host table
145
     *
146
     * ```php
147
     * $router->addHosts([
148
     *   'local' => ['127.0.0.1', 'localhost'],
149
     * ]);
150
     *
151
     * $route->setHost('local');
152
     * ```
153
     *
154
     * @param array<string, string[]> $hosts
155
     *
156
     * @return void
157
     *
158
     * @since 2.11.0
159
     */
160 2
    public function addHosts(array $hosts) : void
161
    {
162 2
        foreach ($hosts as $alias => $hostnames) {
163 2
            $this->addHost($alias, ...$hostnames);
164
        }
165 2
    }
166
167
    /**
168
     * Adds the given alias for hostname(s) to the router's host table
169
     *
170
     * @param string $alias
171
     * @param string ...$hostnames
172
     *
173
     * @return void
174
     *
175
     * @since 2.6.0
176
     */
177 4
    public function addHost(string $alias, string ...$hostnames) : void
178
    {
179 4
        $this->hosts[$alias] = $hostnames;
180 4
    }
181
182
    /**
183
     * Adds the given route(s) to the router
184
     *
185
     * @param RouteInterface ...$routes
186
     *
187
     * @return void
188
     *
189
     * @throws InvalidArgumentException
190
     *         if one of the given routes already exists.
191
     */
192 25
    public function addRoute(RouteInterface ...$routes) : void
193
    {
194 25
        foreach ($routes as $route) {
195 25
            $name = $route->getName();
196 25
            if (isset($this->routes[$name])) {
197 1
                throw new InvalidArgumentException(sprintf(
198 1
                    'The route "%s" already exists.',
199 1
                    $name
200
                ));
201
            }
202
203 25
            $this->routes[$name] = $route;
204
        }
205 25
    }
206
207
    /**
208
     * Adds the given middleware(s) to the router
209
     *
210
     * @param MiddlewareInterface ...$middlewares
211
     *
212
     * @return void
213
     *
214
     * @throws InvalidArgumentException
215
     *         if one of the given middlewares already exists.
216
     */
217 7
    public function addMiddleware(MiddlewareInterface ...$middlewares) : void
218
    {
219 7
        foreach ($middlewares as $middleware) {
220 7
            $hash = spl_object_hash($middleware);
221 7
            if (isset($this->middlewares[$hash])) {
222 1
                throw new InvalidArgumentException(sprintf(
223 1
                    'The middleware "%s" already exists.',
224 1
                    get_class($middleware)
225
                ));
226
            }
227
228 7
            $this->middlewares[$hash] = $middleware;
229
        }
230 7
    }
231
232
    /**
233
     * Gets allowed methods
234
     *
235
     * @return string[]
236
     */
237 1
    public function getAllowedMethods() : array
238
    {
239 1
        $methods = [];
240 1
        foreach ($this->routes as $route) {
241 1
            foreach ($route->getMethods() as $method) {
242 1
                $methods[$method] = true;
243
            }
244
        }
245
246 1
        return array_keys($methods);
247
    }
248
249
    /**
250
     * Gets a route for the given name
251
     *
252
     * @param string $name
253
     *
254
     * @return RouteInterface
255
     *
256
     * @throws RouteNotFoundException
257
     */
258 3
    public function getRoute(string $name) : RouteInterface
259
    {
260 3
        if (!isset($this->routes[$name])) {
261 1
            throw new RouteNotFoundException(sprintf(
262 1
                'No route found for the name "%s".',
263 1
                $name
264
            ));
265
        }
266
267 2
        return $this->routes[$name];
268
    }
269
270
    /**
271
     * Generates a URI for the given named route
272
     *
273
     * @param string $name
274
     * @param array $attributes
275
     * @param bool $strict
276
     *
277
     * @return string
278
     *
279
     * @throws RouteNotFoundException
280
     *         If the given named route wasn't found.
281
     *
282
     * @throws Exception\InvalidAttributeValueException
283
     *         It can be thrown in strict mode, if an attribute value is not valid.
284
     *
285
     * @throws Exception\MissingAttributeValueException
286
     *         If a required attribute value is not given.
287
     */
288 1
    public function generateUri(string $name, array $attributes = [], bool $strict = false) : string
289
    {
290 1
        $route = $this->getRoute($name);
291
292 1
        $attributes += $route->getAttributes();
293
294 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

294
        return /** @scrutinizer ignore-call */ path_build($route->getPath(), $attributes, $strict);
Loading history...
295
    }
296
297
    /**
298
     * Looks for a route that matches the given request
299
     *
300
     * @param ServerRequestInterface $request
301
     *
302
     * @return RouteInterface
303
     *
304
     * @throws MethodNotAllowedException
305
     * @throws PageNotFoundException
306
     */
307 15
    public function match(ServerRequestInterface $request) : RouteInterface
308
    {
309 15
        $requestHost = $request->getUri()->getHost();
310 15
        $requestPath = $request->getUri()->getPath();
311 15
        $requestMethod = $request->getMethod();
312 15
        $allowedMethods = [];
313
314 15
        foreach ($this->routes as $route) {
315 15
            if (!$this->compareHosts($route->getHost(), $requestHost)) {
316 1
                continue;
317
            }
318
319
            // https://github.com/sunrise-php/http-router/issues/50
320
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
321 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

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