Passed
Branch release/v2.0.0 (2af3f3)
by Anatoly
04:30
created

Router   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 247
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 9
Bugs 0 Features 0
Metric Value
wmc 26
eloc 59
c 9
b 0
f 0
dl 0
loc 247
ccs 69
cts 69
cp 1
rs 10

12 Methods

Rating   Name   Duplication   Size   Complexity  
A process() 0 7 2
A getRoute() 0 9 2
A generateUri() 0 7 1
A addMiddleware() 0 12 3
A getAllowedMethods() 0 10 3
A match() 0 26 6
A getMiddlewares() 0 3 1
A load() 0 4 2
A getRoutes() 0 3 1
A addRoute() 0 12 3
A handle() 0 8 1
A group() 0 9 1
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\ExceptionInterface;
23
use Sunrise\Http\Router\Exception\InvalidAttributeValueException;
24
use Sunrise\Http\Router\Exception\MethodNotAllowedException;
25
use Sunrise\Http\Router\Exception\MiddlewareAlreadyExistsException;
26
use Sunrise\Http\Router\Exception\MissingAttributeValueException;
27
use Sunrise\Http\Router\Exception\RouteAlreadyExistsException;
28
use Sunrise\Http\Router\Exception\RouteNotFoundException;
29
use Sunrise\Http\Router\Loader\LoaderInterface;
30
use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler;
31
32
/**
33
 * Import functions
34
 */
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 routes
55
     *
56
     * @var RouteInterface[]
57
     */
58
    private $routes = [];
59
60
    /**
61
     * The router middlewares
62
     *
63
     * @var MiddlewareInterface[]
64
     */
65
    private $middlewares = [];
66
67
    /**
68
     * Gets the router routes
69
     *
70
     * @return RouteInterface[]
71
     */
72 4
    public function getRoutes() : array
73
    {
74 4
        return array_values($this->routes);
75
    }
76
77
    /**
78
     * Gets the router middlewares
79
     *
80
     * @return MiddlewareInterface[]
81
     */
82 5
    public function getMiddlewares() : array
83
    {
84 5
        return array_values($this->middlewares);
85
    }
86
87
    /**
88
     * Adds the given route(s) to the router
89
     *
90
     * @param RouteInterface ...$routes
91
     *
92
     * @return void
93
     *
94
     * @throws RouteAlreadyExistsException
95
     */
96 20
    public function addRoute(RouteInterface ...$routes) : void
97
    {
98 20
        foreach ($routes as $route) {
99 20
            $name = $route->getName();
100
101 20
            if (isset($this->routes[$name])) {
102 1
                throw new RouteAlreadyExistsException(
103 1
                    sprintf('A route with the name "%s" already exists.', $name)
104
                );
105
            }
106
107 20
            $this->routes[$name] = $route;
108
        }
109 20
    }
110
111
    /**
112
     * Adds the given middleware(s) to the router
113
     *
114
     * @param MiddlewareInterface ...$middlewares
115
     *
116
     * @return void
117
     *
118
     * @throws MiddlewareAlreadyExistsException
119
     */
120 4
    public function addMiddleware(MiddlewareInterface ...$middlewares) : void
121
    {
122 4
        foreach ($middlewares as $middleware) {
123 4
            $hash = spl_object_hash($middleware);
124
125 4
            if (isset($this->middlewares[$hash])) {
126 1
                throw new MiddlewareAlreadyExistsException(
127 1
                    sprintf('A middleware with the hash "%s" already exists.', $hash)
128
                );
129
            }
130
131 4
            $this->middlewares[$hash] = $middleware;
132
        }
133 4
    }
134
135
    /**
136
     * Gets allowed HTTP methods
137
     *
138
     * @return string[]
139
     */
140 4
    public function getAllowedMethods() : array
141
    {
142 4
        $methods = [];
143 4
        foreach ($this->routes as $route) {
144 4
            foreach ($route->getMethods() as $method) {
145 4
                $methods[$method] = true;
146
            }
147
        }
148
149 4
        return array_keys($methods);
150
    }
151
152
    /**
153
     * Gets a route for the given name
154
     *
155
     * @param string $name
156
     *
157
     * @return RouteInterface
158
     *
159
     * @throws RouteNotFoundException
160
     */
161 3
    public function getRoute(string $name) : RouteInterface
162
    {
163 3
        if (!isset($this->routes[$name])) {
164 1
            throw new RouteNotFoundException(
165 1
                sprintf('No route found with the name "%s".', $name)
166
            );
167
        }
168
169 2
        return $this->routes[$name];
170
    }
171
172
    /**
173
     * Generates a URI for the given named route
174
     *
175
     * @param string $name
176
     * @param array $attributes
177
     * @param bool $strict
178
     *
179
     * @return string
180
     *
181
     * @throws RouteNotFoundException
182
     *
183
     * @throws InvalidAttributeValueException May be thrown from `path_build`.
184
     * @throws MissingAttributeValueException May be thrown from `path_build`.
185
     */
186 1
    public function generateUri(string $name, array $attributes = [], bool $strict = false) : string
187
    {
188 1
        $route = $this->getRoute($name);
189
190 1
        $attributes += $route->getAttributes();
191
192 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

192
        return /** @scrutinizer ignore-call */ path_build($route->getPath(), $attributes, $strict);
Loading history...
193
    }
194
195
    /**
196
     * Looks for a route that matches the given request
197
     *
198
     * @param ServerRequestInterface $request
199
     *
200
     * @return RouteInterface
201
     *
202
     * @throws MethodNotAllowedException
203
     * @throws RouteNotFoundException
204
     */
205 11
    public function match(ServerRequestInterface $request) : RouteInterface
206
    {
207 11
        $routes = [];
208 11
        foreach ($this->routes as $route) {
209 11
            foreach ($route->getMethods() as $method) {
210 11
                $routes[$method][] = $route;
211
            }
212
        }
213
214 11
        $method = $request->getMethod();
215 11
        if (!isset($routes[$method])) {
216 3
            throw new MethodNotAllowedException(
217 3
                $this->getAllowedMethods(),
218 3
                sprintf('No route found for the method "%s".', $method)
219
            );
220
        }
221
222 8
        $target = $request->getUri()->getPath();
223 8
        foreach ($routes[$method] as $route) {
224 8
            if (path_match($route->getPath(), $target, $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

224
            if (/** @scrutinizer ignore-call */ path_match($route->getPath(), $target, $attributes)) {
Loading history...
Comprehensibility Best Practice introduced by
The variable $attributes seems to be never defined.
Loading history...
225 8
                return $route->withAddedAttributes($attributes);
226
            }
227
        }
228
229 3
        throw new RouteNotFoundException(
230 3
            sprintf('No route found for the URI "%s".', $target)
231
        );
232
    }
233
234
    /**
235
     * {@inheritDoc}
236
     */
237 8
    public function handle(ServerRequestInterface $request) : ResponseInterface
238
    {
239 8
        $route = $this->match($request);
240
241 4
        $handler = new QueueableRequestHandler($route);
242 4
        $handler->add(...$this->getMiddlewares());
243
244 4
        return $handler->handle($request);
245
    }
246
247
    /**
248
     * {@inheritDoc}
249
     */
250 3
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
251
    {
252
        try {
253 3
            return $this->handle($request);
254 2
        } catch (ExceptionInterface $e) {
255 2
            return $handler->handle(
256 2
                $request->withAttribute(self::ATTR_NAME_FOR_ROUTING_ERROR, $e)
257
            );
258
        }
259
    }
260
261
    /**
262
     * Route grouping logic
263
     *
264
     * @param callable $callback
265
     *
266
     * @return RouteCollectionGroupActionInterface
267
     */
268 1
    public function group(callable $callback) : RouteCollectionGroupActionInterface
269
    {
270 1
        $collector = new RouteCollector();
271
272 1
        $callback($collector);
273
274 1
        $this->addRoute(...$collector->getCollection()->all());
275
276 1
        return new RouteCollectionGroupAction($collector->getCollection());
277
    }
278
279
    /**
280
     * Loads routes through the given loaders
281
     *
282
     * @param LoaderInterface ...$loaders
283
     *
284
     * @return void
285
     */
286 2
    public function load(LoaderInterface ...$loaders) : void
287
    {
288 2
        foreach ($loaders as $loader) {
289 2
            $this->addRoute(...$loader->load()->all());
290
        }
291 2
    }
292
}
293