Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit a6aeb6f

Browse files
committed
feat: added Collection rule
1 parent 1c2acad commit a6aeb6f

File tree

8 files changed

+245
-30
lines changed

8 files changed

+245
-30
lines changed

src/ChainedValidatorInterface.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ public function choice(
1818
?string $maxMessage = null
1919
): ChainedValidatorInterface&Validator;
2020

21+
public function collection(
22+
array $fields,
23+
bool $allowExtraFields = false,
24+
?string $message = null,
25+
?string $extraFieldsMessage = null,
26+
?string $missingFieldsMessage = null
27+
): ChainedValidatorInterface&Validator;
28+
2129
public function count(
2230
?int $min = null,
2331
?int $max = null,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\Validator\Exception;
4+
5+
class CollectionException extends ValidationException {}

src/Rule/Collection.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\Validator\Rule;
4+
5+
use ProgrammatorDev\Validator\Exception\CollectionException;
6+
use ProgrammatorDev\Validator\Exception\UnexpectedTypeException;
7+
use ProgrammatorDev\Validator\Exception\UnexpectedValueException;
8+
use ProgrammatorDev\Validator\Exception\ValidationException;
9+
use ProgrammatorDev\Validator\Validator;
10+
11+
class Collection extends AbstractRule implements RuleInterface
12+
{
13+
private string $message = '{{ message }}';
14+
private string $extraFieldsMessage = 'The {{ field }} field is not allowed.';
15+
private string $missingFieldsMessage = 'The {{ field }} field is missing.';
16+
17+
/** @param array<mixed, Validator> $fields */
18+
public function __construct(
19+
private readonly array $fields,
20+
private readonly bool $allowExtraFields = false,
21+
?string $message = null,
22+
?string $extraFieldsMessage = null,
23+
?string $missingFieldsMessage = null
24+
)
25+
{
26+
$this->message = $message ?? $this->message;
27+
$this->extraFieldsMessage = $extraFieldsMessage ?? $this->extraFieldsMessage;
28+
$this->missingFieldsMessage = $missingFieldsMessage ?? $this->missingFieldsMessage;
29+
}
30+
31+
public function assert(mixed $value, ?string $name = null): void
32+
{
33+
try {
34+
Validator::eachValue(
35+
validator: Validator::type(Validator::class),
36+
message: 'At field {{ key }}: {{ message }}'
37+
)->assert($this->fields);
38+
}
39+
catch (ValidationException $exception) {
40+
throw new UnexpectedValueException($exception->getMessage());
41+
}
42+
43+
if (!\is_array($value)) {
44+
throw new UnexpectedTypeException('array', get_debug_type($value));
45+
}
46+
47+
foreach ($this->fields as $field => $validator) {
48+
if (!isset($value[$field])) {
49+
throw new CollectionException(
50+
message: $this->missingFieldsMessage,
51+
parameters: [
52+
'field' => $field
53+
]
54+
);
55+
}
56+
57+
try {
58+
$validator->assert($value[$field], \sprintf('"%s"', $field));
59+
}
60+
catch (ValidationException $exception) {
61+
throw new CollectionException(
62+
message: $this->message,
63+
parameters: [
64+
'field' => $field,
65+
'message' => $exception->getMessage()
66+
]
67+
);
68+
}
69+
}
70+
71+
if (!$this->allowExtraFields) {
72+
foreach ($value as $field => $fieldValue) {
73+
if (!isset($this->fields[$field])) {
74+
throw new CollectionException(
75+
message: $this->extraFieldsMessage,
76+
parameters: [
77+
'field' => $field
78+
]
79+
);
80+
}
81+
}
82+
}
83+
}
84+
}

src/Rule/EachKey.php

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,23 @@ public function assert(mixed $value, ?string $name = null): void
2525
throw new UnexpectedTypeException('array|\Traversable', get_debug_type($value));
2626
}
2727

28-
try {
29-
foreach ($value as $key => $element) {
28+
foreach ($value as $key => $element) {
29+
try {
3030
$this->validator->assert($key, $name);
3131
}
32-
}
33-
catch (ValidationException $exception) {
34-
throw new EachKeyException(
35-
message: $this->message,
36-
parameters: [
37-
'value' => $value,
38-
'name' => $name,
39-
'key' => $key,
40-
'element' => $element,
41-
// Replaces string "value" with string "key value" to get a more intuitive error message
42-
'message' => \str_replace(' value ', ' key value ', $exception->getMessage())
43-
]
44-
);
32+
catch (ValidationException $exception) {
33+
throw new EachKeyException(
34+
message: $this->message,
35+
parameters: [
36+
'value' => $value,
37+
'name' => $name,
38+
'key' => $key,
39+
'element' => $element,
40+
// Replaces string "value" with string "key value" to get a more intuitive error message
41+
'message' => \str_replace(' value ', ' key value ', $exception->getMessage())
42+
]
43+
);
44+
}
4545
}
4646
}
4747
}

src/Rule/EachValue.php

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,22 @@ public function assert(mixed $value, ?string $name = null): void
2525
throw new UnexpectedTypeException('array|\Traversable', get_debug_type($value));
2626
}
2727

28-
try {
29-
foreach ($value as $key => $element) {
28+
foreach ($value as $key => $element) {
29+
try {
3030
$this->validator->assert($element, $name);
3131
}
32-
}
33-
catch (ValidationException $exception) {
34-
throw new EachValueException(
35-
message: $this->message,
36-
parameters: [
37-
'value' => $value,
38-
'name' => $name,
39-
'key' => $key,
40-
'element' => $element,
41-
'message' => $exception->getMessage()
42-
]
43-
);
32+
catch (ValidationException $exception) {
33+
throw new EachValueException(
34+
message: $this->message,
35+
parameters: [
36+
'value' => $value,
37+
'name' => $name,
38+
'key' => $key,
39+
'element' => $element,
40+
'message' => $exception->getMessage()
41+
]
42+
);
43+
}
4444
}
4545
}
4646
}

src/Rule/Type.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ public function assert(mixed $value, ?string $name = null): void
9393
}
9494

9595
if (!isset(self::TYPE_FUNCTIONS[$constraint]) && !\class_exists($constraint) && !\interface_exists($constraint)) {
96-
throw new UnexpectedOptionException('constraint type', \array_keys(self::TYPE_FUNCTIONS), $constraint);
96+
throw new UnexpectedOptionException('type', \array_keys(self::TYPE_FUNCTIONS), $constraint);
9797
}
9898
}
9999

src/StaticValidatorInterface.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ public static function choice(
1717
?string $maxMessage = null
1818
): ChainedValidatorInterface&Validator;
1919

20+
public static function collection(
21+
array $fields,
22+
bool $allowExtraFields = false,
23+
?string $message = null,
24+
?string $extraFieldsMessage = null,
25+
?string $missingFieldsMessage = null
26+
): ChainedValidatorInterface&Validator;
27+
2028
public static function count(
2129
?int $min = null,
2230
?int $max = null,

tests/CollectionTest.php

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\Validator\Test;
4+
5+
use ProgrammatorDev\Validator\Exception\CollectionException;
6+
use ProgrammatorDev\Validator\Rule\Collection;
7+
use ProgrammatorDev\Validator\Test\Util\TestRuleFailureConditionTrait;
8+
use ProgrammatorDev\Validator\Test\Util\TestRuleMessageOptionTrait;
9+
use ProgrammatorDev\Validator\Test\Util\TestRuleSuccessConditionTrait;
10+
use ProgrammatorDev\Validator\Test\Util\TestRuleUnexpectedValueTrait;
11+
use ProgrammatorDev\Validator\Validator;
12+
13+
class CollectionTest extends AbstractTest
14+
{
15+
use TestRuleUnexpectedValueTrait;
16+
use TestRuleFailureConditionTrait;
17+
use TestRuleSuccessConditionTrait;
18+
use TestRuleMessageOptionTrait;
19+
20+
public static function provideRuleUnexpectedValueData(): \Generator
21+
{
22+
$unexpectedFieldValueMessage = '/At field (.*)\: (.*)\./';
23+
$unexpectedTypeMessage = '/Expected value of type "array", "(.*)" given\./';
24+
25+
yield 'invalid field value' => [
26+
new Collection(fields: ['field' => 'invalid']),
27+
['field' => 'value'],
28+
$unexpectedFieldValueMessage
29+
];
30+
yield 'invalid value type' => [
31+
new Collection(fields: ['field' => Validator::notBlank()]),
32+
'invalid',
33+
$unexpectedTypeMessage
34+
];
35+
}
36+
37+
public static function provideRuleFailureConditionData(): \Generator
38+
{
39+
$exception = CollectionException::class;
40+
$extraFieldsMessage = '/The (.*) field is not allowed\./';
41+
$missingFieldsMessage = '/The (.*) field is missing\./';
42+
43+
yield 'invalid field' => [
44+
new Collection(fields: ['field' => Validator::notBlank()]),
45+
['field' => ''],
46+
$exception,
47+
'/The "(.*)" value should not be blank, "" given\./'
48+
];
49+
yield 'extra fields' => [
50+
new Collection(fields: ['field' => Validator::notBlank()]),
51+
['field' => 'value', 'extrafield' => 'extravalue'],
52+
$exception,
53+
$extraFieldsMessage
54+
];
55+
yield 'missing fields' => [
56+
new Collection(
57+
fields: [
58+
'field1' => Validator::notBlank(),
59+
'field2' => Validator::notBlank()
60+
]
61+
),
62+
['field1' => 'value1'],
63+
$exception,
64+
$missingFieldsMessage
65+
];
66+
}
67+
68+
public static function provideRuleSuccessConditionData(): \Generator
69+
{
70+
yield 'field' => [
71+
new Collection(fields: ['field' => Validator::notBlank()]),
72+
['field' => 'value'],
73+
];
74+
yield 'extra fields' => [
75+
new Collection(
76+
fields: ['field' => Validator::notBlank()],
77+
allowExtraFields: true
78+
),
79+
['field' => 'value', 'extrafield' => 'extravalue']
80+
];
81+
}
82+
83+
public static function provideRuleMessageOptionData(): \Generator
84+
{
85+
yield 'message' => [
86+
new Collection(
87+
fields: ['field' => Validator::notBlank()],
88+
message: 'There was an error: {{ message }}'
89+
),
90+
['field' => ''],
91+
'There was an error: The "field" value should not be blank, "" given.'
92+
];
93+
yield 'extra fields message' => [
94+
new Collection(
95+
fields: ['field' => Validator::notBlank()],
96+
extraFieldsMessage: 'The {{ field }} was not expected.'
97+
),
98+
['field' => 'value', 'extrafield' => 'extravalue'],
99+
'The "extrafield" was not expected.'
100+
];
101+
yield 'missing fields message' => [
102+
new Collection(
103+
fields: ['field' => Validator::notBlank()],
104+
missingFieldsMessage: 'Missing field: {{ field }}.'
105+
),
106+
[],
107+
'Missing field: "field".'
108+
];
109+
}
110+
}

0 commit comments

Comments
 (0)