Passed
Push — master ( 74361b...e1642a )
by Anatoly
04:30 queued 01:45
created

Router::run()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

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

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

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