Passed
Pull Request — master (#196)
by Rustam
03:32
created

Route::getDispatcherWithMiddlewares()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 8
nc 5
nop 0
dl 0
loc 20
ccs 9
cts 9
cp 1
crap 5
rs 9.6111
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A Route::action() 0 7 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router;
6
7
use Attribute;
8
use InvalidArgumentException;
9
use RuntimeException;
10
use Stringable;
11
use Yiisoft\Http\Method;
12
13
use function in_array;
14
15
/**
16
 * Route defines a mapping from URL to callback / name and vice versa.
17
 */
18
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
19
class Route implements Stringable
20
{
21
    private bool $actionAdded = false;
22
    /**
23
     * @var array[]|callable[]|string[]
24
     */
25
    private array $builtMiddlewares = [];
26
27
    /**
28
     * @param array $defaults Parameter default values indexed by parameter names.
29
     * @param bool $override Marks route as override. When added it will replace existing route with the same name.
30
     * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled.
31
     * It is useful to avoid invoking one of the parent group middleware for
32
     * a certain route.
33
     */
34 71
    public function __construct(
35
        private array $methods,
36
        private string $pattern,
37
        private ?string $name = null,
38
        private array $middlewares = [],
39
        private array $defaults = [],
40
        private array $hosts = [],
41
        private bool $override = false,
42
        private array $disabledMiddlewares = [],
43
    ) {
44 71
    }
45
46 60
    public static function get(
47
        string $pattern,
48
        ?string $name = null,
49
        array $middlewares = [],
50
        array $defaults = [],
51
        array $hosts = [],
52
        bool $override = false,
53
        array $disabledMiddlewares = []
54
    ): self {
55 60
        return self::methods(
56 60
            [Method::GET],
57 60
            $pattern,
58 60
            $name,
59 60
            $middlewares,
60 60
            $defaults,
61 60
            $hosts,
62 60
            $override,
63 60
            $disabledMiddlewares
64 60
        );
65
    }
66
67 8
    public static function post(
68
        string $pattern,
69
        ?string $name = null,
70
        array $middlewares = [],
71
        array $defaults = [],
72
        array $hosts = [],
73
        bool $override = false,
74
        array $disabledMiddlewares = []
75
    ): self {
76 8
        return self::methods(
77 8
            [Method::POST],
78 8
            $pattern,
79 8
            $name,
80 8
            $middlewares,
81 8
            $defaults,
82 8
            $hosts,
83 8
            $override,
84 8
            $disabledMiddlewares
85 8
        );
86
    }
87
88 4
    public static function put(
89
        string $pattern,
90
        ?string $name = null,
91
        array $middlewares = [],
92
        array $defaults = [],
93
        array $hosts = [],
94
        bool $override = false,
95
        array $disabledMiddlewares = []
96
    ): self {
97 4
        return self::methods(
98 4
            [Method::PUT],
99 4
            $pattern,
100 4
            $name,
101 4
            $middlewares,
102 4
            $defaults,
103 4
            $hosts,
104 4
            $override,
105 4
            $disabledMiddlewares
106 4
        );
107
    }
108
109 1
    public static function delete(
110
        string $pattern,
111
        ?string $name = null,
112
        array $middlewares = [],
113
        array $defaults = [],
114
        array $hosts = [],
115
        bool $override = false,
116
        array $disabledMiddlewares = []
117
    ): self {
118 1
        return self::methods(
119 1
            [Method::DELETE],
120 1
            $pattern,
121 1
            $name,
122 1
            $middlewares,
123 1
            $defaults,
124 1
            $hosts,
125 1
            $override,
126 1
            $disabledMiddlewares
127 1
        );
128
    }
129
130 1
    public static function patch(
131
        string $pattern,
132
        ?string $name = null,
133
        array $middlewares = [],
134
        array $defaults = [],
135
        array $hosts = [],
136
        bool $override = false,
137
        array $disabledMiddlewares = []
138
    ): self {
139 1
        return self::methods(
140 1
            [Method::PATCH],
141 1
            $pattern,
142 1
            $name,
143 1
            $middlewares,
144 1
            $defaults,
145 1
            $hosts,
146 1
            $override,
147 1
            $disabledMiddlewares
148 1
        );
149
    }
150
151 1
    public static function head(
152
        string $pattern,
153
        ?string $name = null,
154
        array $middlewares = [],
155
        array $defaults = [],
156
        array $hosts = [],
157
        bool $override = false,
158
        array $disabledMiddlewares = []
159
    ): self {
160 1
        return self::methods(
161 1
            [Method::HEAD],
162 1
            $pattern,
163 1
            $name,
164 1
            $middlewares,
165 1
            $defaults,
166 1
            $hosts,
167 1
            $override,
168 1
            $disabledMiddlewares
169 1
        );
170
    }
171
172 7
    public static function options(
173
        string $pattern,
174
        ?string $name = null,
175
        array $middlewares = [],
176
        array $defaults = [],
177
        array $hosts = [],
178
        bool $override = false,
179
        array $disabledMiddlewares = []
180
    ): self {
181 7
        return self::methods(
182 7
            [Method::OPTIONS],
183 7
            $pattern,
184 7
            $name,
185 7
            $middlewares,
186 7
            $defaults,
187 7
            $hosts,
188 7
            $override,
189 7
            $disabledMiddlewares
190 7
        );
191
    }
192
193
    /**
194
     * @param string[] $methods
195
     */
196 71
    public static function methods(
197
        array $methods,
198
        string $pattern,
199
        ?string $name = null,
200
        array $middlewares = [],
201
        array $defaults = [],
202
        array $hosts = [],
203
        bool $override = false,
204
        array $disabledMiddlewares = []
205
    ): self {
206 71
        return new self(
207 71
            methods: $methods,
208 71
            pattern: $pattern,
209 71
            name: $name,
210 71
            middlewares: $middlewares,
211 71
            defaults: $defaults,
212 71
            hosts: $hosts,
213 71
            override: $override,
214 71
            disabledMiddlewares: $disabledMiddlewares
215 71
        );
216
    }
217
218 23
    public function name(string $name): self
219
    {
220 23
        $route = clone $this;
221 23
        $route->name = $name;
222 23
        return $route;
223
    }
224
225 21
    public function pattern(string $pattern): self
226
    {
227 21
        $new = clone $this;
228 21
        $new->pattern = $pattern;
229 21
        return $new;
230
    }
231
232 8
    public function host(string $host): self
233
    {
234 8
        return $this->hosts($host);
235
    }
236
237 12
    public function hosts(string ...$hosts): self
238
    {
239 12
        $route = clone $this;
240 12
        $route->hosts = [];
241
242 12
        foreach ($hosts as $host) {
243 12
            $host = rtrim($host, '/');
244
245 12
            if ($host !== '' && !in_array($host, $route->hosts, true)) {
246 11
                $route->hosts[] = $host;
247
            }
248
        }
249
250 12
        return $route;
251
    }
252
253
    /**
254
     * Marks route as override. When added it will replace existing route with the same name.
255
     */
256 4
    public function override(): self
257
    {
258 4
        $route = clone $this;
259 4
        $route->override = true;
260 4
        return $route;
261
    }
262
263
    /**
264
     * Parameter default values indexed by parameter names.
265
     *
266
     * @psalm-param array<string,null|Stringable|scalar> $defaults
267
     */
268 3
    public function defaults(array $defaults): self
269
    {
270 3
        $route = clone $this;
271 3
        $route->defaults = array_map('\strval', $defaults);
272 3
        return $route;
273
    }
274
275
    /**
276
     * Appends a handler middleware definition that should be invoked for a matched route.
277
     * First added handler will be executed first.
278
     */
279 17
    public function middleware(array|callable|string ...$middlewareDefinition): self
280
    {
281 17
        if ($this->actionAdded) {
282 1
            throw new RuntimeException('middleware() can not be used after action().');
283
        }
284 16
        $route = clone $this;
285 16
        array_push(
286 16
            $route->middlewares,
287 16
            ...array_values($middlewareDefinition)
288 16
        );
289 16
        $route->builtMiddlewares = [];
290 16
        return $route;
291
    }
292
293
    /**
294
     * Prepends a handler middleware definition that should be invoked for a matched route.
295
     * Last added handler will be executed first.
296
     */
297 18
    public function prependMiddleware(array|callable|string ...$middlewareDefinition): self
298
    {
299 18
        if (!$this->actionAdded) {
300 1
            throw new RuntimeException('prependMiddleware() can not be used before action().');
301
        }
302 17
        $route = clone $this;
303 17
        array_unshift(
304 17
            $route->middlewares,
305 17
            ...array_values($middlewareDefinition)
306 17
        );
307 17
        $route->builtMiddlewares = [];
308 17
        return $route;
309
    }
310
311
    /**
312
     * Appends action handler. It is a primary middleware definition that should be invoked last for a matched route.
313
     */
314 20
    public function action(array|callable|string $middlewareDefinition): self
315
    {
316 20
        $route = clone $this;
317 20
        $route->middlewares[] = $middlewareDefinition;
318 20
        $route->actionAdded = true;
319 20
        $route->builtMiddlewares = [];
320 20
        return $route;
321
    }
322
323
    /**
324
     * Excludes middleware from being invoked when action is handled.
325
     * It is useful to avoid invoking one of the parent group middleware for
326
     * a certain route.
327
     */
328 3
    public function disableMiddleware(mixed ...$middlewareDefinition): self
329
    {
330 3
        $route = clone $this;
331 3
        array_push(
332 3
            $route->disabledMiddlewares,
333 3
            ...array_values($middlewareDefinition)
334 3
        );
335 3
        $route->builtMiddlewares = [];
336 3
        return $route;
337
    }
338
339
    /**
340
     * @psalm-template T as string
341
     *
342
     * @psalm-param T $key
343
     *
344
     * @psalm-return (
345
     *   T is ('name'|'pattern') ? string :
346
     *       (T is 'host' ? string|null :
347
     *           (T is 'hosts' ? array<array-key, string> :
348
     *               (T is 'methods' ? array<array-key,string> :
349
     *                   (T is 'defaults' ? array<string,string> :
350
     *                       (T is ('override'|'hasMiddlewares') ? bool : mixed)
351
     *                   )
352
     *               )
353
     *           )
354
     *       )
355
     *    )
356
     */
357 45
    public function getData(string $key): mixed
358
    {
359 45
        return match ($key) {
360 45
            'name' => $this->name ??
361 26
                (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern),
362 45
            'pattern' => $this->pattern,
363 45
            'host' => $this->hosts[0] ?? null,
364 45
            'hosts' => $this->hosts,
365 45
            'methods' => $this->methods,
366 45
            'defaults' => $this->defaults,
367 45
            'override' => $this->override,
368 45
            'hasMiddlewares' => $this->middlewares !== [],
369 45
            default => throw new InvalidArgumentException('Unknown data key: ' . $key),
370 45
        };
371
    }
372
373 4
    public function __toString(): string
374
    {
375 4
        $result = $this->name === null
376 1
            ? ''
377 3
            : '[' . $this->name . '] ';
378
379 4
        if ($this->methods !== []) {
380 4
            $result .= implode(',', $this->methods) . ' ';
381
        }
382
383 4
        if ($this->hosts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->hosts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
384 2
            $quoted = array_map(static fn ($host) => preg_quote($host, '/'), $this->hosts);
385
386 2
            if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) {
387 2
                $result .= implode('|', $this->hosts);
388
            }
389
        }
390
391 4
        $result .= $this->pattern;
392
393 4
        return $result;
394
    }
395
396 1
    public function __debugInfo()
397
    {
398 1
        return [
399 1
            'name' => $this->name,
400 1
            'methods' => $this->methods,
401 1
            'pattern' => $this->pattern,
402 1
            'hosts' => $this->hosts,
403 1
            'defaults' => $this->defaults,
404 1
            'override' => $this->override,
405 1
            'actionAdded' => $this->actionAdded,
406 1
            'middlewares' => $this->middlewares,
407 1
            'builtMiddlewares' => $this->builtMiddlewares,
408 1
            'disabledMiddlewares' => $this->disabledMiddlewares,
409 1
        ];
410
    }
411
412
    /**
413
     * @return array[]|callable[]|string[]
414
     */
415 15
    public function getBuiltMiddlewares(): array
416
    {
417
        // Don't build middlewares if we did it earlier.
418
        // This improves performance in event-loop applications.
419 15
        if ($this->builtMiddlewares !== []) {
420
            return $this->builtMiddlewares;
421
        }
422
423 15
        $builtMiddlewares = $this->middlewares;
424
425 15
        foreach ($builtMiddlewares as $index => $definition) {
426 14
            if (in_array($definition, $this->disabledMiddlewares, true)) {
427 1
                unset($builtMiddlewares[$index]);
428
            }
429
        }
430
431 15
        return $this->builtMiddlewares = $builtMiddlewares;
432
    }
433
}
434