Passed
Pull Request — master (#196)
by Alexander
05:57 queued 03:03
created

Route::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 7
nc 1
nop 8
dl 0
loc 17
ccs 8
cts 8
cp 1
crap 1
rs 10
c 1
b 0
f 0

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 85
    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 85
        $this->assertListOfStrings($methods, 'methods');
61 85
        $this->assertMiddlewares($middlewares);
62 85
        $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 85
    private function assertListOfStrings(array $items, string $argument): void
335
    {
336 85
        foreach ($items as $item) {
337 85
            if (!is_string($item)) {
338
                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 85
    private function assertMiddlewares(array $middlewares): void
347
    {
348
        /** @var mixed $middleware */
349 85
        foreach ($middlewares as $middleware) {
350
            if (is_string($middleware)) {
351
                continue;
352
            }
353
354
            if (is_callable($middleware) || is_array($middleware)) {
355
                continue;
356
            }
357
358
            throw new \InvalidArgumentException(
359
                'Invalid middlewares provided, list of string or array or callable expected.'
360
            );
361
        }
362
    }
363
}
364