Passed
Pull Request — master (#194)
by Alexander
02:33
created

Route::__construct()   A

Complexity

Conditions 1
Paths 1

Size

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