Test Failed
Pull Request — master (#173)
by Rustam
12:30
created

Route::override()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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