Passed
Pull Request — master (#194)
by Dmitriy
02:30
created

Route   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Test Coverage

Coverage 94.52%

Importance

Changes 17
Bugs 4 Features 0
Metric Value
eloc 115
c 17
b 4
f 0
dl 0
loc 316
ccs 138
cts 146
cp 0.9452
rs 9.2
wmc 40

26 Methods

Rating   Name   Duplication   Size   Complexity  
A patch() 0 3 1
A getData() 0 13 1
A put() 0 3 1
A hosts() 0 7 1
A name() 0 5 1
A processDefaults() 0 3 1
A defaults() 0 5 1
A override() 0 5 1
A processMethods() 0 17 3
A head() 0 3 1
A methods() 0 5 1
A action() 0 7 1
A __construct() 0 13 1
A processHosts() 0 11 4
A pattern() 0 5 1
A delete() 0 3 1
A get() 0 3 1
A disableMiddleware() 0 9 1
A post() 0 3 1
A __toString() 0 21 5
A options() 0 3 1
A middleware() 0 12 2
A host() 0 3 1
A prependMiddleware() 0 12 2
A getMiddlewares() 0 15 4
A __debugInfo() 0 13 1

How to fix   Complexity   

Complex Class

Complex classes like Route often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Route, and based on these observations, apply Extract Interface, too.

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