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

Route::__toString()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 5

Importance

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