Passed
Push — master ( 1c2f0d...cfbfb8 )
by Alexander
16:54 queued 10:16
created

Route.php$2 ➔ process()   A

Complexity

Conditions 2

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
cc 2
crap 2
rs 10
1
<?php
2
3
namespace Yiisoft\Router;
4
5
use InvalidArgumentException;
6
use Yiisoft\Http\Method;
7
use Yiisoft\Injector\Injector;
8
use Psr\Container\ContainerInterface;
9
use Psr\Http\Server\MiddlewareInterface;
10
use Psr\Http\Server\RequestHandlerInterface;
11
use Psr\Http\Message\ServerRequestInterface;
12
use Psr\Http\Message\ResponseInterface;
13
14
/**
15
 * Route defines a mapping from URL to callback / name and vice versa
16
 */
17
final class Route implements MiddlewareInterface
18
{
19
    private ?string $name = null;
20
    /** @var string[] */
21
    private array $methods;
22
    private string $pattern;
23
    private ?string $host = null;
24
    private ?ContainerInterface $container = null;
25
    /**
26
     * Contains a stack of middleware wrapped in handlers.
27
     * Each handler points to the handler of middleware that will be processed next.
28
     * @var RequestHandlerInterface|null stack of middleware
29
     */
30
    private ?RequestHandlerInterface $stack = null;
31
32
    /**
33
     * @var callable[]|string[]|array[]
34
     */
35
    private array $middlewares = [];
36
    private array $defaults = [];
37
38 44
    private function __construct(?ContainerInterface $container = null)
39
    {
40 44
        $this->container = $container;
41 44
    }
42
43 7
    public function withContainer(ContainerInterface $container)
44
    {
45 7
        $route = clone $this;
46 7
        $route->container = $container;
47 7
        return $route;
48
    }
49
50 10
    public function hasContainer()
51
    {
52 10
        return $this->container !== null;
53
    }
54
55
    /**
56
     * @param string $pattern
57
     * @param callable|string|array|null $middleware primary route handler {@see addMiddleware()}
58
     * @param ContainerInterface $container|null
59
     * @return self
60
     */
61 34
    public static function get(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
62
    {
63 34
        return self::methods([Method::GET], $pattern, $middleware, $container);
64
    }
65
66
    /**
67
     * @param string $pattern
68
     * @param callable|string|array|null $middleware primary route handler {@see addMiddleware()}
69
     * @param ContainerInterface|null $container
70
     * @return self
71
     */
72 4
    public static function post(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
73
    {
74 4
        return self::methods([Method::POST], $pattern, $middleware, $container);
75
    }
76
77
    /**
78
     * @param string $pattern
79
     * @param callable|string|array|null $middleware primary route handler {@see addMiddleware()}
80
     * @param ContainerInterface|null $container
81
     * @return self
82
     */
83 1
    public static function put(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
84
    {
85 1
        return self::methods([Method::PUT], $pattern, $middleware, $container);
86
    }
87
88
    /**
89
     * @param string $pattern
90
     * @param callable|string|array|null $middleware primary route handler {@see addMiddleware()}
91
     * @param ContainerInterface|null $container
92
     * @return self
93
     */
94 1
    public static function delete(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
95
    {
96 1
        return self::methods([Method::DELETE], $pattern, $middleware, $container);
97
    }
98
99
    /**
100
     * @param string $pattern
101
     * @param callable|string|array|null $middleware primary route handler {@see addMiddleware()}
102
     * @param ContainerInterface|null $container
103
     * @return self
104
     */
105 1
    public static function patch(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
106
    {
107 1
        return self::methods([Method::PATCH], $pattern, $middleware, $container);
108
    }
109
110
    /**
111
     * @param string $pattern
112
     * @param callable|string|array|null $middleware primary route handler {@see addMiddleware()}
113
     * @param ContainerInterface|null $container
114
     * @return self
115
     */
116 1
    public static function head(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
117
    {
118 1
        return self::methods([Method::HEAD], $pattern, $middleware, $container);
119
    }
120
121
    /**
122
     * @param string $pattern
123
     * @param callable|string|array|null $middleware primary route handler {@see addMiddleware()}
124
     * @param ContainerInterface|null $container
125
     * @return self
126
     */
127 1
    public static function options(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
128
    {
129 1
        return self::methods([Method::OPTIONS], $pattern, $middleware, $container);
130
    }
131
132
    /**
133
     * @param array $methods
134
     * @param string $pattern
135
     * @param callable|string|array|null $middleware primary route handler {@see addMiddleware()}
136
     * @param ContainerInterface|null $container
137
     * @return self
138
     */
139 44
    public static function methods(array $methods, string $pattern, $middleware = null, ?ContainerInterface $container = null): self
140
    {
141 44
        $route = new self($container);
142 44
        $route->methods = $methods;
143 44
        $route->pattern = $pattern;
144 44
        if ($middleware !== null) {
145 10
            $route->validateMiddleware($middleware);
146 4
            $route->middlewares[] = $middleware;
147
        }
148 38
        return $route;
149
    }
150
151
    /**
152
     * @param string $pattern
153
     * @param callable|string|array|null $middleware primary route handler {@see addMiddleware()}
154
     * @param ContainerInterface|null $container
155
     * @return self
156
     */
157 1
    public static function anyMethod(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
158
    {
159 1
        return self::methods(Method::ANY, $pattern, $middleware, $container);
160
    }
161
162 8
    public function name(string $name): self
163
    {
164 8
        $route = clone $this;
165 8
        $route->name = $name;
166 8
        return $route;
167
    }
168
169 4
    public function pattern(string $pattern): self
170
    {
171 4
        $new = clone $this;
172 4
        $new->pattern = $pattern;
173 4
        return $new;
174
    }
175
176 2
    public function host(string $host): self
177
    {
178 2
        $route = clone $this;
179 2
        $route->host = rtrim($host, '/');
180 2
        return $route;
181
    }
182
183
    /**
184
     * Parameter default values indexed by parameter names
185
     *
186
     * @param array $defaults
187
     * @return self
188
     */
189 1
    public function defaults(array $defaults): self
190
    {
191 1
        $route = clone $this;
192 1
        $route->defaults = $defaults;
193 1
        return $route;
194
    }
195
196
    /**
197
     * @param callable|string|array $middleware
198
     */
199 20
    private function validateMiddleware($middleware): void
200
    {
201
        if (
202
            is_string($middleware) && is_subclass_of($middleware, MiddlewareInterface::class)
203
        ) {
204 1
            return;
205
        }
206
207 20
        if ($this->isCallable($middleware) && (!is_array($middleware) || !is_object($middleware[0]))) {
208 13
            return;
209
        }
210
211 7
        throw new InvalidArgumentException('Parameter should be either PSR middleware class name or a callable.');
212
    }
213
214
    /**
215
     * @param callable|string|array $middleware
216
     * @return MiddlewareInterface|string|array
217
     */
218 12
    private function prepareMiddleware($middleware)
219
    {
220 12
        if (is_string($middleware)) {
221
            if ($this->container === null) {
222
                throw new InvalidArgumentException('Route container must not be null for lazy loaded middleware.');
223
            }
224
            return $this->container->get($middleware);
225
        }
226
227 12
        if (is_array($middleware) && !is_object($middleware[0])) {
228 3
            if ($this->container === null) {
229
                throw new InvalidArgumentException('Route container must not be null for handler action.');
230
            }
231 3
            return $this->wrapCallable($middleware);
232
        }
233
234 9
        if ($this->isCallable($middleware)) {
235 9
            if ($this->container === null) {
236
                throw new InvalidArgumentException('Route container must not be null for callable.');
237
            }
238 9
            return $this->wrapCallable($middleware);
239
        }
240
241
        return $middleware;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $middleware returns the type callable which is incompatible with the documented return type Psr\Http\Server\MiddlewareInterface|array|string.
Loading history...
242
    }
243
244 20
    private function isCallable($definition): bool
245
    {
246 20
        if (is_callable($definition)) {
247 13
            return true;
248
        }
249
250 7
        return is_array($definition) && array_keys($definition) === [0, 1] && in_array($definition[1], get_class_methods($definition[0]) ?? [], true);
251
    }
252
253
    /**
254
     * Prepends a handler that should be invoked for a matching route.
255
     * Last added handler will be invoked first.
256
     *
257
     * Parameter can be a PSR middleware class name, handler action
258
     * (an array of [handlerClass, handlerMethod]) or a callable.
259
     *
260
     * For handler action and callable typed parameters are automatically injected using dependency
261
     * injection container passed to the route. Current request and handler could be obtained by
262
     * type-hinting for ServerRequestInterface and RequestHandlerInterface.
263
     *
264
     * @param callable|string|array $middleware
265
     * @return Route
266
     */
267 11
    public function addMiddleware($middleware): self
268
    {
269 11
        $this->validateMiddleware($middleware);
270
271 10
        $route = clone $this;
272 10
        $route->middlewares[] = $middleware;
273 10
        return $route;
274
    }
275
276 2
    public function __toString(): string
277
    {
278 2
        $result = '';
279
280 2
        if ($this->name !== null) {
281 1
            $result .= '[' . $this->name . '] ';
282
        }
283
284 2
        if ($this->methods !== []) {
285 2
            $result .= implode(',', $this->methods) . ' ';
286
        }
287 2
        if ($this->host !== null && strrpos($this->pattern, $this->host) === false) {
288 1
            $result .= $this->host;
289
        }
290 2
        $result .= $this->pattern;
291
292 2
        return $result;
293
    }
294
295 6
    public function getName(): string
296
    {
297 6
        return $this->name ?? (implode(', ', $this->methods) . ' ' . $this->pattern);
298
    }
299
300 9
    public function getMethods(): array
301
    {
302 9
        return $this->methods;
303
    }
304
305 4
    public function getPattern(): string
306
    {
307 4
        return $this->pattern;
308
    }
309
310 1
    public function getHost(): ?string
311
    {
312 1
        return $this->host;
313
    }
314
315 1
    public function getDefaults(): array
316
    {
317 1
        return $this->defaults;
318
    }
319
320 12
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
321
    {
322 12
        if ($this->stack === null) {
323 12
            foreach ($this->middlewares as $middleware) {
324 12
                $handler = $this->wrap($this->prepareMiddleware($middleware), $handler);
325
            }
326 12
            $this->stack = $handler;
327
        }
328
329 12
        return $this->stack->handle($request);
0 ignored issues
show
Bug introduced by
The method handle() 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

329
        return $this->stack->/** @scrutinizer ignore-call */ handle($request);

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...
330
    }
331
332
    /**
333
     * Wraps handler by middlewares
334
     */
335 12
    private function wrap(MiddlewareInterface $middleware, RequestHandlerInterface $handler): RequestHandlerInterface
336
    {
337
        return new class($middleware, $handler) implements RequestHandlerInterface {
338
            private MiddlewareInterface $middleware;
339
            private RequestHandlerInterface $handler;
340
341
            public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $handler)
342
            {
343 12
                $this->middleware = $middleware;
344 12
                $this->handler = $handler;
345 12
            }
346
347
            public function handle(ServerRequestInterface $request): ResponseInterface
348
            {
349 12
                return $this->middleware->process($request, $this->handler);
350
            }
351
        };
352
    }
353
354 12
    private function wrapCallable($callback): MiddlewareInterface
355
    {
356 12
        if (is_array($callback) && !is_object($callback[0])) {
357 3
            [$controller, $action] = $callback;
358
            return new class($controller, $action, $this->container) implements MiddlewareInterface {
0 ignored issues
show
Bug introduced by
It seems like $this->container can also be of type null; however, parameter $container of anonymous//src/Route.php$1::__construct() does only seem to accept Psr\Container\ContainerInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

358
            return new class($controller, $action, /** @scrutinizer ignore-type */ $this->container) implements MiddlewareInterface {
Loading history...
359
                private string $class;
360
                private string $method;
361
                private ContainerInterface $container;
362
363
                public function __construct(string $class, string $method, ContainerInterface $container)
364
                {
365 3
                    $this->class = $class;
366 3
                    $this->method = $method;
367 3
                    $this->container = $container;
368 3
                }
369
370
                public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
371
                {
372 3
                    $controller = $this->container->get($this->class);
373 3
                    return (new Injector($this->container))->invoke([$controller, $this->method], [$request, $handler]);
374
                }
375
            };
376
        }
377
378
        return new class($callback, $this->container) implements MiddlewareInterface {
0 ignored issues
show
Bug introduced by
It seems like $this->container can also be of type null; however, parameter $container of anonymous//src/Route.php$2::__construct() does only seem to accept Psr\Container\ContainerInterface, maybe add an additional type check? ( Ignorable by Annotation )

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

378
        return new class($callback, /** @scrutinizer ignore-type */ $this->container) implements MiddlewareInterface {
Loading history...
379
            private ContainerInterface $container;
380
            private $callback;
381
382
            public function __construct(callable $callback, ContainerInterface $container)
383
            {
384 9
                $this->callback = $callback;
385 9
                $this->container = $container;
386 9
            }
387
388
            public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
389
            {
390 9
                $response = (new Injector($this->container))->invoke($this->callback, [$request, $handler]);
391 9
                return $response instanceof MiddlewareInterface ? $response->process($request, $handler) : $response;
392
            }
393
        };
394
    }
395
}
396