Passed
Push — master ( e1642a...3c4285 )
by Anatoly
01:03 queued 12s
created

Router::match()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 36
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6

Importance

Changes 9
Bugs 0 Features 0
Metric Value
eloc 20
c 9
b 0
f 0
dl 0
loc 36
ccs 20
cts 20
cp 1
rs 8.9777
cc 6
nc 9
nop 1
crap 6
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\MethodNotAllowedException;
23
use Sunrise\Http\Router\Exception\MiddlewareAlreadyExistsException;
24
use Sunrise\Http\Router\Exception\PageNotFoundException;
25
use Sunrise\Http\Router\Exception\RouteAlreadyExistsException;
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
31
/**
32
 * Import functions
33
 */
34
use function array_flip;
35
use function array_keys;
36
use function array_values;
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
     * The router host table
55
     *
56
     * @var array<string, string[]>
57
     */
58
    private $hosts = [];
59
60
    /**
61
     * The router routes
62
     *
63
     * @var RouteInterface[]
64
     */
65
    private $routes = [];
66
67
    /**
68
     * The router middlewares
69
     *
70
     * @var MiddlewareInterface[]
71
     */
72
    private $middlewares = [];
73
74
    /**
75
     * Gets the router host table
76
     *
77
     * @return array
78
     *
79
     * @since 2.6.0
80
     */
81 2
    public function getHosts() : array
82
    {
83 2
        return $this->hosts;
84
    }
85
86
    /**
87
     * Gets the router routes
88
     *
89
     * @return RouteInterface[]
90
     */
91 4
    public function getRoutes() : array
92
    {
93 4
        return array_values($this->routes);
94
    }
95
96
    /**
97
     * Gets the router middlewares
98
     *
99
     * @return MiddlewareInterface[]
100
     */
101 9
    public function getMiddlewares() : array
102
    {
103 9
        return array_values($this->middlewares);
104
    }
105
106
    /**
107
     * Adds the given host alias to the router host table
108
     *
109
     * @param string $alias
110
     * @param string ...$hostname
111
     *
112
     * @return void
113
     *
114
     * @since 2.6.0
115
     */
116 3
    public function addHost(string $alias, string ...$hostname) : void
117
    {
118 3
        $this->hosts[$alias] = $hostname;
119 3
    }
120
121
    /**
122
     * Adds the given route(s) to the router
123
     *
124
     * @param RouteInterface ...$routes
125
     *
126
     * @return void
127
     *
128
     * @throws RouteAlreadyExistsException
129
     */
130 25
    public function addRoute(RouteInterface ...$routes) : void
131
    {
132 25
        foreach ($routes as $route) {
133 25
            $name = $route->getName();
134
135 25
            if (isset($this->routes[$name])) {
136 1
                throw new RouteAlreadyExistsException(
137 1
                    sprintf('A route with the name "%s" already exists.', $name)
138
                );
139
            }
140
141 25
            $this->routes[$name] = $route;
142
        }
143 25
    }
144
145
    /**
146
     * Adds the given middleware(s) to the router
147
     *
148
     * @param MiddlewareInterface ...$middlewares
149
     *
150
     * @return void
151
     *
152
     * @throws MiddlewareAlreadyExistsException
153
     */
154 7
    public function addMiddleware(MiddlewareInterface ...$middlewares) : void
155
    {
156 7
        foreach ($middlewares as $middleware) {
157 7
            $hash = spl_object_hash($middleware);
158
159 7
            if (isset($this->middlewares[$hash])) {
160 1
                throw new MiddlewareAlreadyExistsException(
161 1
                    sprintf('A middleware with the hash "%s" already exists.', $hash)
162
                );
163
            }
164
165 7
            $this->middlewares[$hash] = $middleware;
166
        }
167 7
    }
168
169
    /**
170
     * Gets allowed methods
171
     *
172
     * @return string[]
173
     */
174 1
    public function getAllowedMethods() : array
175
    {
176 1
        $methods = [];
177 1
        foreach ($this->routes as $route) {
178 1
            foreach ($route->getMethods() as $method) {
179 1
                $methods[$method] = true;
180
            }
181
        }
182
183 1
        return array_keys($methods);
184
    }
185
186
    /**
187
     * Gets a route for the given name
188
     *
189
     * @param string $name
190
     *
191
     * @return RouteInterface
192
     *
193
     * @throws RouteNotFoundException
194
     */
195 3
    public function getRoute(string $name) : RouteInterface
196
    {
197 3
        if (!isset($this->routes[$name])) {
198 1
            throw new RouteNotFoundException(
199 1
                sprintf('No route found for the name "%s".', $name)
200
            );
201
        }
202
203 2
        return $this->routes[$name];
204
    }
205
206
    /**
207
     * Generates a URI for the given named route
208
     *
209
     * @param string $name
210
     * @param array $attributes
211
     * @param bool $strict
212
     *
213
     * @return string
214
     *
215
     * @throws RouteNotFoundException
216
     *         If the given named route wasn't found.
217
     *
218
     * @throws Exception\InvalidAttributeValueException
219
     *         It can be thrown in strict mode, if an attribute value is not valid.
220
     *
221
     * @throws Exception\MissingAttributeValueException
222
     *         If a required attribute value is not given.
223
     */
224 1
    public function generateUri(string $name, array $attributes = [], bool $strict = false) : string
225
    {
226 1
        $route = $this->getRoute($name);
227
228 1
        $attributes += $route->getAttributes();
229
230 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

230
        return /** @scrutinizer ignore-call */ path_build($route->getPath(), $attributes, $strict);
Loading history...
231
    }
232
233
    /**
234
     * Looks for a route that matches the given request
235
     *
236
     * @param ServerRequestInterface $request
237
     *
238
     * @return RouteInterface
239
     *
240
     * @throws MethodNotAllowedException
241
     * @throws PageNotFoundException
242
     */
243 15
    public function match(ServerRequestInterface $request) : RouteInterface
244
    {
245 15
        $requestHost = $request->getUri()->getHost();
246 15
        $requestPath = $request->getUri()->getPath();
247 15
        $requestMethod = $request->getMethod();
248 15
        $allowedMethods = [];
249
250 15
        foreach ($this->routes as $route) {
251 15
            if (!$this->compareHosts($route->getHost(), $requestHost)) {
252 1
                continue;
253
            }
254
255
            // https://github.com/sunrise-php/http-router/issues/50
256
            // https://tools.ietf.org/html/rfc7231#section-6.5.5
257 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

257
            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...
258 12
                continue;
259
            }
260
261 11
            $routeMethods = array_flip($route->getMethods());
262 11
            $allowedMethods += $routeMethods;
263
264 11
            if (!isset($routeMethods[$requestMethod])) {
265 4
                continue;
266
            }
267
268 7
            return $route->withAddedAttributes($attributes);
269
        }
270
271 9
        if (!empty($allowedMethods)) {
272 4
            throw new MethodNotAllowedException('Method Not Allowed', [
273 4
                'method' => $requestMethod,
274 4
                'allowed' => array_keys($allowedMethods),
275
            ]);
276
        }
277
278 5
        throw new PageNotFoundException('Page Not Found');
279
    }
280
281
    /**
282
     * Runs the router
283
     *
284
     * @param ServerRequestInterface $request
285
     *
286
     * @return ResponseInterface
287
     *
288
     * @since 2.8.0
289
     */
290 3
    public function run(ServerRequestInterface $request) : ResponseInterface
291
    {
292
        // lazy resolving of the given request...
293 3
        $routing = new CallableRequestHandler(function (ServerRequestInterface $request) : ResponseInterface {
294 3
            return $this->match($request)->handle($request);
295 3
        });
296
297 3
        $middlewares = $this->getMiddlewares();
298 3
        if (empty($middlewares)) {
299 1
            return $routing->handle($request);
300
        }
301
302 2
        $handler = new QueueableRequestHandler($routing);
303 2
        $handler->add(...$middlewares);
304
305 2
        return $handler->handle($request);
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311 8
    public function handle(ServerRequestInterface $request) : ResponseInterface
312
    {
313 8
        $route = $this->match($request);
314
315 4
        $middlewares = $this->getMiddlewares();
316 4
        if (empty($middlewares)) {
317 2
            return $route->handle($request);
318
        }
319
320 2
        $handler = new QueueableRequestHandler($route);
321 2
        $handler->add(...$middlewares);
322
323 2
        return $handler->handle($request);
324
    }
325
326
    /**
327
     * {@inheritdoc}
328
     */
329 3
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
330
    {
331
        try {
332 3
            return $this->handle($request);
333 2
        } catch (MethodNotAllowedException|PageNotFoundException $e) {
334 2
            $request = $request->withAttribute(self::ATTR_NAME_FOR_ROUTING_ERROR, $e);
335
336 2
            return $handler->handle($request);
337
        }
338
    }
339
340
    /**
341
     * Loads routes through the given loaders
342
     *
343
     * @param LoaderInterface ...$loaders
344
     *
345
     * @return void
346
     */
347 3
    public function load(LoaderInterface ...$loaders) : void
348
    {
349 3
        foreach ($loaders as $loader) {
350 3
            $this->addRoute(...$loader->load()->all());
351
        }
352 3
    }
353
354
    /**
355
     * Compares the given route host and the given request host
356
     *
357
     * Returns `true` if the route host is `null`
358
     * or if the route host is equal to the request host,
359
     * otherwise returns `false`.
360
     *
361
     * @param null|string $routeHost
362
     * @param string $requestHost
363
     *
364
     * @return bool
365
     */
366 15
    private function compareHosts(?string $routeHost, string $requestHost) : bool
367
    {
368 15
        if (null === $routeHost) {
369 15
            return true;
370
        }
371
372 1
        if ($requestHost === $routeHost) {
373 1
            return true;
374
        }
375
376 1
        if (!empty($this->hosts[$routeHost])) {
377 1
            foreach ($this->hosts[$routeHost] as $hostname) {
378 1
                if ($hostname === $requestHost) {
379 1
                    return true;
380
                }
381
            }
382
        }
383
384 1
        return false;
385
    }
386
}
387