Passed
Pull Request — master (#246)
by Dmitriy
11:05
created

Route::put()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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