Passed
Pull Request — master (#196)
by Sergei
02:31
created

Route::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 10
c 1
b 0
f 0
nc 2
nop 9
dl 0
loc 21
ccs 11
cts 11
cp 1
crap 2
rs 9.9332

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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 $builtMiddlewares = [];
24
    /**
25
     * @var array[]|callable[]|string[]
26
     */
27
    private array $middlewares = [];
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 $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 103
    public function __construct(
51
        array $methods,
52
        private string $pattern,
53
        private ?string $name = null,
54
        array|callable|string $action = null,
55
        array $middlewares = [],
56
        array $defaults = [],
57
        array $hosts = [],
58
        private bool $override = false,
59
        private array $disabledMiddlewares = [],
60
    ) {
61 103
        $this->assertListOfStrings($methods, 'methods');
62 103
        $this->assertMiddlewares($middlewares);
63 102
        $this->assertListOfStrings($hosts, 'hosts');
64 101
        $this->methods = $methods;
65 101
        $this->middlewares = $middlewares;
66 101
        $this->hosts = $hosts;
67 101
        $this->defaults = array_map('\strval', $defaults);
68 101
        if (!empty($action)) {
69 1
            $this->middlewares[] = $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->middlewares,
189 22
            ...array_values($middlewareDefinition)
190 22
        );
191 22
        $route->builtMiddlewares = [];
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->middlewares,
207 22
            ...array_values($middlewareDefinition)
208 22
        );
209 22
        $route->builtMiddlewares = [];
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->middlewares[] = $middlewareDefinition;
220 26
        $route->actionAdded = true;
221 26
        $route->builtMiddlewares = [];
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->disabledMiddlewares,
235 3
            ...array_values($middlewareDefinition)
236 3
        );
237 3
        $route->builtMiddlewares = [];
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 'builtMiddlewares' ? 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->middlewares),
273 73
            'builtMiddlewares' => $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
            'middlewares' => $this->middlewares,
312 1
            'builtMiddlewares' => $this->builtMiddlewares,
313 1
            'disabledMiddlewares' => $this->disabledMiddlewares,
314 1
        ];
315
    }
316
317
    /**
318
     * @return array[]|callable[]|string[]
319
     */
320 21
    private function getBuiltMiddlewares(): array
321
    {
322
        // Don't build middlewares if we did it earlier.
323
        // This improves performance in event-loop applications.
324 21
        if (!empty($this->builtMiddlewares)) {
325 1
            return $this->builtMiddlewares;
326
        }
327
328 21
        $builtMiddlewares = $this->middlewares;
329
330 21
        foreach ($builtMiddlewares as $index => $definition) {
331 20
            if (in_array($definition, $this->disabledMiddlewares, true)) {
332 1
                unset($builtMiddlewares[$index]);
333
            }
334
        }
335
336 21
        return $this->builtMiddlewares = $builtMiddlewares;
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> $middlewares
353
     */
354 103
    private function assertMiddlewares(array $middlewares): void
355
    {
356
        /** @var mixed $middleware */
357 103
        foreach ($middlewares as $middleware) {
358 2
            if (is_string($middleware)) {
359 1
                continue;
360
            }
361
362 1
            if (is_callable($middleware) || is_array($middleware)) {
363 1
                continue;
364
            }
365
366 1
            throw new \InvalidArgumentException(
367 1
                'Invalid $middlewares provided, list of string or array or callable expected.'
368 1
            );
369
        }
370
    }
371
}
372