Passed
Pull Request — master (#194)
by Alexander
04:50 queued 02:22
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 73
    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 73
        $this->methods = $this->processMethods($this->methods);
47 73
        $this->hosts = $this->processHosts($this->hosts);
48 73
        $this->defaults = $this->processDefaults($this->defaults);
49
    }
50
51 60
    public static function get(string $pattern): self
52
    {
53 60
        return self::methods([Method::GET], $pattern);
54
    }
55
56 9
    public static function post(string $pattern): self
57
    {
58 9
        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 7
    public static function options(string $pattern): self
82
    {
83 7
        return self::methods([Method::OPTIONS], $pattern);
84
    }
85
86
    /**
87
     * @param string[] $methods
88
     */
89 72
    public static function methods(
90
        array $methods,
91
        string $pattern,
92
    ): self {
93 72
        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 21
    public function pattern(string $pattern): self
104
    {
105 21
        $new = clone $this;
106 21
        $new->pattern = $pattern;
107 21
        return $new;
108
    }
109
110 8
    public function host(string $host): self
111
    {
112 8
        return $this->hosts($host);
113
    }
114
115 12
    public function hosts(string ...$hosts): self
116
    {
117 12
        $route = clone $this;
118
119 12
        $route->hosts = $this->processHosts($hosts);
120
121 12
        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 18
    public function middleware(array|callable|string ...$middlewareDefinition): self
151
    {
152 18
        if ($this->actionAdded) {
153 1
            throw new RuntimeException('middleware() can not be used after action().');
154
        }
155 17
        $route = clone $this;
156 17
        array_push(
157 17
            $route->middlewares,
158 17
            ...array_values($middlewareDefinition)
159 17
        );
160 17
        $route->builtMiddlewares = [];
161 17
        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 18
    public function prependMiddleware(array|callable|string ...$middlewareDefinition): self
169
    {
170 18
        if (!$this->actionAdded) {
171 1
            throw new RuntimeException('prependMiddleware() can not be used before action().');
172
        }
173 17
        $route = clone $this;
174 17
        array_unshift(
175 17
            $route->middlewares,
176 17
            ...array_values($middlewareDefinition)
177 17
        );
178 17
        $route->builtMiddlewares = [];
179 17
        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 20
    public function action(array|callable|string $middlewareDefinition): self
186
    {
187 20
        $route = clone $this;
188 20
        $route->middlewares[] = $middlewareDefinition;
189 20
        $route->builtMiddlewares = [];
190 20
        $route->actionAdded = true;
191 20
        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 46
    public function getData(string $key): mixed
229
    {
230 46
        return match ($key) {
231 46
            'name' => $this->name ??
232 27
                (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern),
233 46
            'pattern' => $this->pattern,
234 46
            'host' => $this->hosts[0] ?? null,
235 46
            'hosts' => $this->hosts,
236 46
            'methods' => $this->methods,
237 46
            'defaults' => $this->defaults,
238 46
            'override' => $this->override,
239 46
            'hasMiddlewares' => $this->middlewares !== [],
240 46
            default => throw new InvalidArgumentException('Unknown data key: ' . $key),
241 46
        };
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 16
    public function getMiddlewares(): array
284
    {
285 16
        if ($this->builtMiddlewares !== []) {
286
            return $this->builtMiddlewares;
287
        }
288 16
        $result = $this->middlewares;
289
        /** @var mixed $definition */
290 16
        foreach ($result as $index => $definition) {
291 15
            if (in_array($definition, $this->disabledMiddlewares, true)) {
292 1
                unset($result[$index]);
293
            }
294
        }
295 16
        $this->builtMiddlewares = $result;
296
297 16
        return $result;
298
    }
299
300 73
    private function processDefaults(array $defaults): array
301
    {
302 73
        return array_map('\strval', $defaults);
303
    }
304
305 73
    private function processHosts(array $hosts): array
306
    {
307 73
        $result = [];
308 73
        foreach ($hosts as $host) {
309 13
            $host = rtrim($host, '/');
310
311 13
            if ($host !== '' && !in_array($host, $result, true)) {
312 12
                $result[] = $host;
313
            }
314
        }
315 73
        return $result;
316
    }
317
318 73
    private function processMethods(array $methods): array
319
    {
320 73
        $result = [];
321 73
        foreach ($methods as $method) {
322 73
            $method = strtoupper($method);
323 73
            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 73
            $result[] = $method;
333
        }
334 73
        return $result;
335
    }
336
}
337