Passed
Pull Request — master (#222)
by Sergei
02:37
created

Route   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 303
Duplicated Lines 0 %

Test Coverage

Coverage 99.21%

Importance

Changes 17
Bugs 5 Features 0
Metric Value
eloc 103
dl 0
loc 303
ccs 125
cts 126
cp 0.9921
rs 9.76
c 17
b 5
f 0
wmc 33

23 Methods

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