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

Commit 120ee88

Browse files
authored
Merge pull request #60 from programmatordev/YAPV-34-create-collection-rule
Create Collection rule
2 parents 1c2acad + e6bbfaa commit 120ee88

File tree

10 files changed

+376
-30
lines changed

10 files changed

+376
-30
lines changed

docs/03-rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,6 @@
4141

4242
## Iterable Rules
4343

44+
- [Collection](03-rules_collection.md)
4445
- [EachValue](03-rules_each-value.md)
4546
- [EachKey](03-rules_each-key.md)

docs/03-rules_collection.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Collection
2+
3+
Validates each key of an `array`, or object implementing `\Traversable`, with a set of validation constraints.
4+
5+
```php
6+
/** @var array<mixed, Validator> $fields */
7+
Collection(
8+
array $fields,
9+
bool $allowExtraFields = false,
10+
?string $message = null,
11+
?string $extraFieldsMessage = null,
12+
?string $missingFieldsMessage = null
13+
);
14+
```
15+
16+
## Basic Usage
17+
18+
```php
19+
Validator::collection(fields: [
20+
'name' => Validator::notBlank(),
21+
'age' => Validator::type('int')->greaterThanOrEqual(18)
22+
])->validate([
23+
'name' => 'Name',
24+
'age' => 25
25+
]); // true
26+
27+
Validator::collection(fields: [
28+
'name' => Validator::notBlank(),
29+
'age' => Validator::type('int')->greaterThanOrEqual(18)
30+
])->validate([
31+
'name' => '',
32+
'age' => 25
33+
]); // false ("name" is blank)
34+
35+
// by default, unknown keys are not allowed
36+
Validator::collection(fields: [
37+
'name' => Validator::notBlank(),
38+
'age' => Validator::type('int')->greaterThanOrEqual(18)
39+
])->validate([
40+
'name' => 'Name',
41+
'age' => 25,
42+
'email' => 'mail@example.com'
43+
]); // false ("email" field is not allowed)
44+
45+
// to allow extra fields, set option to true
46+
Validator::collection(
47+
fields: [
48+
'name' => Validator::notBlank(),
49+
'age' => Validator::type('int')->greaterThanOrEqual(18)
50+
],
51+
allowExtraFields: true
52+
)->validate([
53+
'name' => 'Name',
54+
'age' => 25,
55+
'email' => 'mail@example.com'
56+
]); // true
57+
```
58+
59+
> [!NOTE]
60+
> An `UnexpectedValueException` will be thrown when a value in the `fields` associative array is not an instance of `Validator`.
61+
62+
> [!NOTE]
63+
> An `UnexpectedValueException` will be thrown when the input value is not an `array` or an object implementing `\Traversable`.
64+
65+
## Options
66+
67+
### `fields`
68+
69+
type: `array<mixed, Validator>` `required`
70+
71+
Associative array with a set of validation constraints for each key.
72+
73+
### `allowExtraFields`
74+
75+
type: `bool` default: `false`
76+
77+
By default, it is not allowed to have fields (array keys) that are not defined in the `fields` option.
78+
If set to `true`, it will be allowed (but not validated).
79+
80+
### `message`
81+
82+
type: `?string` default: `{{ message }}`
83+
84+
Message that will be shown when one of the fields is invalid.
85+
86+
The following parameters are available:
87+
88+
| Parameter | Description |
89+
|-----------------|---------------------------------------|
90+
| `{{ name }}` | Name of the invalid value |
91+
| `{{ field }}` | Name of the invalid field (array key) |
92+
| `{{ message }}` | The rule message of the invalid field |
93+
94+
### `extraFieldsMessage`
95+
96+
type: `?string` default: `The {{ field }} field is not allowed.`
97+
98+
Message that will be shown when the input value has a field that is not defined in the `fields` option
99+
and `allowExtraFields` is set to `false`.
100+
101+
The following parameters are available:
102+
103+
| Parameter | Description |
104+
|-----------------|---------------------------------------|
105+
| `{{ name }}` | Name of the invalid value |
106+
| `{{ field }}` | Name of the invalid field (array key) |
107+
108+
### `missingFieldsMessage`
109+
110+
type: `?string` default: `The {{ field }} field is missing.`
111+
112+
Message that will be shown when the input value *does not* have a field that is defined in the `fields` option.
113+
114+
The following parameters are available:
115+
116+
| Parameter | Description |
117+
|-----------------|---------------------------------------|
118+
| `{{ name }}` | Name of the invalid value |
119+
| `{{ field }}` | Name of the invalid field (array key) |
120+
121+
## Changelog
122+
123+
- `1.0.0` Created

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: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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_iterable($value)) {
44+
throw new UnexpectedTypeException('array|\Traversable', 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+
'name' => $name,
53+
'field' => $field
54+
]
55+
);
56+
}
57+
58+
try {
59+
$validator->assert($value[$field], \sprintf('"%s"', $field));
60+
}
61+
catch (ValidationException $exception) {
62+
throw new CollectionException(
63+
message: $this->message,
64+
parameters: [
65+
'name' => $name,
66+
'field' => $field,
67+
'message' => $exception->getMessage()
68+
]
69+
);
70+
}
71+
}
72+
73+
if (!$this->allowExtraFields) {
74+
foreach ($value as $field => $fieldValue) {
75+
if (!isset($this->fields[$field])) {
76+
throw new CollectionException(
77+
message: $this->extraFieldsMessage,
78+
parameters: [
79+
'name' => $name,
80+
'field' => $field
81+
]
82+
);
83+
}
84+
}
85+
}
86+
}
87+
}

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,

0 commit comments

Comments
 (0)