Passed
Pull Request — master (#196)
by Rustam
02:41
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 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 90
    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 90
        $this->assertListOfStrings($methods, 'methods');
62 90
        $this->assertMiddlewares($middlewares);
63 89
        $this->assertListOfStrings($hosts, 'hosts');
64 88
        $this->methods = $methods;
65 88
        $this->middlewares = $middlewares;
66 88
        $this->hosts = $hosts;
67 88
        $this->defaults = array_map('\strval', $defaults);
68 88
        if (!empty($action)) {
69 1
            $this->middlewares[] = $action;
70 1
            $this->actionAdded = true;
71
        }
72
    }
73
74 67
    public static function get(string $pattern): self
75
    {
76 67
        return self::methods([Method::GET], $pattern);
77
    }
78
79 9
    public static function post(string $pattern): self
80
    {
81 9
        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 79
    public static function methods(array $methods, string $pattern): self
113
    {
114 79
        return new self(
115 79
            methods: $methods,
116 79
            pattern: $pattern
117 79
        );
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 27
    public function action(array|callable|string $middlewareDefinition): self
217
    {
218 27
        $route = clone $this;
219 27
        $route->middlewares[] = $middlewareDefinition;
220 27
        $route->actionAdded = true;
221 27
        $route->builtMiddlewares = [];
222 27
        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 67
    public function getData(string $key): mixed
262
    {
263 67
        return match ($key) {
264 67
            'name' => $this->name ??
265 32
                (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern),
266 67
            'pattern' => $this->pattern,
267 67
            'host' => $this->hosts[0] ?? null,
268 67
            'hosts' => $this->hosts,
269 67
            'methods' => $this->methods,
270 67
            'defaults' => $this->defaults,
271 67
            'override' => $this->override,
272 67
            'hasMiddlewares' => !empty($this->middlewares),
273 67
            'builtMiddlewares' => $this->getBuiltMiddlewares(),
274 67
            default => throw new InvalidArgumentException('Unknown data key: ' . $key),
275 67
        };
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 22
    private function getBuiltMiddlewares(): array
321
    {
322
        // Don't build middlewares if we did it earlier.
323
        // This improves performance in event-loop applications.
324 22
        if (!empty($this->builtMiddlewares)) {
325 2
            return $this->builtMiddlewares;
326
        }
327
328 22
        $builtMiddlewares = $this->middlewares;
329
330 22
        foreach ($builtMiddlewares as $index => $definition) {
331 21
            if (in_array($definition, $this->disabledMiddlewares, true)) {
332 1
                unset($builtMiddlewares[$index]);
333
            }
334
        }
335
336 22
        return $this->builtMiddlewares = $builtMiddlewares;
337
    }
338
339
    /**
340
     * @psalm-assert array<string> $items
341
     */
342 90
    private function assertListOfStrings(array $items, string $argument): void
343
    {
344 90
        foreach ($items as $item) {
345 90
            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 90
    private function assertMiddlewares(array $middlewares): void
355
    {
356
        /** @var mixed $middleware */
357 90
        foreach ($middlewares as $middleware) {
358 1
            if (is_string($middleware)) {
359
                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