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

Route::post()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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