Passed
Pull Request — master (#194)
by Dmitriy
12:10
created

Route   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 16
Bugs 4 Features 0
Metric Value
eloc 96
c 16
b 4
f 0
dl 0
loc 288
ccs 123
cts 123
cp 1
rs 9.68
wmc 34

23 Methods

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