Passed
Pull Request — master (#194)
by Dmitriy
11:40 queued 09:09
created

Route::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
c 2
b 0
f 0
nc 1
nop 8
dl 0
loc 13
ccs 4
cts 4
cp 1
crap 1
rs 10

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