Passed
Pull Request — master (#196)
by Rustam
13:36
created

Group::withCors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router;
6
7
use Attribute;
8
use InvalidArgumentException;
9
use RuntimeException;
10
11
use function in_array;
12
13
#[Attribute(Attribute::TARGET_CLASS)]
14
final class Group
15
{
16
    /**
17
     * @var Group[]|Route[]
18
     */
19
    private array $routes = [];
20
    private bool $routesAdded = false;
21
    private bool $middlewareAdded = false;
22
    private array $builtMiddlewareDefinitions = [];
23
    /**
24
     * @var array|callable|string|null Middleware definition for CORS requests.
25
     */
26
    private $corsMiddleware;
27
    /**
28
     * @var string[]
29
     */
30
    private array $hosts = [];
31
    /**
32
     * @var array[]|callable[]|string[]
33
     */
34
    private array $middlewareDefinitions = [];
35
36
    /**
37
     * @param array $disabledMiddlewareDefinitions Excludes middleware from being invoked when action is handled.
38
     * It is useful to avoid invoking one of the parent group middleware for
39
     * a certain route.
40
     */
41 46
    public function __construct(
42
        private ?string $prefix = null,
43
        array $middlewareDefinitions = [],
44
        array $hosts = [],
45
        private ?string $namePrefix = null,
46
        private array $disabledMiddlewareDefinitions = [],
47
        array|callable|string|null $corsMiddleware = null
48
    ) {
49 46
        $this->assertMiddlewares($middlewareDefinitions);
50 45
        $this->assertHosts($hosts);
51 44
        $this->middlewareDefinitions = $middlewareDefinitions;
52 44
        $this->hosts = $hosts;
53 44
        $this->corsMiddleware = $corsMiddleware;
54
    }
55
56
    /**
57
     * Create a new group instance.
58
     *
59
     * @param string|null $prefix URL prefix to prepend to all routes of the group.
60
     */
61 44
    public static function create(?string $prefix = null): self
62
    {
63 44
        return new self($prefix);
64
    }
65
66 35
    public function routes(self|Route ...$routes): self
67
    {
68 35
        if ($this->middlewareAdded) {
69 1
            throw new RuntimeException('routes() can not be used after prependMiddleware().');
70
        }
71 34
        $new = clone $this;
72 34
        $new->routes = $routes;
73 34
        $new->routesAdded = true;
74
75 34
        return $new;
76
    }
77
78
    /**
79
     * Adds a middleware definition that handles CORS requests.
80
     * If set, routes for {@see Method::OPTIONS} request will be added automatically.
81
     *
82
     * @param array|callable|string|null $middlewareDefinition Middleware definition for CORS requests.
83
     */
84 8
    public function withCors(array|callable|string|null $middlewareDefinition): self
85
    {
86 8
        $group = clone $this;
87 8
        $group->corsMiddleware = $middlewareDefinition;
88
89 8
        return $group;
90
    }
91
92
    /**
93
     * Appends a handler middleware definition that should be invoked for a matched route.
94
     * First added handler will be executed first.
95
     */
96 12
    public function middleware(array|callable|string ...$middlewareDefinition): self
97
    {
98 12
        if ($this->routesAdded) {
99 1
            throw new RuntimeException('middleware() can not be used after routes().');
100
        }
101 11
        $new = clone $this;
102 11
        array_push(
103 11
            $new->middlewareDefinitions,
104 11
            ...array_values($middlewareDefinition)
105 11
        );
106 11
        $new->builtMiddlewareDefinitions = [];
107 11
        return $new;
108
    }
109
110
    /**
111
     * Prepends a handler middleware definition that should be invoked for a matched route.
112
     * First added handler will be executed last.
113
     */
114 27
    public function prependMiddleware(array|callable|string ...$middlewareDefinition): self
115
    {
116 27
        $new = clone $this;
117 27
        array_unshift(
118 27
            $new->middlewareDefinitions,
119 27
            ...array_values($middlewareDefinition)
120 27
        );
121 27
        $new->middlewareAdded = true;
122 27
        $new->builtMiddlewareDefinitions = [];
123 27
        return $new;
124
    }
125
126 4
    public function namePrefix(string $namePrefix): self
127
    {
128 4
        $new = clone $this;
129 4
        $new->namePrefix = $namePrefix;
130 4
        return $new;
131
    }
132
133 2
    public function host(string $host): self
134
    {
135 2
        return $this->hosts($host);
136
    }
137
138 5
    public function hosts(string ...$hosts): self
139
    {
140 5
        $new = clone $this;
141
142 5
        foreach ($hosts as $host) {
143 4
            $host = rtrim($host, '/');
144
145 4
            if ($host !== '' && !in_array($host, $new->hosts, true)) {
146 4
                $new->hosts[] = $host;
147
            }
148
        }
149
150 5
        return $new;
151
    }
152
153
    /**
154
     * Excludes middleware from being invoked when action is handled.
155
     * It is useful to avoid invoking one of the parent group middleware for
156
     * a certain route.
157
     */
158 3
    public function disableMiddleware(mixed ...$middlewareDefinition): self
159
    {
160 3
        $new = clone $this;
161 3
        array_push(
162 3
            $new->disabledMiddlewareDefinitions,
163 3
            ...array_values($middlewareDefinition),
164 3
        );
165 3
        $new->builtMiddlewareDefinitions = [];
166 3
        return $new;
167
    }
168
169
    /**
170
     * @psalm-template T as string
171
     *
172
     * @psalm-param T $key
173
     *
174
     * @psalm-return (
175
     *   T is ('prefix'|'namePrefix'|'host') ? string|null :
176
     *   (T is 'routes' ? Group[]|Route[] :
177
     *     (T is 'hosts' ? array<array-key, string> :
178
     *       (T is 'hasCorsMiddleware' ? bool :
179
     *         (T is 'middlewareDefinitions' ? list<array|callable|string> :
180
     *           (T is 'corsMiddleware' ? array|callable|string|null : mixed)
181
     *         )
182
     *       )
183
     *     )
184
     *   )
185
     * )
186
     */
187 32
    public function getData(string $key): mixed
188
    {
189 32
        return match ($key) {
190 32
            'prefix' => $this->prefix,
191 32
            'namePrefix' => $this->namePrefix,
192 32
            'host' => $this->hosts[0] ?? null,
193 32
            'hosts' => $this->hosts,
194 32
            'corsMiddleware' => $this->corsMiddleware,
195 32
            'routes' => $this->routes,
196 32
            'hasCorsMiddleware' => $this->corsMiddleware !== null,
197 32
            'middlewareDefinitions' => $this->getBuiltMiddlewares(),
198 32
            default => throw new InvalidArgumentException('Unknown data key: ' . $key),
199 32
        };
200
    }
201
202 22
    private function getBuiltMiddlewares(): array
203
    {
204 22
        if (!empty($this->builtMiddlewareDefinitions)) {
205 5
            return $this->builtMiddlewareDefinitions;
206
        }
207
208 22
        $builtMiddlewareDefinitions = $this->middlewareDefinitions;
209
210
        /** @var mixed $definition */
211 22
        foreach ($builtMiddlewareDefinitions as $index => $definition) {
212 12
            if (in_array($definition, $this->disabledMiddlewareDefinitions, true)) {
213 2
                unset($builtMiddlewareDefinitions[$index]);
214
            }
215
        }
216
217 22
        return $this->builtMiddlewareDefinitions = array_values($builtMiddlewareDefinitions);
218
    }
219
220
    /**
221
     * @psalm-assert array<string> $hosts
222
     */
223 45
    private function assertHosts(array $hosts): void
224
    {
225 45
        foreach ($hosts as $host) {
226 1
            if (!is_string($host)) {
227 1
                throw new \InvalidArgumentException('Invalid $hosts provided, list of string expected.');
228
            }
229
        }
230
    }
231
232
    /**
233
     * @psalm-assert array<array|callable|string> $middlewareDefinitions
234
     */
235 46
    private function assertMiddlewares(array $middlewareDefinitions): void
236
    {
237
        /** @var mixed $middlewareDefinition */
238 46
        foreach ($middlewareDefinitions as $middlewareDefinition) {
239 1
            if (is_string($middlewareDefinition) || is_callable($middlewareDefinition) || is_array($middlewareDefinition)) {
240 1
                continue;
241
            }
242
243 1
            throw new \InvalidArgumentException(
244 1
                'Invalid $middlewareDefinitions provided, list of string or array or callable expected.'
245 1
            );
246
        }
247
    }
248
}
249