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

Route::post()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

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