Skip to content

Commit c87ca7b

Browse files
committed
Support in param
1 parent 2768c4e commit c87ca7b

6 files changed

Lines changed: 176 additions & 11 deletions

File tree

src/Annotation/Param.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ class Param
1111
public function __construct(
1212
public string|array $rules = '',
1313
public array $messages = [],
14-
public string $attribute = ''
14+
public string $attribute = '',
15+
public string|array|null $in = null
1516
) {
1617
}
1718
}

src/Annotation/Validate.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ public function __construct(
1313
public array $messages = [],
1414
public array $attributes = [],
1515
public ?string $validator = null,
16-
public ?string $scene = null
16+
public ?string $scene = null,
17+
public string|array|null $in = null
1718
) {
1819
}
1920
}

src/Middleware.php

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ public function process(Request $request, callable $handler)
2626
return $handler($request);
2727
}
2828

29-
$data = $this->getRequestData($request);
29+
$defaultData = $this->getRequestData($request);
3030

31-
$this->handleMethodValidation($metadata['methods'], $data);
32-
$this->handleParamValidation($metadata['params'], $data);
31+
$this->handleMethodValidation($request, $metadata['methods'], $defaultData);
32+
$this->handleParamValidation($request, $metadata['params'], $defaultData);
3333

3434
return $handler($request);
3535
}
@@ -46,18 +46,19 @@ private function resolveMetadata(Request $request): ?array
4646
return $this->getCallableMetadata($request);
4747
}
4848

49-
private function handleMethodValidation(array $methods, array $data): void
49+
private function handleMethodValidation(Request $request, array $methods, array $defaultData): void
5050
{
5151
if ($methods === []) {
5252
return;
5353
}
5454

5555
foreach ($methods as $config) {
56+
$data = $this->resolveRequestData($request, $config->in, $defaultData);
5657
$this->validateMethod($config, $data);
5758
}
5859
}
5960

60-
private function handleParamValidation(array $params, array $data): void
61+
private function handleParamValidation(Request $request, array $params, array $defaultData): void
6162
{
6263
if ($params === []) {
6364
return;
@@ -73,7 +74,8 @@ private function handleParamValidation(array $params, array $data): void
7374
/** @var \Webman\Validation\Annotation\Param $config */
7475
$config = $item['config'];
7576

76-
$value = $data[$name] ?? null;
77+
$dataForParam = $this->resolveRequestData($request, $config->in, $defaultData);
78+
$value = $dataForParam[$name] ?? null;
7779
if ($value === null && $item['hasDefault']) {
7880
$value = $item['default'];
7981
}
@@ -135,6 +137,40 @@ private function getRequestData(Request $request): array
135137
return array_merge($request->all() ?: [], $routeParams);
136138
}
137139

140+
private function resolveRequestData(Request $request, string|array|null $in, array $defaultData): array
141+
{
142+
if ($in === null || $in === []) {
143+
return $defaultData;
144+
}
145+
146+
$parts = is_array($in) ? $in : [$in];
147+
$data = [];
148+
foreach ($parts as $part) {
149+
$data = array_merge($data, $this->getRequestPartData($request, $part));
150+
}
151+
return $data;
152+
}
153+
154+
private function getRequestPartData(Request $request, mixed $part): array
155+
{
156+
if (!is_string($part) || $part === '') {
157+
throw new InvalidArgumentException('Validate/Param in must be a non-empty string or string array.');
158+
}
159+
160+
return match ($part) {
161+
'query' => $request->get() ?: [],
162+
'body' => $request->post() ?: [],
163+
'path' => $this->getPathParams($request),
164+
default => throw new InvalidArgumentException("Unsupported in value: {$part}. Only query|body|path are supported."),
165+
};
166+
}
167+
168+
private function getPathParams(Request $request): array
169+
{
170+
$routeParams = $request->route ? $request->route->param() : [];
171+
return is_array($routeParams) ? $routeParams : [];
172+
}
173+
138174
private function getMethodMetadata(string $controller, string $action): ?array
139175
{
140176
$key = $controller . '::' . $action;
@@ -251,7 +287,8 @@ private function resolveParamConfig(ReflectionParameter $parameter, bool $inferW
251287
return new Param(
252288
rules: $completedRules,
253289
messages: $config->messages,
254-
attribute: $config->attribute
290+
attribute: $config->attribute,
291+
in: $config->in
255292
);
256293
}
257294

src/Tests/ValidateMiddlewareTest.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,91 @@ public function testMultipleMethodValidation(): void
123123
$this->assertTrue($called);
124124
}
125125

126+
public function testValidateInQueryBodyBodyOverridesQueryByOrder(): void
127+
{
128+
$request = $this->makeRequest(
129+
controller: InQueryBodyController::class,
130+
action: 'send',
131+
query: ['id' => 1],
132+
body: ['id' => 2]
133+
);
134+
135+
$called = false;
136+
(new Middleware())->process($request, function () use (&$called) {
137+
$called = true;
138+
return 'ok';
139+
});
140+
141+
$this->assertTrue($called);
142+
}
143+
144+
public function testValidateInBodyQueryQueryOverridesBodyByOrder(): void
145+
{
146+
$request = $this->makeRequest(
147+
controller: InBodyQueryController::class,
148+
action: 'send',
149+
query: ['id' => 1],
150+
body: ['id' => 2]
151+
);
152+
153+
$called = false;
154+
(new Middleware())->process($request, function () use (&$called) {
155+
$called = true;
156+
return 'ok';
157+
});
158+
159+
$this->assertTrue($called);
160+
}
161+
162+
public function testValidateInQueryPathPathOverridesQueryByOrder(): void
163+
{
164+
$request = $this->makeRequest(
165+
controller: InQueryPathController::class,
166+
action: 'send',
167+
query: ['id' => 1],
168+
routeParams: ['id' => 7]
169+
);
170+
171+
$called = false;
172+
(new Middleware())->process($request, function () use (&$called) {
173+
$called = true;
174+
return 'ok';
175+
});
176+
177+
$this->assertTrue($called);
178+
}
179+
180+
public function testParamInPathReadsFromPathOnly(): void
181+
{
182+
$request = $this->makeRequest(
183+
controller: ParamInPathController::class,
184+
action: 'send',
185+
query: ['id' => 1],
186+
routeParams: ['id' => 7]
187+
);
188+
189+
$called = false;
190+
(new Middleware())->process($request, function () use (&$called) {
191+
$called = true;
192+
return 'ok';
193+
});
194+
195+
$this->assertTrue($called);
196+
}
197+
198+
public function testValidateInUnsupportedValueThrows(): void
199+
{
200+
$request = $this->makeRequest(
201+
controller: InvalidInController::class,
202+
action: 'send',
203+
query: ['id' => 1]
204+
);
205+
206+
$this->expectException(InvalidArgumentException::class);
207+
$this->expectExceptionMessage('Unsupported in value');
208+
(new Middleware())->process($request, fn () => 'ok');
209+
}
210+
126211
public function testParamValidationUsesRouteParams(): void
127212
{
128213
$request = $this->makeRequest(
@@ -1018,6 +1103,47 @@ public function send(Request $request): void
10181103
}
10191104
}
10201105

1106+
final class InQueryBodyController
1107+
{
1108+
#[Validate(in: ['query', 'body'], rules: ['id' => 'required|in:2'])]
1109+
public function send(Request $request): void
1110+
{
1111+
}
1112+
}
1113+
1114+
final class InBodyQueryController
1115+
{
1116+
#[Validate(in: ['body', 'query'], rules: ['id' => 'required|in:1'])]
1117+
public function send(Request $request): void
1118+
{
1119+
}
1120+
}
1121+
1122+
final class InQueryPathController
1123+
{
1124+
#[Validate(in: ['query', 'path'], rules: ['id' => 'required|in:7'])]
1125+
public function send(Request $request): void
1126+
{
1127+
}
1128+
}
1129+
1130+
final class ParamInPathController
1131+
{
1132+
public function send(
1133+
#[Param(in: 'path', rules: 'required|in:7')]
1134+
int $id
1135+
): void {
1136+
}
1137+
}
1138+
1139+
final class InvalidInController
1140+
{
1141+
#[Validate(in: 'header', rules: ['id' => 'required|integer'])]
1142+
public function send(Request $request): void
1143+
{
1144+
}
1145+
}
1146+
10211147
final class ParamController
10221148
{
10231149
public function send(

src/Tests/ValidatorExceptionTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function test_exception_override_uses_custom_exception(): void
3838
['name' => null],
3939
['name' => 'required']
4040
)
41-
->exception(\RuntimeException::class)
41+
->withException(\RuntimeException::class)
4242
->validate();
4343
}
4444
}

src/Validator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public function withScene(string $scene): static
5252
return $clone;
5353
}
5454

55-
public function exception(string $exceptionClass): static
55+
public function withException(string $exceptionClass): static
5656
{
5757
if ($exceptionClass === '') {
5858
throw new InvalidArgumentException('Validation exception must be a non-empty class string.');

0 commit comments

Comments
 (0)