Passed
Push — master ( a586e8...77a217 )
by Alexander
01:33
created

Route.php$2 ➔ wrapCallable()   A

Complexity

Conditions 1

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 37
ccs 4
cts 4
cp 1
crap 1
rs 9.328
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A Route.php$1 ➔ __construct() 0 5 1
A Route.php$2 ➔ __construct() 0 4 1
A Route.php$2 ➔ process() 0 3 1
A Route.php$1 ➔ process() 0 4 1
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 MiddlewareInterface[]|callable[]|string[]|array[]
34
     */
35
    private array $middlewares = [];
36
    private array $defaults = [];
37
38
    private function __construct(?ContainerInterface $container = null)
39
    {
40
        $this->container = $container;
41
    }
42
43
    public function withContainer(ContainerInterface $container)
44
    {
45
        $route = clone $this;
46
        $route->container = $container;
47
        return $route;
48
    }
49
50
    public function hasContainer()
51
    {
52
        return $this->container !== null;
53
    }
54
55
    /**
56
     * @param string $pattern
57
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
58
     * @param ContainerInterface $container|null
59
     * @return static
60
     */
61
    public static function get(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
62
    {
63
        return static::methods([Method::GET], $pattern, $middleware, $container);
64
    }
65
66
    /**
67
     * @param string $pattern
68
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
69
     * @param ContainerInterface|null $container
70
     * @return static
71
     */
72
    public static function post(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
73
    {
74
        return static::methods([Method::POST], $pattern, $middleware, $container);
75
    }
76
77
    /**
78
     * @param string $pattern
79
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
80
     * @param ContainerInterface|null $container
81
     * @return static
82
     */
83
    public static function put(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
84
    {
85
        return static::methods([Method::PUT], $pattern, $middleware, $container);
86
    }
87
88
    /**
89
     * @param string $pattern
90
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
91
     * @param ContainerInterface|null $container
92
     * @return static
93
     */
94
    public static function delete(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
95
    {
96
        return static::methods([Method::DELETE], $pattern, $middleware, $container);
97
    }
98
99
    /**
100
     * @param string $pattern
101
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
102
     * @param ContainerInterface|null $container
103
     * @return static
104
     */
105
    public static function patch(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
106
    {
107
        return static::methods([Method::PATCH], $pattern, $middleware, $container);
108
    }
109
110
    /**
111
     * @param string $pattern
112
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
113
     * @param ContainerInterface|null $container
114
     * @return static
115
     */
116
    public static function head(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
117
    {
118
        return static::methods([Method::HEAD], $pattern, $middleware, $container);
119
    }
120
121
    /**
122
     * @param string $pattern
123
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
124
     * @param ContainerInterface|null $container
125
     * @return static
126
     */
127
    public static function options(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
128
    {
129
        return static::methods([Method::OPTIONS], $pattern, $middleware, $container);
130
    }
131
132
    /**
133
     * @param array $methods
134
     * @param string $pattern
135
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
136
     * @param ContainerInterface|null $container
137
     * @return static
138
     */
139
    public static function methods(array $methods, string $pattern, $middleware = null, ?ContainerInterface $container = null): self
140
    {
141
        $route = new static($container);
142
        $route->methods = $methods;
143
        $route->pattern = $pattern;
144
        if ($middleware !== null) {
145
            $route->validateMiddleware($middleware);
146
            $route->middlewares[] = $middleware;
147
        }
148
        return $route;
149
    }
150
151
    /**
152
     * @param string $pattern
153
     * @param MiddlewareInterface|callable|string|array|null $middleware primary route handler {@see addMiddleware()}
154
     * @param ContainerInterface|null $container
155
     * @return static
156
     */
157
    public static function anyMethod(string $pattern, $middleware = null, ?ContainerInterface $container = null): self
158
    {
159
        return static::methods(Method::ANY, $pattern, $middleware, $container);
160
    }
161
162
    public function name(string $name): self
163
    {
164
        $route = clone $this;
165
        $route->name = $name;
166
        return $route;
167
    }
168
169
    public function pattern(string $pattern): self
170
    {
171
        $new = clone $this;
172
        $new->pattern = $pattern;
173
        return $new;
174
    }
175
176
    public function host(string $host): self
177
    {
178
        $route = clone $this;
179
        $route->host = rtrim($host, '/');
180
        return $route;
181
    }
182
183
    /**
184
     * Parameter default values indexed by parameter names
185
     *
186
     * @param array $defaults
187
     * @return self
188
     */
189
    public function defaults(array $defaults): self
190
    {
191
        $route = clone $this;
192
        $route->defaults = $defaults;
193
        return $route;
194
    }
195
196
    /**
197
     * @param MiddlewareInterface|callable|string|array $middleware
198
     */
199
    private function validateMiddleware($middleware): void
200
    {
201
        if (
202
            is_string($middleware) && is_subclass_of($middleware, MiddlewareInterface::class)
203
        ) {
204
            return;
205
        }
206
207
        if (is_callable($middleware)) {
208
            return;
209
        }
210
211
        throw new InvalidArgumentException('Parameter should be either PSR middleware class name, handler action or a callable.');
212
    }
213
214
    /**
215
     * @param MiddlewareInterface|callable|string|array $middleware
216
     * @return MiddlewareInterface|string|array
217
     */
218
    private function prepareMiddleware($middleware)
219
    {
220
        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
        if (is_array($middleware) && !is_object($middleware[0])) {
228
            if ($this->container === null) {
229
                throw new InvalidArgumentException('Route container must not be null for handler action.');
230
            }
231
            return $this->wrapCallable($middleware);
232
        }
233
234
        if (is_callable($middleware)) {
235
            if ($this->container === null) {
236
                throw new InvalidArgumentException('Route container must not be null for callable.');
237
            }
238
            return $this->wrapCallable($middleware);
239
        }
240
241
        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...
242
    }
243
244
    /**
245
     * Prepends a handler that should be invoked for a matching route.
246
     * Last added handler will be invoked first.
247
     *
248
     * Parameter should be either PSR middleware instance, PSR middleware class name, handler action or a callable.
249
     *
250
     * It can be a PSR middleware instance, PSR middleware class name, handler action
251
     * (an array of [handlerClass, handlerMethod]) or a callable.
252
     *
253
     * For handler action and callable typed parameters are automatically injected using dependency
254
     * injection container passed to the route. Current request and handler could be obtained by
255
     * type-hinting for ServerRequestInterface and RequestHandlerInterface.
256
     *
257
     * @param MiddlewareInterface|callable|string|array $middleware
258
     * @return Route
259
     */
260
    public function addMiddleware($middleware): self
261
    {
262
        $this->validateMiddleware($middleware);
263
264
        $route = clone $this;
265
        $route->middlewares[] = $middleware;
266
        return $route;
267
    }
268
269
    public function __toString(): string
270
    {
271
        $result = '';
272
273
        if ($this->name !== null) {
274
            $result .= '[' . $this->name . '] ';
275
        }
276
277
        if ($this->methods !== []) {
278
            $result .= implode(',', $this->methods) . ' ';
279
        }
280
        if ($this->host !== null && strrpos($this->pattern, $this->host) === false) {
281
            $result .= $this->host;
282
        }
283
        $result .= $this->pattern;
284
285
        return $result;
286
    }
287
288
    public function getName(): string
289
    {
290
        return $this->name ?? (implode(', ', $this->methods) . ' ' . $this->pattern);
291
    }
292
293
    public function getMethods(): array
294
    {
295
        return $this->methods;
296
    }
297
298
    public function getPattern(): string
299
    {
300
        return $this->pattern;
301
    }
302
303
    public function getHost(): ?string
304
    {
305
        return $this->host;
306
    }
307
308
    public function getDefaults(): array
309
    {
310
        return $this->defaults;
311
    }
312
313
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
314
    {
315
        if ($this->stack === null) {
316
            foreach ($this->middlewares as $middleware) {
317
                $handler = $this->wrap($this->prepareMiddleware($middleware), $handler);
318
            }
319
            $this->stack = $handler;
320
        }
321
322
        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

322
        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...
323
    }
324
325
    /**
326
     * Wraps handler by middlewares
327
     */
328
    private function wrap(MiddlewareInterface $middleware, RequestHandlerInterface $handler): RequestHandlerInterface
329
    {
330
        return new class($middleware, $handler) implements RequestHandlerInterface {
331
            private MiddlewareInterface $middleware;
332
            private RequestHandlerInterface $handler;
333
334
            public function __construct(MiddlewareInterface $middleware, RequestHandlerInterface $handler)
335
            {
336
                $this->middleware = $middleware;
337
                $this->handler = $handler;
338
            }
339
340
            public function handle(ServerRequestInterface $request): ResponseInterface
341
            {
342
                return $this->middleware->process($request, $this->handler);
343
            }
344
        };
345
    }
346
347 10
    private function wrapCallable($callback): MiddlewareInterface
348
    {
349
        if (is_array($callback) && !is_object($callback[0])) {
350
            [$controller, $action] = $callback;
351
            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

351
            return new class($controller, $action, /** @scrutinizer ignore-type */ $this->container) implements MiddlewareInterface {
Loading history...
352
                private string $class;
353
                private string $method;
354
                private ContainerInterface $container;
355
356
                public function __construct(string $class, string $method, ContainerInterface $container)
357
                {
358
                    $this->class = $class;
359
                    $this->method = $method;
360
                    $this->container = $container;
361
                }
362
363
                public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
364
                {
365
                    $controller = $this->container->get($this->class);
366
                    return (new Injector($this->container))->invoke([$controller, $this->method], [$request, $handler]);
367
                }
368
            };
369
        }
370
371
        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

371
        return new class($callback, /** @scrutinizer ignore-type */ $this->container) implements MiddlewareInterface {
Loading history...
372
            private ContainerInterface $container;
373
            private $callback;
374
375
            public function __construct(callable $callback, ContainerInterface $container)
376
            {
377 10
                $this->callback = $callback;
378 10
                $this->container = $container;
379
            }
380
381
            public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
382
            {
383 10
                return (new Injector($this->container))->invoke($this->callback, [$request, $handler]);
384
            }
385
        };
386
    }
387
}
388