Passed
Push — master ( cfbfb8...ab20eb )
by Rustam
01:31
created

Route::validateMiddleware()   A

Complexity

Conditions 6
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
dl 0
loc 13
ccs 6
cts 6
cp 1
crap 6
rs 9.2222
c 1
b 0
f 0
eloc 6
nc 3
nop 1
1
<?php
2
3
namespace Yiisoft\Router;
4
5
use InvalidArgumentException;
6
use Psr\Container\ContainerInterface;
7
use Psr\Http\Message\ResponseInterface;
8
use Psr\Http\Message\ServerRequestInterface;
9
use Psr\Http\Server\MiddlewareInterface;
10
use Psr\Http\Server\RequestHandlerInterface;
11
use Yiisoft\Http\Method;
12
use Yiisoft\Injector\Injector;
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(
140
        array $methods,
141
        string $pattern,
142
        $middleware = null,
143
        ?ContainerInterface $container = null
144
    ): self {
145 44
        $route = new self($container);
146 44
        $route->methods = $methods;
147 44
        $route->pattern = $pattern;
148 44
        if ($middleware !== null) {
149 10
            $route->validateMiddleware($middleware);
150 4
            $route->middlewares[] = $middleware;
151
        }
152 38
        return $route;
153
    }
154
155
    /**
156
     * @param string $pattern
157
     * @param callable|string|array|null $middleware primary route handler {@see addMiddleware()}
158
     * @param ContainerInterface|null $container
159
     * @return self
160
     */
161 1
    public static function anyMethod(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
162
    {
163 1
        return self::methods(Method::ANY, $pattern, $middleware, $container);
164
    }
165
166 8
    public function name(string $name): self
167
    {
168 8
        $route = clone $this;
169 8
        $route->name = $name;
170 8
        return $route;
171
    }
172
173 4
    public function pattern(string $pattern): self
174
    {
175 4
        $new = clone $this;
176 4
        $new->pattern = $pattern;
177 4
        return $new;
178
    }
179
180 2
    public function host(string $host): self
181
    {
182 2
        $route = clone $this;
183 2
        $route->host = rtrim($host, '/');
184 2
        return $route;
185
    }
186
187
    /**
188
     * Parameter default values indexed by parameter names
189
     *
190
     * @param array $defaults
191
     * @return self
192
     */
193 1
    public function defaults(array $defaults): self
194
    {
195 1
        $route = clone $this;
196 1
        $route->defaults = $defaults;
197 1
        return $route;
198
    }
199
200
    /**
201
     * @param callable|string|array $middleware
202
     */
203 21
    private function validateMiddleware($middleware): void
204
    {
205
        if (
206 21
            is_string($middleware) && is_subclass_of($middleware, MiddlewareInterface::class)
207
        ) {
208 1
            return;
209
        }
210
211 20
        if ($this->isCallable($middleware) && (!is_array($middleware) || !is_object($middleware[0]))) {
212 13
            return;
213
        }
214
215 7
        throw new InvalidArgumentException('Parameter should be either PSR middleware class name or a callable.');
216
    }
217
218
    /**
219
     * @param callable|string|array $middleware
220
     * @return MiddlewareInterface|string|array
221
     */
222 12
    private function prepareMiddleware($middleware)
223
    {
224 12
        if (is_string($middleware)) {
225
            if ($this->container === null) {
226
                throw new InvalidArgumentException('Route container must not be null for lazy loaded middleware.');
227
            }
228
            return $this->container->get($middleware);
229
        }
230
231 12
        if (is_array($middleware) && !is_object($middleware[0])) {
232 3
            if ($this->container === null) {
233
                throw new InvalidArgumentException('Route container must not be null for handler action.');
234
            }
235 3
            return $this->wrapCallable($middleware);
236
        }
237
238 9
        if ($this->isCallable($middleware)) {
239 9
            if ($this->container === null) {
240
                throw new InvalidArgumentException('Route container must not be null for callable.');
241
            }
242 9
            return $this->wrapCallable($middleware);
243
        }
244
245
        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...
246
    }
247
248 20
    private function isCallable($definition): bool
249
    {
250 20
        if (is_callable($definition)) {
251 13
            return true;
252
        }
253
254 7
        return is_array($definition) && array_keys($definition) === [0, 1] && in_array($definition[1], get_class_methods($definition[0]) ?? [], true);
255
    }
256
257
    /**
258
     * Prepends a handler that should be invoked for a matching route.
259
     * Last added handler will be invoked first.
260
     *
261
     * Parameter can be a PSR middleware class name, handler action
262
     * (an array of [handlerClass, handlerMethod]) or a callable.
263
     *
264
     * For handler action and callable typed parameters are automatically injected using dependency
265
     * injection container passed to the route. Current request and handler could be obtained by
266
     * type-hinting for ServerRequestInterface and RequestHandlerInterface.
267
     *
268
     * @param callable|string|array $middleware
269
     * @return Route
270
     */
271 11
    public function addMiddleware($middleware): self
272
    {
273 11
        $this->validateMiddleware($middleware);
274
275 10
        $route = clone $this;
276 10
        $route->middlewares[] = $middleware;
277 10
        return $route;
278
    }
279
280 2
    public function __toString(): string
281
    {
282 2
        $result = '';
283
284 2
        if ($this->name !== null) {
285 1
            $result .= '[' . $this->name . '] ';
286
        }
287
288 2
        if ($this->methods !== []) {
289 2
            $result .= implode(',', $this->methods) . ' ';
290
        }
291 2
        if ($this->host !== null && strrpos($this->pattern, $this->host) === false) {
292 1
            $result .= $this->host;
293
        }
294 2
        $result .= $this->pattern;
295
296 2
        return $result;
297
    }
298
299 6
    public function getName(): string
300
    {
301 6
        return $this->name ?? (implode(', ', $this->methods) . ' ' . $this->host . $this->pattern);
302
    }
303
304 9
    public function getMethods(): array
305
    {
306 9
        return $this->methods;
307
    }
308
309 4
    public function getPattern(): string
310
    {
311 4
        return $this->pattern;
312
    }
313
314 1
    public function getHost(): ?string
315
    {
316 1
        return $this->host;
317
    }
318
319 1
    public function getDefaults(): array
320
    {
321 1
        return $this->defaults;
322
    }
323
324 12
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
325
    {
326 12
        if ($this->stack === null) {
327 12
            foreach ($this->middlewares as $middleware) {
328 12
                $handler = $this->wrap($this->prepareMiddleware($middleware), $handler);
329
            }
330 12
            $this->stack = $handler;
331
        }
332
333 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

333
        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...
334
    }
335
336
    /**
337
     * Wraps handler by middlewares
338
     */
339 12
    private function wrap(MiddlewareInterface $middleware, RequestHandlerInterface $handler): RequestHandlerInterface
340
    {
341 12
        return new class($middleware, $handler) implements RequestHandlerInterface {
342
            private MiddlewareInterface $middleware;
343
            private RequestHandlerInterface $handler;
344
345
            public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $handler)
346
            {
347 12
                $this->middleware = $middleware;
348 12
                $this->handler = $handler;
349 12
            }
350
351
            public function handle(ServerRequestInterface $request): ResponseInterface
352
            {
353 12
                return $this->middleware->process($request, $this->handler);
354
            }
355
        };
356
    }
357
358 12
    private function wrapCallable($callback): MiddlewareInterface
359
    {
360 12
        if (is_array($callback) && !is_object($callback[0])) {
361 3
            [$controller, $action] = $callback;
362 3
            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

362
            return new class($controller, $action, /** @scrutinizer ignore-type */ $this->container) implements MiddlewareInterface {
Loading history...
363
                private string $class;
364
                private string $method;
365
                private ContainerInterface $container;
366
367
                public function __construct(string $class, string $method, ContainerInterface $container)
368
                {
369 3
                    $this->class = $class;
370 3
                    $this->method = $method;
371 3
                    $this->container = $container;
372 3
                }
373
374
                public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
375
                {
376 3
                    $controller = $this->container->get($this->class);
377 3
                    return (new Injector($this->container))->invoke([$controller, $this->method], [$request, $handler]);
378
                }
379
            };
380
        }
381
382 9
        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

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