Passed
Pull Request — master (#173)
by Rustam
02:30
created

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