Passed
Push — master ( a5fb1c...659b35 )
by Anatoly
01:04 queued 10s
created

Router::handle()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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

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

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