Passed
Pull Request — master (#80)
by Anatoly
02:19
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 5
    public function getRoutes() : array
104
    {
105 5
        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
     * @param array<string, string> $patterns
122
     *
123
     * @return void
124
     *
125
     * @since 2.11.0
126
     */
127 2
    public function addPatterns(array $patterns) : void
128
    {
129 2
        foreach ($patterns as $alias => $pattern) {
130 2
            self::$patterns[$alias] = $pattern;
131
        }
132 2
    }
133
134
    /**
135
     * Adds the given host aliases to the router host table
136
     *
137
     * @param array<string, string[]> $hosts
138
     *
139
     * @return void
140
     *
141
     * @since 2.11.0
142
     */
143 2
    public function addHosts(array $hosts) : void
144
    {
145 2
        foreach ($hosts as $alias => $hostnames) {
146 2
            $this->addHost($alias, ...$hostnames);
147
        }
148 2
    }
149
150
    /**
151
     * Adds the given host alias to the router host table
152
     *
153
     * @param string $alias
154
     * @param string ...$hostnames
155
     *
156
     * @return void
157
     *
158
     * @since 2.6.0
159
     */
160 4
    public function addHost(string $alias, string ...$hostnames) : void
161
    {
162 4
        $this->hosts[$alias] = $hostnames;
163 4
    }
164
165
    /**
166
     * Adds the given route(s) to the router
167
     *
168
     * @param RouteInterface ...$routes
169
     *
170
     * @return void
171
     *
172
     * @throws InvalidArgumentException
173
     *         if one of the given routes already exists.
174
     */
175 25
    public function addRoute(RouteInterface ...$routes) : void
176
    {
177 25
        foreach ($routes as $route) {
178 25
            $name = $route->getName();
179 25
            if (isset($this->routes[$name])) {
180 1
                throw new InvalidArgumentException(sprintf(
181 1
                    'The route "%s" already exists.',
182 1
                    $name
183
                ));
184
            }
185
186 25
            $this->routes[$name] = $route;
187
        }
188 25
    }
189
190
    /**
191
     * Adds the given middleware(s) to the router
192
     *
193
     * @param MiddlewareInterface ...$middlewares
194
     *
195
     * @return void
196
     *
197
     * @throws InvalidArgumentException
198
     *         if one of the given middlewares already exists.
199
     */
200 7
    public function addMiddleware(MiddlewareInterface ...$middlewares) : void
201
    {
202 7
        foreach ($middlewares as $middleware) {
203 7
            $hash = spl_object_hash($middleware);
204 7
            if (isset($this->middlewares[$hash])) {
205 1
                throw new InvalidArgumentException(sprintf(
206 1
                    'The middleware "%s" already exists.',
207 1
                    get_class($middleware)
208
                ));
209
            }
210
211 7
            $this->middlewares[$hash] = $middleware;
212
        }
213 7
    }
214
215
    /**
216
     * Gets allowed methods
217
     *
218
     * @return string[]
219
     */
220 1
    public function getAllowedMethods() : array
221
    {
222 1
        $methods = [];
223 1
        foreach ($this->routes as $route) {
224 1
            foreach ($route->getMethods() as $method) {
225 1
                $methods[$method] = true;
226
            }
227
        }
228
229 1
        return array_keys($methods);
230
    }
231
232
    /**
233
     * Gets a route for the given name
234
     *
235
     * @param string $name
236
     *
237
     * @return RouteInterface
238
     *
239
     * @throws RouteNotFoundException
240
     */
241 3
    public function getRoute(string $name) : RouteInterface
242
    {
243 3
        if (!isset($this->routes[$name])) {
244 1
            throw new RouteNotFoundException(sprintf(
245 1
                'No route found for the name "%s".',
246 1
                $name
247
            ));
248
        }
249
250 2
        return $this->routes[$name];
251
    }
252
253
    /**
254
     * Generates a URI for the given named route
255
     *
256
     * @param string $name
257
     * @param array $attributes
258
     * @param bool $strict
259
     *
260
     * @return string
261
     *
262
     * @throws RouteNotFoundException
263
     *         If the given named route wasn't found.
264
     *
265
     * @throws Exception\InvalidAttributeValueException
266
     *         It can be thrown in strict mode, if an attribute value is not valid.
267
     *
268
     * @throws Exception\MissingAttributeValueException
269
     *         If a required attribute value is not given.
270
     */
271 1
    public function generateUri(string $name, array $attributes = [], bool $strict = false) : string
272
    {
273 1
        $route = $this->getRoute($name);
274
275 1
        $attributes += $route->getAttributes();
276
277 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

277
        return /** @scrutinizer ignore-call */ path_build($route->getPath(), $attributes, $strict);
Loading history...
278
    }
279
280
    /**
281
     * Looks for a route that matches the given request
282
     *
283
     * @param ServerRequestInterface $request
284
     *
285
     * @return RouteInterface
286
     *
287
     * @throws MethodNotAllowedException
288
     * @throws PageNotFoundException
289
     */
290 15
    public function match(ServerRequestInterface $request) : RouteInterface
291
    {
292 15
        $requestHost = $request->getUri()->getHost();
293 15
        $requestPath = $request->getUri()->getPath();
294 15
        $requestMethod = $request->getMethod();
295 15
        $allowedMethods = [];
296
297 15
        foreach ($this->routes as $route) {
298 15
            if (!$this->compareHosts($route->getHost(), $requestHost)) {
299 1
                continue;
300
            }
301
302
            // https://github.com/sunrise-php/http-router/issues/50
303
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
304 15
            if (!path_match($route->getPath(), $requestPath, $attributes)) {
0 ignored issues
show
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

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