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

Route   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 354
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 17
Bugs 5 Features 0
Metric Value
eloc 126
c 17
b 5
f 0
dl 0
loc 354
ccs 151
cts 151
cp 1
rs 8.8
wmc 45

25 Methods

Rating   Name   Duplication   Size   Complexity  
A patch() 0 3 1
A getData() 0 14 1
A put() 0 3 1
A getBuiltMiddlewares() 0 17 4
A assertMiddlewares() 0 14 5
A hosts() 0 14 4
A name() 0 5 1
A defaults() 0 5 1
A override() 0 5 1
A head() 0 3 1
A methods() 0 5 1
A action() 0 7 1
A pattern() 0 5 1
A delete() 0 3 1
A assertListOfStrings() 0 5 3
A get() 0 3 1
A disableMiddleware() 0 9 1
A post() 0 3 1
A options() 0 3 1
A __toString() 0 21 5
A middleware() 0 12 2
A host() 0 3 1
A prependMiddleware() 0 12 2
A __debugInfo() 0 13 1
A __construct() 0 24 3

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 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