Passed
Pull Request — master (#196)
by Rustam
02:36
created

Route::disableMiddleware()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 6
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 9
ccs 8
cts 8
cp 1
crap 1
rs 10
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_METHOD | Attribute::TARGET_CLASS | 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
     * @var array[]|callable[]|string[]
28
     */
29
    private array $middlewares = [];
30
    /**
31
     * @var string[]
32
     */
33
    private array $methods;
34
    /**
35
     * @var string[]
36
     */
37
    private array $hosts = [];
38
    /**
39
     * @var array<string,scalar|Stringable|null>
40
     */
41
    private array $defaults = [];
42
43
    /**
44
     * @param array|callable|string|null $action Action handler. It is a primary middleware definition that
45
     * should be invoked last for a matched route.
46
     * @param array<string,scalar|Stringable|null> $defaults Parameter default values indexed by parameter names.
47
     * @param bool $override Marks route as override. When added it will replace existing route with the same name.
48
     * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled.
49
     * It is useful to avoid invoking one of the parent group middleware for
50
     * a certain route.
51
     */
52 103
    public function __construct(
53
        array $methods,
54
        private string $pattern,
55
        private ?string $name = null,
56
        array|callable|string $action = null,
57
        array $middlewares = [],
58
        array $defaults = [],
59
        array $hosts = [],
60
        private bool $override = false,
61
        private array $disabledMiddlewares = [],
62
    ) {
63 103
        $this->assertListOfStrings($methods, 'methods');
64 103
        $this->assertMiddlewares($middlewares);
65 102
        $this->assertListOfStrings($hosts, 'hosts');
66 101
        $this->methods = $methods;
67 101
        $this->middlewares = $middlewares;
68 101
        $this->hosts = $hosts;
69 101
        $this->defaults = array_map('\strval', $defaults);
70 101
        if (!empty($action)) {
71 1
            $this->middlewares[] = $action;
72 1
            $this->actionAdded = true;
73
        }
74
    }
75
76 74
    public static function get(string $pattern): self
77
    {
78 74
        return self::methods([Method::GET], $pattern);
79
    }
80
81 10
    public static function post(string $pattern): self
82
    {
83 10
        return self::methods([Method::POST], $pattern);
84
    }
85
86 4
    public static function put(string $pattern): self
87
    {
88 4
        return self::methods([Method::PUT], $pattern);
89
    }
90
91 1
    public static function delete(string $pattern): self
92
    {
93 1
        return self::methods([Method::DELETE], $pattern);
94
    }
95
96 1
    public static function patch(string $pattern): self
97
    {
98 1
        return self::methods([Method::PATCH], $pattern);
99
    }
100
101 1
    public static function head(string $pattern): self
102
    {
103 1
        return self::methods([Method::HEAD], $pattern);
104
    }
105
106 9
    public static function options(string $pattern): self
107
    {
108 9
        return self::methods([Method::OPTIONS], $pattern);
109
    }
110
111
    /**
112
     * @param string[] $methods
113
     */
114 86
    public static function methods(array $methods, string $pattern): self
115
    {
116 86
        return new self(
117 86
            methods: $methods,
118 86
            pattern: $pattern
119 86
        );
120
    }
121
122 23
    public function name(string $name): self
123
    {
124 23
        $route = clone $this;
125 23
        $route->name = $name;
126 23
        return $route;
127
    }
128
129 24
    public function pattern(string $pattern): self
130
    {
131 24
        $new = clone $this;
132 24
        $new->pattern = $pattern;
133 24
        return $new;
134
    }
135
136 9
    public function host(string $host): self
137
    {
138 9
        return $this->hosts($host);
139
    }
140
141 13
    public function hosts(string ...$hosts): self
142
    {
143 13
        $route = clone $this;
144 13
        $route->hosts = [];
145
146 13
        foreach ($hosts as $host) {
147 13
            $host = rtrim($host, '/');
148
149 13
            if ($host !== '' && !in_array($host, $route->hosts, true)) {
150 12
                $route->hosts[] = $host;
151
            }
152
        }
153
154 13
        return $route;
155
    }
156
157
    /**
158
     * Marks route as override. When added it will replace existing route with the same name.
159
     */
160 4
    public function override(): self
161
    {
162 4
        $route = clone $this;
163 4
        $route->override = true;
164 4
        return $route;
165
    }
166
167
    /**
168
     * Parameter default values indexed by parameter names.
169
     *
170
     * @psalm-param array<string,null|Stringable|scalar> $defaults
171
     */
172 3
    public function defaults(array $defaults): self
173
    {
174 3
        $route = clone $this;
175 3
        $route->defaults = array_map('\strval', $defaults);
176 3
        return $route;
177
    }
178
179
    /**
180
     * Appends a handler middleware definition that should be invoked for a matched route.
181
     * First added handler will be executed first.
182
     */
183 23
    public function middleware(array|callable|string ...$middlewareDefinition): self
184
    {
185 23
        if ($this->actionAdded) {
186 1
            throw new RuntimeException('middleware() can not be used after action().');
187
        }
188 22
        $route = clone $this;
189 22
        array_push(
190 22
            $route->middlewares,
191 22
            ...array_values($middlewareDefinition)
192 22
        );
193 22
        $route->builtMiddlewares = [];
194 22
        return $route;
195
    }
196
197
    /**
198
     * Prepends a handler middleware definition that should be invoked for a matched route.
199
     * Last added handler will be executed first.
200
     */
201 23
    public function prependMiddleware(array|callable|string ...$middlewareDefinition): self
202
    {
203 23
        if (!$this->actionAdded) {
204 1
            throw new RuntimeException('prependMiddleware() can not be used before action().');
205
        }
206 22
        $route = clone $this;
207 22
        array_unshift(
208 22
            $route->middlewares,
209 22
            ...array_values($middlewareDefinition)
210 22
        );
211 22
        $route->builtMiddlewares = [];
212 22
        return $route;
213
    }
214
215
    /**
216
     * Appends action handler. It is a primary middleware definition that should be invoked last for a matched route.
217
     */
218 26
    public function action(array|callable|string $middlewareDefinition): self
219
    {
220 26
        $route = clone $this;
221 26
        $route->middlewares[] = $middlewareDefinition;
222 26
        $route->actionAdded = true;
223 26
        $route->builtMiddlewares = [];
224 26
        return $route;
225
    }
226
227
    /**
228
     * Excludes middleware from being invoked when action is handled.
229
     * It is useful to avoid invoking one of the parent group middleware for
230
     * a certain route.
231
     */
232 3
    public function disableMiddleware(mixed ...$middlewareDefinition): self
233
    {
234 3
        $route = clone $this;
235 3
        array_push(
236 3
            $route->disabledMiddlewares,
237 3
            ...array_values($middlewareDefinition)
238 3
        );
239 3
        $route->builtMiddlewares = [];
240 3
        return $route;
241
    }
242
243
    /**
244
     * @psalm-template T as string
245
     *
246
     * @psalm-param T $key
247
     *
248
     * @psalm-return (
249
     *   T is ('name'|'pattern') ? string :
250
     *       (T is 'host' ? string|null :
251
     *           (T is 'hosts' ? array<array-key, string> :
252
     *               (T is 'methods' ? array<array-key,string> :
253
     *                   (T is 'defaults' ? array<string,string> :
254
     *                       (T is ('override'|'hasMiddlewares') ? bool :
255
     *                           (T is 'builtMiddlewares' ? array<array-key,array|callable|string> : mixed)
256
     *                       )
257
     *                   )
258
     *               )
259
     *           )
260
     *       )
261
     *    )
262
     */
263 73
    public function getData(string $key): mixed
264
    {
265 73
        return match ($key) {
266 73
            'name' => $this->name ??
267 32
                (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern),
268 73
            'pattern' => $this->pattern,
269 73
            'host' => $this->hosts[0] ?? null,
270 73
            'hosts' => $this->hosts,
271 73
            'methods' => $this->methods,
272 73
            'defaults' => $this->defaults,
273 73
            'override' => $this->override,
274 73
            'hasMiddlewares' => !empty($this->middlewares),
275 73
            'builtMiddlewares' => $this->getBuiltMiddlewares(),
276 73
            default => throw new InvalidArgumentException('Unknown data key: ' . $key),
277 73
        };
278
    }
279
280 5
    public function __toString(): string
281
    {
282 5
        $result = $this->name === null
283 2
            ? ''
284 3
            : '[' . $this->name . '] ';
285
286 5
        if ($this->methods !== []) {
287 5
            $result .= implode(',', $this->methods) . ' ';
288
        }
289
290 5
        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...
291 2
            $quoted = array_map(static fn ($host) => preg_quote($host, '/'), $this->hosts);
292
293 2
            if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) {
294 2
                $result .= implode('|', $this->hosts);
295
            }
296
        }
297
298 5
        $result .= $this->pattern;
299
300 5
        return $result;
301
    }
302
303 1
    public function __debugInfo()
304
    {
305 1
        return [
306 1
            'name' => $this->name,
307 1
            'methods' => $this->methods,
308 1
            'pattern' => $this->pattern,
309 1
            'hosts' => $this->hosts,
310 1
            'defaults' => $this->defaults,
311 1
            'override' => $this->override,
312 1
            'actionAdded' => $this->actionAdded,
313 1
            'middlewares' => $this->middlewares,
314 1
            'builtMiddlewares' => $this->builtMiddlewares,
315 1
            'disabledMiddlewares' => $this->disabledMiddlewares,
316 1
        ];
317
    }
318
319
    /**
320
     * @return array[]|callable[]|string[]
321
     */
322 21
    private function getBuiltMiddlewares(): array
323
    {
324
        // Don't build middlewares if we did it earlier.
325
        // This improves performance in event-loop applications.
326 21
        if (!empty($this->builtMiddlewares)) {
327 1
            return $this->builtMiddlewares;
328
        }
329
330 21
        $builtMiddlewares = $this->middlewares;
331
332 21
        foreach ($builtMiddlewares as $index => $definition) {
333 20
            if (in_array($definition, $this->disabledMiddlewares, true)) {
334 1
                unset($builtMiddlewares[$index]);
335
            }
336
        }
337
338 21
        return $this->builtMiddlewares = $builtMiddlewares;
339
    }
340
341
    /**
342
     * @psalm-assert array<string> $items
343
     */
344 103
    private function assertListOfStrings(array $items, string $argument): void
345
    {
346 103
        foreach ($items as $item) {
347 103
            if (!is_string($item)) {
348 1
                throw new \InvalidArgumentException('Invalid $' . $argument . ' provided, list of string expected.');
349
            }
350
        }
351
    }
352
353
    /**
354
     * @psalm-assert array<array|callable|string> $middlewares
355
     */
356 103
    private function assertMiddlewares(array $middlewares): void
357
    {
358
        /** @var mixed $middleware */
359 103
        foreach ($middlewares as $middleware) {
360 2
            if (is_string($middleware)) {
361 1
                continue;
362
            }
363
364 1
            if (is_callable($middleware) || is_array($middleware)) {
365 1
                continue;
366
            }
367
368 1
            throw new \InvalidArgumentException(
369 1
                'Invalid $middlewares provided, list of string or array or callable expected.'
370 1
            );
371
        }
372
    }
373
}
374