Router   A
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 267
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 37
Bugs 0 Features 3
Metric Value
eloc 91
c 37
b 0
f 3
dl 0
loc 267
ccs 99
cts 99
cp 1
rs 9.44
wmc 37

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A getRoute() 0 9 2
B match() 0 38 8
A hasRoute() 0 5 1
A getRoutes() 0 5 2
A handle() 0 3 1
A getRequestHandler() 0 19 4
A runRoute() 0 27 5
A compileRoute() 0 4 1
A load() 0 15 4
A buildRoute() 0 16 4
A getRouteRequestHandler() 0 19 4
1
<?php
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-router
10
 */
11
12
declare(strict_types=1);
13
14
namespace Sunrise\Http\Router;
15
16
use InvalidArgumentException;
17
use LogicException;
18
use Psr\EventDispatcher\EventDispatcherInterface;
19
use Psr\Http\Message\ResponseInterface;
20
use Psr\Http\Message\ServerRequestInterface;
21
use Psr\Http\Server\RequestHandlerInterface;
22
use Sunrise\Http\Router\Dictionary\HeaderName;
23
use Sunrise\Http\Router\Dictionary\PlaceholderCode;
24
use Sunrise\Http\Router\Event\RoutePostRunEvent;
25
use Sunrise\Http\Router\Event\RoutePreRunEvent;
26
use Sunrise\Http\Router\Exception\HttpException;
27
use Sunrise\Http\Router\Exception\HttpExceptionFactory;
28
use Sunrise\Http\Router\Helper\RouteBuilder;
29
use Sunrise\Http\Router\Helper\RouteCompiler;
30
use Sunrise\Http\Router\Helper\RouteMatcher;
31
use Sunrise\Http\Router\Loader\LoaderInterface;
32
use Sunrise\Http\Router\RequestHandler\CallableRequestHandler;
33
use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler;
34
use UnexpectedValueException;
35
36
use function array_flip;
37
use function array_keys;
38
use function array_merge;
39
use function rawurldecode;
40
use function sprintf;
41
42
final class Router implements RouterInterface
43
{
44
    /** @var array<string, RouteInterface> */
45
    private array $routes = [];
46
47
    /** @var array<string, non-empty-string> */
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<string, non-empty-string> at position 4 could not be parsed: Unknown type name 'non-empty-string' at position 4 in array<string, non-empty-string>.
Loading history...
48
    private array $routePatterns = [];
49
50
    /** @var array<string, RequestHandlerInterface> */
51
    private array $routeRequestHandlers = [];
52
53
    private ?RequestHandlerInterface $requestHandler = null;
54
55
    private bool $isLoaded = false;
56
57
    /**
58
     * @since 3.0.0
59
     */
60 58
    public function __construct(
61
        private readonly ReferenceResolverInterface $referenceResolver,
62
        /** @var array<array-key, LoaderInterface> */
63
        private readonly array $loaders,
64
        /** @var array<array-key, mixed> */
65
        private readonly array $middlewares = [],
66
        /** @var array<array-key, mixed> */
67
        private readonly array $routeMiddlewares = [],
68
        private readonly ?EventDispatcherInterface $eventDispatcher = null,
69
    ) {
70 58
    }
71
72
    /**
73
     * @inheritDoc
74
     *
75
     * @throws InvalidArgumentException
76
     */
77 35
    public function getRoutes(): array
78
    {
79 35
        $this->isLoaded or $this->load();
80
81 34
        return $this->routes;
82
    }
83
84
    /**
85
     * @inheritDoc
86
     *
87
     * @throws InvalidArgumentException
88
     */
89 15
    public function getRoute(string $name): RouteInterface
90
    {
91 15
        $routes = $this->getRoutes();
92
93 15
        if (!isset($routes[$name])) {
94 3
            throw new InvalidArgumentException(sprintf('The route "%s" does not exist.', $name));
95
        }
96
97 12
        return $routes[$name];
98
    }
99
100
    /**
101
     * @inheritDoc
102
     *
103
     * @throws InvalidArgumentException
104
     */
105 2
    public function hasRoute(string $name): bool
106
    {
107 2
        $routes = $this->getRoutes();
108
109 2
        return isset($routes[$name]);
110
    }
111
112
    /**
113
     * @inheritDoc
114
     *
115
     * @throws HttpException
116
     * @throws InvalidArgumentException
117
     * @throws LogicException
118
     */
119 1
    public function handle(ServerRequestInterface $request): ResponseInterface
120
    {
121 1
        return $this->getRequestHandler()->handle($request);
122
    }
123
124
    /**
125
     * @inheritDoc
126
     *
127
     * @throws HttpException
128
     * @throws InvalidArgumentException
129
     * @throws LogicException
130
     */
131 13
    public function match(ServerRequestInterface $request): RouteInterface
132
    {
133 13
        $routes = $this->getRoutes();
134
135 13
        if ($routes === []) {
136 1
            throw new LogicException('The router does not contain any routes.');
137
        }
138
139 12
        $requestPath = rawurldecode($request->getUri()->getPath());
140 12
        $requestMethod = $request->getMethod();
141 12
        $allowedMethods = [];
142
143 12
        foreach ($routes as $route) {
144
            try {
145 12
                if (!RouteMatcher::matchRoute($this->compileRoute($route), $requestPath, $matches)) {
146 7
                    continue;
147
                }
148 5
            } catch (UnexpectedValueException $e) {
149 1
                throw HttpExceptionFactory::malformedUri(previous: $e);
150
            }
151
152 6
            $routeMethods = array_flip($route->getMethods());
153 6
            if (!isset($routeMethods[$requestMethod])) {
154 1
                $allowedMethods += $routeMethods;
155 1
                continue;
156
            }
157
158 5
            return $matches === [] ? $route : $route->withAddedAttributes($matches);
159
        }
160
161 2
        if ($allowedMethods !== []) {
162 1
            throw HttpExceptionFactory::methodNotAllowed()
163 1
                ->addMessagePlaceholder(PlaceholderCode::REQUEST_METHOD, $requestMethod)
164 1
                ->addHeaderField(HeaderName::ALLOW, ...array_keys($allowedMethods));
165
        }
166
167 1
        throw HttpExceptionFactory::resourceNotFound()
168 1
            ->addMessagePlaceholder(PlaceholderCode::REQUEST_URI, $requestPath);
169
    }
170
171
    /**
172
     * @inheritDoc
173
     *
174
     * @throws InvalidArgumentException
175
     */
176 8
    public function runRoute(RouteInterface|string $route, ServerRequestInterface $request): ResponseInterface
177
    {
178 8
        if (! $route instanceof RouteInterface) {
179 2
            $route = $this->getRoute($route);
180
        }
181
182 7
        foreach ($route->getAttributes() as $name => $value) {
183 2
            $request = $request->withAttribute($name, $value);
184
        }
185
186 7
        $request = $request->withAttribute(RouteInterface::class, $route);
187
188 7
        if ($this->eventDispatcher !== null) {
189 7
            $event = new RoutePreRunEvent($route, $request);
190 7
            $this->eventDispatcher->dispatch($event);
191 7
            $request = $event->request;
192
        }
193
194 7
        $response = $this->getRouteRequestHandler($route)->handle($request);
195
196 7
        if ($this->eventDispatcher !== null) {
197 7
            $event = new RoutePostRunEvent($route, $request, $response);
198 7
            $this->eventDispatcher->dispatch($event);
199 7
            $response = $event->response;
200
        }
201
202 7
        return $response;
203
    }
204
205
    /**
206
     * @inheritDoc
207
     *
208
     * @throws InvalidArgumentException
209
     */
210 27
    public function buildRoute(RouteInterface|string $route, array $values = [], bool $strictly = false): string
211
    {
212 27
        if (! $route instanceof RouteInterface) {
213 11
            $route = $this->getRoute($route);
214
        }
215
216 26
        $result = RouteBuilder::buildRoute($route->getPath(), $values + $route->getAttributes());
217
218 21
        if ($strictly && !RouteMatcher::matchRoute($this->compileRoute($route), $result)) {
219 1
            throw new InvalidArgumentException(sprintf(
220 1
                'The route "%s" could not be built because one of the values does not match its pattern.',
221 1
                $route->getName(),
222 1
            ));
223
        }
224
225 20
        return $result;
226
    }
227
228
    /**
229
     * @throws InvalidArgumentException
230
     *
231
     * @return non-empty-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
232
     */
233 15
    private function compileRoute(RouteInterface $route): string
234
    {
235 15
        return $this->routePatterns[$route->getName()] ??= $route->getPattern()
236 13
            ?? RouteCompiler::compileRoute($route->getPath(), $route->getPatterns());
237
    }
238
239
    /**
240
     * @internal
241
     *
242
     * @throws InvalidArgumentException
243
     */
244 8
    public function getRouteRequestHandler(RouteInterface $route): RequestHandlerInterface
245
    {
246 8
        $name = $route->getName();
247 8
        if (isset($this->routeRequestHandlers[$name])) {
248 1
            return $this->routeRequestHandlers[$name];
249
        }
250
251 8
        $this->routeRequestHandlers[$name] = $this->referenceResolver
252 8
            ->resolveRequestHandler($route->getRequestHandler());
253
254 8
        $middlewares = array_merge($this->routeMiddlewares, $route->getMiddlewares());
255 8
        if ($middlewares !== []) {
256 2
            $this->routeRequestHandlers[$name] = new QueueableRequestHandler($this->routeRequestHandlers[$name]);
257 2
            foreach ($middlewares as $middleware) {
258 2
                $this->routeRequestHandlers[$name]->enqueue($this->referenceResolver->resolveMiddleware($middleware));
259
            }
260
        }
261
262 8
        return $this->routeRequestHandlers[$name];
263
    }
264
265
    /**
266
     * @internal
267
     *
268
     * @throws InvalidArgumentException
269
     */
270 2
    public function getRequestHandler(): RequestHandlerInterface
271
    {
272 2
        if ($this->requestHandler !== null) {
273 1
            return $this->requestHandler;
274
        }
275
276 2
        $this->requestHandler = new CallableRequestHandler(
277 2
            fn(ServerRequestInterface $request): ResponseInterface =>
278 2
                $this->runRoute($this->match($request), $request),
279 2
        );
280
281 2
        if ($this->middlewares !== []) {
282 1
            $this->requestHandler = new QueueableRequestHandler($this->requestHandler);
0 ignored issues
show
Bug introduced by
$this->requestHandler of type null is incompatible with the type Psr\Http\Server\RequestHandlerInterface expected by parameter $endpoint of Sunrise\Http\Router\Requ...tHandler::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

282
            $this->requestHandler = new QueueableRequestHandler(/** @scrutinizer ignore-type */ $this->requestHandler);
Loading history...
283 1
            foreach ($this->middlewares as $middleware) {
284 1
                $this->requestHandler->enqueue($this->referenceResolver->resolveMiddleware($middleware));
0 ignored issues
show
Bug introduced by
The method enqueue() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

284
                $this->requestHandler->/** @scrutinizer ignore-call */ 
285
                                       enqueue($this->referenceResolver->resolveMiddleware($middleware));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
285
            }
286
        }
287
288 2
        return $this->requestHandler;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->requestHandler returns the type null which is incompatible with the type-hinted return Psr\Http\Server\RequestHandlerInterface.
Loading history...
289
    }
290
291
    /**
292
     * @throws InvalidArgumentException
293
     */
294 35
    private function load(): void
295
    {
296 35
        foreach ($this->loaders as $loader) {
297 30
            foreach ($loader->load() as $route) {
298 30
                $name = $route->getName();
299
300 30
                if (isset($this->routes[$name])) {
301 1
                    throw new InvalidArgumentException(sprintf('The route "%s" already exists.', $name));
302
                }
303
304 30
                $this->routes[$name] = $route;
305
            }
306
        }
307
308 34
        $this->isLoaded = true;
309
    }
310
}
311