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