Passed
Push — master ( 04051f...b6349f )
by Alexander
06:10
created

Route::name()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 3
c 1
b 0
f 0
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 10
cc 1
nc 1
nop 1
crap 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 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
    }
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 21
    private function validateMiddleware($middleware): void
200
    {
201
        if (
202 21
            is_string($middleware) && is_subclass_of($middleware, MiddlewareInterface::class)
203
        ) {
204 1
            return;
205
        }
206
207 20
        if (is_callable($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 (is_callable($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
    /**
245
     * Prepends a handler that should be invoked for a matching route.
246
     * Last added handler will be invoked first.
247
     *
248
     * Parameter can be a PSR middleware class name, handler action
249
     * (an array of [handlerClass, handlerMethod]) or a callable.
250
     *
251
     * For handler action and callable typed parameters are automatically injected using dependency
252
     * injection container passed to the route. Current request and handler could be obtained by
253
     * type-hinting for ServerRequestInterface and RequestHandlerInterface.
254
     *
255
     * @param callable|string|array $middleware
256
     * @return Route
257
     */
258 11
    public function addMiddleware($middleware): self
259
    {
260 11
        $this->validateMiddleware($middleware);
261
262 10
        $route = clone $this;
263 10
        $route->middlewares[] = $middleware;
264 10
        return $route;
265
    }
266
267 2
    public function __toString(): string
268
    {
269 2
        $result = '';
270
271 2
        if ($this->name !== null) {
272 1
            $result .= '[' . $this->name . '] ';
273
        }
274
275 2
        if ($this->methods !== []) {
276 2
            $result .= implode(',', $this->methods) . ' ';
277
        }
278 2
        if ($this->host !== null && strrpos($this->pattern, $this->host) === false) {
279 1
            $result .= $this->host;
280
        }
281 2
        $result .= $this->pattern;
282
283 2
        return $result;
284
    }
285
286 6
    public function getName(): string
287
    {
288 6
        return $this->name ?? (implode(', ', $this->methods) . ' ' . $this->pattern);
289
    }
290
291 9
    public function getMethods(): array
292
    {
293 9
        return $this->methods;
294
    }
295
296 4
    public function getPattern(): string
297
    {
298 4
        return $this->pattern;
299
    }
300
301 1
    public function getHost(): ?string
302
    {
303 1
        return $this->host;
304
    }
305
306 1
    public function getDefaults(): array
307
    {
308 1
        return $this->defaults;
309
    }
310
311 12
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
312
    {
313 12
        if ($this->stack === null) {
314 12
            foreach ($this->middlewares as $middleware) {
315 12
                $handler = $this->wrap($this->prepareMiddleware($middleware), $handler);
316
            }
317 12
            $this->stack = $handler;
318
        }
319
320 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

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

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

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