Passed
Push — master ( 70c68f...c76078 )
by Alexander
06:58
created

Route::prepareMiddleware()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
eloc 13
c 0
b 0
f 0
dl 0
loc 24
ccs 0
cts 0
cp 0
rs 8.4444
cc 8
nc 7
nop 1
crap 72
1
<?php
2
3
namespace Yiisoft\Router;
4
5
use InvalidArgumentException;
6
use Yiisoft\Http\Method;
7
use Yiisoft\Router\Middleware\Callback;
8
use Yiisoft\Router\Middleware\ActionCaller;
9
use Psr\Container\ContainerInterface;
10
use Psr\Http\Server\MiddlewareInterface;
11
use Psr\Http\Server\RequestHandlerInterface;
12
use Psr\Http\Message\ServerRequestInterface;
13
use Psr\Http\Message\ResponseInterface;
14
15
/**
16
 * Route defines a mapping from URL to callback / name and vice versa
17
 */
18
final class Route implements MiddlewareInterface
19
{
20
    private ?string $name = null;
21
    /** @var string[] */
22
    private array $methods;
23
    private string $pattern;
24
    private ?string $host = null;
25
    private ?ContainerInterface $container = null;
26
    /**
27
     * Contains a chain of middleware wrapped in handlers.
28
     * Each handler points to the handler of middleware that will be processed next.
29
     * @var RequestHandlerInterface|null stack of middleware
30
     */
31
    private ?RequestHandlerInterface $stack = null;
32
33
    /**
34
     * @var MiddlewareInterface[]|callable[]|string[]|array[]
35
     */
36
    private array $middlewares = [];
37
    private array $defaults = [];
38
39
    private function __construct(?ContainerInterface $container = null)
40
    {
41
        $this->container = $container;
42
    }
43
44
    public function withContainer(ContainerInterface $container)
45
    {
46
        $route = clone $this;
47
        $route->container = $container;
48
        return $route;
49
    }
50
51
    public function hasContainer()
52
    {
53
        return $this->container !== null;
54
    }
55
56
    /**
57
     * @param string $pattern
58
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
59
     * @param ContainerInterface $container|null
60
     * @return static
61
     */
62
    public static function get(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
63
    {
64
        return static::methods([Method::GET], $pattern, $middleware, $container);
65
    }
66
67
    /**
68
     * @param string $pattern
69
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
70
     * @param ContainerInterface|null $container
71
     * @return static
72
     */
73
    public static function post(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
74
    {
75
        return static::methods([Method::POST], $pattern, $middleware, $container);
76
    }
77
78
    /**
79
     * @param string $pattern
80
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
81
     * @param ContainerInterface|null $container
82
     * @return static
83
     */
84
    public static function put(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
85
    {
86
        return static::methods([Method::PUT], $pattern, $middleware, $container);
87
    }
88
89
    /**
90
     * @param string $pattern
91
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
92
     * @param ContainerInterface|null $container
93
     * @return static
94
     */
95
    public static function delete(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
96
    {
97
        return static::methods([Method::DELETE], $pattern, $middleware, $container);
98
    }
99
100
    /**
101
     * @param string $pattern
102
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
103
     * @param ContainerInterface|null $container
104
     * @return static
105
     */
106
    public static function patch(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
107
    {
108
        return static::methods([Method::PATCH], $pattern, $middleware, $container);
109
    }
110
111
    /**
112
     * @param string $pattern
113
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
114
     * @param ContainerInterface|null $container
115
     * @return static
116
     */
117
    public static function head(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
118
    {
119
        return static::methods([Method::HEAD], $pattern, $middleware, $container);
120
    }
121
122
    /**
123
     * @param string $pattern
124
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
125
     * @param ContainerInterface|null $container
126
     * @return static
127
     */
128
    public static function options(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
129
    {
130
        return static::methods([Method::OPTIONS], $pattern, $middleware, $container);
131
    }
132
133
    /**
134
     * @param array $methods
135
     * @param string $pattern
136
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
137
     * @param ContainerInterface|null $container
138
     * @return static
139
     */
140
    public static function methods(array $methods, string $pattern, $middleware = null, ?ContainerInterface $container = null): self
141
    {
142
        $route = new static($container);
143
        $route->methods = $methods;
144
        $route->pattern = $pattern;
145
        if ($middleware !== null) {
146
            $route->validateMiddleware($middleware);
147
            $route->middlewares[] = $middleware;
148
        }
149
        return $route;
150
    }
151
152
    /**
153
     * @param string $pattern
154
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
155
     * @param ContainerInterface|null $container
156
     * @return static
157
     */
158
    public static function anyMethod(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
159
    {
160
        return static::methods(Method::ANY, $pattern, $middleware, $container);
161
    }
162
163
    public function name(string $name): self
164
    {
165
        $route = clone $this;
166
        $route->name = $name;
167
        return $route;
168
    }
169
170
    public function pattern(string $pattern): self
171
    {
172
        $new = clone $this;
173
        $new->pattern = $pattern;
174
        return $new;
175
    }
176
177
    public function host(string $host): self
178
    {
179
        $route = clone $this;
180
        $route->host = rtrim($host, '/');
181
        return $route;
182
    }
183
184
    /**
185
     * Parameter default values indexed by parameter names
186
     *
187
     * @param array $defaults
188
     * @return self
189
     */
190
    public function defaults(array $defaults): self
191
    {
192
        $route = clone $this;
193
        $route->defaults = $defaults;
194
        return $route;
195
    }
196
197
    /**
198
     * @param MiddlewareInterface|callable|string|array $middleware
199
     */
200
    private function validateMiddleware($middleware): void
201
    {
202
        if (
203
            is_string($middleware) && is_subclass_of($middleware, MiddlewareInterface::class)
204
        ) {
205
            return;
206
        }
207
208
        if (is_callable($middleware)) {
209
            return;
210
        }
211
212
        if (!$middleware instanceof MiddlewareInterface) {
213
            throw new InvalidArgumentException('Parameter should be either PSR middleware instance, PSR middleware class name, handler action or a callable.');
214
        }
215
    }
216
217
    /**
218
     * @param MiddlewareInterface|callable|string|array $middleware
219
     * @return MiddlewareInterface|string|array
220
     */
221
    private function prepareMiddleware($middleware)
222
    {
223
        if (is_string($middleware)) {
224
            if ($this->container === null) {
225
                throw new InvalidArgumentException('Route container must not be null for lazy loaded middleware.');
226
            }
227
            return $this->container->get($middleware);
228
        }
229
230
        if (is_array($middleware) && !is_object($middleware[0])) {
231
            if ($this->container === null) {
232
                throw new InvalidArgumentException('Route container must not be null for handler action.');
233
            }
234
            return new ActionCaller($middleware[0], $middleware[1], $this->container);
235
        }
236
237
        if (is_callable($middleware)) {
238
            if ($this->container === null) {
239
                throw new InvalidArgumentException('Route container must not be null for callable.');
240
            }
241
            return new Callback($middleware, $this->container);
0 ignored issues
show
Bug introduced by
It seems like $middleware can also be of type Psr\Http\Server\MiddlewareInterface; however, parameter $callback of Yiisoft\Router\Middleware\Callback::__construct() does only seem to accept callable, 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

241
            return new Callback(/** @scrutinizer ignore-type */ $middleware, $this->container);
Loading history...
242
        }
243
244
        return $middleware;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $middleware also could return the type callable which is incompatible with the documented return type Psr\Http\Server\MiddlewareInterface|array|string.
Loading history...
245
    }
246
247
    /**
248
     * Prepends a handler that should be invoked for a matching route.
249
     * Last added handler will be invoked first.
250
     *
251
     * Parameter should be either PSR middleware instance, PSR middleware class name, handler action or a callable.
252
     *
253
     * It can be a PSR middleware instance, PSR middleware class name, handler action
254
     * (an array of [handlerClass, handlerMethod]) or a callable.
255
     *
256
     * For handler action and callable typed parameters are automatically injected using dependency
257
     * injection container passed to the route. Current request and handler could be obtained by
258
     * type-hinting for ServerRequestInterface and RequestHandlerInterface.
259
     *
260
     * @param MiddlewareInterface|callable|string|array $middleware
261
     * @return Route
262
     */
263
    public function addMiddleware($middleware): self
264
    {
265
        $this->validateMiddleware($middleware);
266
267
        $route = clone $this;
268
        array_unshift($route->middlewares, $middleware);
269
        return $route;
270
    }
271
272
    public function __toString(): string
273
    {
274
        $result = '';
275
276
        if ($this->name !== null) {
277
            $result .= '[' . $this->name . '] ';
278
        }
279
280
        if ($this->methods !== []) {
281
            $result .= implode(',', $this->methods) . ' ';
282
        }
283
        if ($this->host !== null && strrpos($this->pattern, $this->host) === false) {
284
            $result .= $this->host;
285
        }
286
        $result .= $this->pattern;
287
288
        return $result;
289
    }
290
291
    public function getName(): string
292
    {
293
        return $this->name ?? (implode(', ', $this->methods) . ' ' . $this->pattern);
294
    }
295
296
    public function getMethods(): array
297
    {
298
        return $this->methods;
299
    }
300
301
    public function getPattern(): string
302
    {
303
        return $this->pattern;
304
    }
305
306
    public function getHost(): ?string
307
    {
308
        return $this->host;
309
    }
310
311
    public function getDefaults(): array
312
    {
313
        return $this->defaults;
314
    }
315
316
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
317
    {
318
        if ($this->stack === null) {
319
            for ($i = count($this->middlewares) - 1; $i >= 0; $i--) {
320
                $handler = $this->wrap($this->prepareMiddleware($this->middlewares[$i]), $handler);
321
            }
322
            $this->stack = $handler;
323
        }
324
325
        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

325
        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...
326
    }
327
328
    /**
329
     * Wraps handler by middlewares
330
     */
331 11
    private function wrap(MiddlewareInterface $middleware, RequestHandlerInterface $handler): RequestHandlerInterface
332
    {
333
        return new class($middleware, $handler) implements RequestHandlerInterface {
334
            private MiddlewareInterface $middleware;
335
            private RequestHandlerInterface $handler;
336
337
            public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $handler)
338
            {
339 11
                $this->middleware = $middleware;
340 11
                $this->handler = $handler;
341
            }
342
343 11
            public function handle(ServerRequestInterface $request): ResponseInterface
344
            {
345 11
                return $this->middleware->process($request, $this->handler);
346
            }
347
        };
348
    }
349
}
350