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