Passed
Pull Request — master (#196)
by Rustam
13:36
created

Route::__construct()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 12
c 1
b 0
f 0
nc 3
nop 9
dl 0
loc 24
ccs 13
cts 13
cp 1
crap 3
rs 9.8666

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 $builtMiddlewareDefinitions = [];
24
    /**
25
     * @var array[]|callable[]|string[]
26
     */
27
    private array $middlewareDefinitions = [];
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 $disabledMiddlewareDefinitions 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 106
    public function __construct(
51
        array $methods,
52
        private string $pattern,
53
        private ?string $name = null,
54
        array|callable|string $action = null,
55
        array $middlewareDefinitions = [],
56
        array $defaults = [],
57
        array $hosts = [],
58
        private bool $override = false,
59
        private array $disabledMiddlewareDefinitions = [],
60
    ) {
61 106
        if (empty($methods)) {
62 1
            throw new InvalidArgumentException('$methods cannot be empty.');
63
        }
64 105
        $this->assertListOfStrings($methods, 'methods');
65 105
        $this->assertMiddlewares($middlewareDefinitions);
66 104
        $this->assertListOfStrings($hosts, 'hosts');
67 103
        $this->methods = $methods;
68 103
        $this->middlewareDefinitions = $middlewareDefinitions;
69 103
        $this->hosts = $hosts;
70 103
        $this->defaults = array_map('\strval', $defaults);
71 103
        if (!empty($action)) {
72 1
            $this->middlewareDefinitions[] = $action;
73 1
            $this->actionAdded = true;
74
        }
75
    }
76
77 74
    public static function get(string $pattern): self
78
    {
79 74
        return self::methods([Method::GET], $pattern);
80
    }
81
82 10
    public static function post(string $pattern): self
83
    {
84 10
        return self::methods([Method::POST], $pattern);
85
    }
86
87 4
    public static function put(string $pattern): self
88
    {
89 4
        return self::methods([Method::PUT], $pattern);
90
    }
91
92 1
    public static function delete(string $pattern): self
93
    {
94 1
        return self::methods([Method::DELETE], $pattern);
95
    }
96
97 1
    public static function patch(string $pattern): self
98
    {
99 1
        return self::methods([Method::PATCH], $pattern);
100
    }
101
102 1
    public static function head(string $pattern): self
103
    {
104 1
        return self::methods([Method::HEAD], $pattern);
105
    }
106
107 9
    public static function options(string $pattern): self
108
    {
109 9
        return self::methods([Method::OPTIONS], $pattern);
110
    }
111
112
    /**
113
     * @param string[] $methods
114
     */
115 86
    public static function methods(array $methods, string $pattern): self
116
    {
117 86
        return new self(
118 86
            methods: $methods,
119 86
            pattern: $pattern
120 86
        );
121
    }
122
123 23
    public function name(string $name): self
124
    {
125 23
        $route = clone $this;
126 23
        $route->name = $name;
127 23
        return $route;
128
    }
129
130 24
    public function pattern(string $pattern): self
131
    {
132 24
        $new = clone $this;
133 24
        $new->pattern = $pattern;
134 24
        return $new;
135
    }
136
137 9
    public function host(string $host): self
138
    {
139 9
        return $this->hosts($host);
140
    }
141
142 13
    public function hosts(string ...$hosts): self
143
    {
144 13
        $route = clone $this;
145 13
        $route->hosts = [];
146
147 13
        foreach ($hosts as $host) {
148 13
            $host = rtrim($host, '/');
149
150 13
            if ($host !== '' && !in_array($host, $route->hosts, true)) {
151 12
                $route->hosts[] = $host;
152
            }
153
        }
154
155 13
        return $route;
156
    }
157
158
    /**
159
     * Marks route as override. When added it will replace existing route with the same name.
160
     */
161 4
    public function override(): self
162
    {
163 4
        $route = clone $this;
164 4
        $route->override = true;
165 4
        return $route;
166
    }
167
168
    /**
169
     * Parameter default values indexed by parameter names.
170
     *
171
     * @psalm-param array<string,null|Stringable|scalar> $defaults
172
     */
173 3
    public function defaults(array $defaults): self
174
    {
175 3
        $route = clone $this;
176 3
        $route->defaults = array_map('\strval', $defaults);
177 3
        return $route;
178
    }
179
180
    /**
181
     * Appends a handler middleware definition that should be invoked for a matched route.
182
     * First added handler will be executed first.
183
     */
184 23
    public function middleware(array|callable|string ...$middlewareDefinition): self
185
    {
186 23
        if ($this->actionAdded) {
187 1
            throw new RuntimeException('middleware() can not be used after action().');
188
        }
189 22
        $route = clone $this;
190 22
        array_push(
191 22
            $route->middlewareDefinitions,
192 22
            ...array_values($middlewareDefinition)
193 22
        );
194 22
        $route->builtMiddlewareDefinitions = [];
195 22
        return $route;
196
    }
197
198
    /**
199
     * Prepends a handler middleware definition that should be invoked for a matched route.
200
     * Last added handler will be executed first.
201
     */
202 23
    public function prependMiddleware(array|callable|string ...$middlewareDefinition): self
203
    {
204 23
        if (!$this->actionAdded) {
205 1
            throw new RuntimeException('prependMiddleware() can not be used before action().');
206
        }
207 22
        $route = clone $this;
208 22
        array_unshift(
209 22
            $route->middlewareDefinitions,
210 22
            ...array_values($middlewareDefinition)
211 22
        );
212 22
        $route->builtMiddlewareDefinitions = [];
213 22
        return $route;
214
    }
215
216
    /**
217
     * Appends action handler. It is a primary middleware definition that should be invoked last for a matched route.
218
     */
219 26
    public function action(array|callable|string $middlewareDefinition): self
220
    {
221 26
        $route = clone $this;
222 26
        $route->middlewareDefinitions[] = $middlewareDefinition;
223 26
        $route->actionAdded = true;
224 26
        $route->builtMiddlewareDefinitions = [];
225 26
        return $route;
226
    }
227
228
    /**
229
     * Excludes middleware from being invoked when action is handled.
230
     * It is useful to avoid invoking one of the parent group middleware for
231
     * a certain route.
232
     */
233 3
    public function disableMiddleware(mixed ...$middlewareDefinition): self
234
    {
235 3
        $route = clone $this;
236 3
        array_push(
237 3
            $route->disabledMiddlewareDefinitions,
238 3
            ...array_values($middlewareDefinition)
239 3
        );
240 3
        $route->builtMiddlewareDefinitions = [];
241 3
        return $route;
242
    }
243
244
    /**
245
     * @psalm-template T as string
246
     *
247
     * @psalm-param T $key
248
     *
249
     * @psalm-return (
250
     *   T is ('name'|'pattern') ? string :
251
     *       (T is 'host' ? string|null :
252
     *           (T is 'hosts' ? array<array-key, string> :
253
     *               (T is 'methods' ? array<array-key,string> :
254
     *                   (T is 'defaults' ? array<string,string> :
255
     *                       (T is ('override'|'hasMiddlewares') ? bool :
256
     *                           (T is 'builtMiddlewareDefinitions' ? array<array-key,array|callable|string> : mixed)
257
     *                       )
258
     *                   )
259
     *               )
260
     *           )
261
     *       )
262
     *    )
263
     */
264 75
    public function getData(string $key): mixed
265
    {
266 75
        return match ($key) {
267 75
            'name' => $this->name ??
268 32
                (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern),
269 75
            'pattern' => $this->pattern,
270 75
            'host' => $this->hosts[0] ?? null,
271 75
            'hosts' => $this->hosts,
272 75
            'methods' => $this->methods,
273 75
            'defaults' => $this->defaults,
274 75
            'override' => $this->override,
275 75
            'hasMiddlewares' => !empty($this->middlewareDefinitions),
276 75
            'builtMiddlewareDefinitions' => $this->getBuiltMiddlewares(),
277 75
            default => throw new InvalidArgumentException('Unknown data key: ' . $key),
278 75
        };
279
    }
280
281 5
    public function __toString(): string
282
    {
283 5
        $result = $this->name === null
284 2
            ? ''
285 3
            : '[' . $this->name . '] ';
286
287 5
        if ($this->methods !== []) {
288 5
            $result .= implode(',', $this->methods) . ' ';
289
        }
290
291 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...
292 2
            $quoted = array_map(static fn ($host) => preg_quote($host, '/'), $this->hosts);
293
294 2
            if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) {
295 2
                $result .= implode('|', $this->hosts);
296
            }
297
        }
298
299 5
        $result .= $this->pattern;
300
301 5
        return $result;
302
    }
303
304 1
    public function __debugInfo()
305
    {
306 1
        return [
307 1
            'name' => $this->name,
308 1
            'methods' => $this->methods,
309 1
            'pattern' => $this->pattern,
310 1
            'hosts' => $this->hosts,
311 1
            'defaults' => $this->defaults,
312 1
            'override' => $this->override,
313 1
            'actionAdded' => $this->actionAdded,
314 1
            'middlewareDefinitions' => $this->middlewareDefinitions,
315 1
            'builtMiddlewareDefinitions' => $this->builtMiddlewareDefinitions,
316 1
            'disabledMiddlewareDefinitions' => $this->disabledMiddlewareDefinitions,
317 1
        ];
318
    }
319
320
    /**
321
     * @return array[]|callable[]|string[]
322
     */
323 21
    private function getBuiltMiddlewares(): array
324
    {
325
        // Don't build middlewareDefinitions if we did it earlier.
326
        // This improves performance in event-loop applications.
327 21
        if (!empty($this->builtMiddlewareDefinitions)) {
328 1
            return $this->builtMiddlewareDefinitions;
329
        }
330
331 21
        $builtMiddlewareDefinitions = $this->middlewareDefinitions;
332
333 21
        foreach ($builtMiddlewareDefinitions as $index => $definition) {
334 20
            if (in_array($definition, $this->disabledMiddlewareDefinitions, true)) {
335 1
                unset($builtMiddlewareDefinitions[$index]);
336
            }
337
        }
338
339 21
        return $this->builtMiddlewareDefinitions = $builtMiddlewareDefinitions;
340
    }
341
342
    /**
343
     * @psalm-assert array<string> $items
344
     */
345 105
    private function assertListOfStrings(array $items, string $argument): void
346
    {
347 105
        foreach ($items as $item) {
348 105
            if (!is_string($item)) {
349 1
                throw new \InvalidArgumentException('Invalid $' . $argument . ' provided, list of string expected.');
350
            }
351
        }
352
    }
353
354
    /**
355
     * @psalm-assert array<array|callable|string> $middlewareDefinitions
356
     */
357 105
    private function assertMiddlewares(array $middlewareDefinitions): void
358
    {
359
        /** @var mixed $middlewareDefinition */
360 105
        foreach ($middlewareDefinitions as $middlewareDefinition) {
361 2
            if (is_string($middlewareDefinition)) {
362 1
                continue;
363
            }
364
365 1
            if (is_callable($middlewareDefinition) || is_array($middlewareDefinition)) {
366 1
                continue;
367
            }
368
369 1
            throw new \InvalidArgumentException(
370 1
                'Invalid $middlewareDefinitions provided, list of string or array or callable expected.'
371 1
            );
372
        }
373
    }
374
}
375