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

Commit 0e29447

Browse files
authored
Merge pull request #38 from programmatordev/YAPV-50-create-eachkey-rule
Create EachKey rule
2 parents fe76321 + 224bfb5 commit 0e29447

File tree

9 files changed

+197
-3
lines changed

9 files changed

+197
-3
lines changed

docs/03-rules.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@
3030

3131
## Iterable Rules
3232

33-
- [EachValue](03x-rules-eachvalue.md)
33+
- [EachValue](03x-rules-each-value.md)
34+
- [EachKey](03x-rules-each-key.md)

docs/03x-rules-each-key.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
## EachKey
2+
3+
Validates every key of an `array` or object implementing `\Traversable` with a given set of rules.
4+
5+
```php
6+
EachKey(
7+
Validator $validator,
8+
string $message = 'Invalid key: {{ message }}'
9+
);
10+
```
11+
12+
## Basic Usage
13+
14+
```php
15+
Validator::eachKey(Validator::notBlank()->type('string'))->validate(['red' => '#f00', 'green' => '#0f0']); // true
16+
Validator::eachKey(Validator::notBlank()->type('string'))->validate(['red' => '#f00', 1 => '#0f0']); // false
17+
```
18+
19+
> **Note**
20+
> An `UnexpectedValueException` will be thrown when the input value is not an `array` or an object implementing `\Traversable`.
21+
22+
## Options
23+
24+
### `validator`
25+
26+
type: `Validator` `required`
27+
28+
Validator that will validate each key of an `array` or object implementing `\Traversable`.
29+
30+
### `message`
31+
32+
type: `string` default: `Invalid key: {{ message }}`
33+
34+
Message that will be shown if at least one input value key is invalid according to the given `validator`.
35+
36+
```php
37+
Validator::eachKey(Validator::notBlank())->assert(['red' => '#f00', 1 => '#0f0'], 'Test');
38+
// Throws: Invalid key: The "Test" key should be of type "string", "1" given.
39+
```
40+
41+
The following parameters are available:
42+
43+
| Parameter | Description |
44+
|-----------------|--------------------------------------------------|
45+
| `{{ value }}` | The current invalid value |
46+
| `{{ name }}` | Name of the invalid value |
47+
| `{{ key }}` | The key of the invalid iterable element |
48+
| `{{ element }}` | The value of the invalid iterable element |
49+
| `{{ message }}` | The rule message of the invalid iterable element |

docs/03x-rules-eachvalue.md renamed to docs/03x-rules-each-value.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,5 @@ The following parameters are available:
4545
| `{{ value }}` | The current invalid value |
4646
| `{{ name }}` | Name of the invalid value |
4747
| `{{ key }}` | The key of the invalid iterable element |
48+
| `{{ element }}` | The value of the invalid iterable element |
4849
| `{{ message }}` | The rule message of the invalid iterable element |

src/ChainedValidatorInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ public function country(
2323
string $message = 'The "{{ name }}" value is not a valid "{{ code }}" country code, "{{ value }}" given.'
2424
): ChainedValidatorInterface&Validator;
2525

26+
public function eachKey(
27+
Validator $validator,
28+
string $message = 'Invalid key: {{ message }}'
29+
): ChainedValidatorInterface&Validator;
30+
2631
public function eachValue(
2732
Validator $validator,
2833
string $message = 'At key "{{ key }}": {{ message }}'

src/Exception/EachKeyException.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Exception;
4+
5+
class EachKeyException extends ValidationException {}

src/Rule/EachKey.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Rule;
4+
5+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\EachKeyException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\UnexpectedValueException;
7+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\ValidationException;
8+
use ProgrammatorDev\YetAnotherPhpValidator\Validator;
9+
10+
class EachKey extends AbstractRule implements RuleInterface
11+
{
12+
public function __construct(
13+
private readonly Validator $validator,
14+
private readonly string $message = 'Invalid key: {{ message }}'
15+
) {}
16+
17+
public function assert(mixed $value, string $name): void
18+
{
19+
if (!\is_iterable($value)) {
20+
throw new UnexpectedValueException(
21+
\sprintf('Expected value of type "array|\Traversable", "%s" given.', get_debug_type($value))
22+
);
23+
}
24+
25+
try {
26+
foreach ($value as $key => $element) {
27+
$this->validator->assert($key, $name);
28+
}
29+
}
30+
catch (ValidationException $exception) {
31+
throw new EachKeyException(
32+
message: $this->message,
33+
parameters: [
34+
'value' => $value,
35+
'name' => $name,
36+
'key' => $key,
37+
'element' => $element,
38+
// Replaces string "value" with string "key" to get a more intuitive error message
39+
'message' => \preg_replace(
40+
\sprintf('/"(%s)" value/', $name),
41+
'"$1" key',
42+
$exception->getMessage()
43+
)
44+
]
45+
);
46+
}
47+
}
48+
}

src/Rule/EachValue.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public function assert(mixed $value, string $name): void
2323
}
2424

2525
try {
26-
foreach ($value as $key => $input) {
27-
$this->validator->assert($input, $name);
26+
foreach ($value as $key => $element) {
27+
$this->validator->assert($element, $name);
2828
}
2929
}
3030
catch (ValidationException $exception) {
@@ -34,6 +34,7 @@ public function assert(mixed $value, string $name): void
3434
'value' => $value,
3535
'name' => $name,
3636
'key' => $key,
37+
'element' => $element,
3738
'message' => $exception->getMessage()
3839
]
3940
);

src/StaticValidatorInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ public static function country(
2222
string $message = 'The "{{ name }}" value is not a valid "{{ code }}" country code, "{{ value }}" given.'
2323
): ChainedValidatorInterface&Validator;
2424

25+
public static function eachKey(
26+
Validator $validator,
27+
string $message = 'Invalid key: {{ message }}'
28+
): ChainedValidatorInterface&Validator;
29+
2530
public static function eachValue(
2631
Validator $validator,
2732
string $message = 'At key "{{ key }}": {{ message }}'

tests/EachKeyTest.php

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
namespace ProgrammatorDev\YetAnotherPhpValidator\Test;
4+
5+
use ProgrammatorDev\YetAnotherPhpValidator\Exception\EachKeyException;
6+
use ProgrammatorDev\YetAnotherPhpValidator\Rule\EachKey;
7+
use ProgrammatorDev\YetAnotherPhpValidator\Rule\GreaterThan;
8+
use ProgrammatorDev\YetAnotherPhpValidator\Rule\NotBlank;
9+
use ProgrammatorDev\YetAnotherPhpValidator\Rule\Type;
10+
use ProgrammatorDev\YetAnotherPhpValidator\Test\Util\TestRuleFailureConditionTrait;
11+
use ProgrammatorDev\YetAnotherPhpValidator\Test\Util\TestRuleMessageOptionTrait;
12+
use ProgrammatorDev\YetAnotherPhpValidator\Test\Util\TestRuleSuccessConditionTrait;
13+
use ProgrammatorDev\YetAnotherPhpValidator\Test\Util\TestRuleUnexpectedValueTrait;
14+
use ProgrammatorDev\YetAnotherPhpValidator\Validator;
15+
16+
class EachKeyTest extends AbstractTest
17+
{
18+
use TestRuleUnexpectedValueTrait;
19+
use TestRuleFailureConditionTrait;
20+
use TestRuleSuccessConditionTrait;
21+
use TestRuleMessageOptionTrait;
22+
23+
public static function provideRuleUnexpectedValueData(): \Generator
24+
{
25+
yield 'invalid value type' => [
26+
new EachKey(new Validator(new Type('string'))),
27+
'invalid',
28+
'/Expected value of type "(.*)", "(.*)" given./'
29+
];
30+
yield 'unexpected value propagation' => [
31+
new EachKey(new Validator(new GreaterThan(10))),
32+
['key1' => 1],
33+
'/Cannot compare a type "(.*)" with a type "(.*)"./'
34+
];
35+
}
36+
37+
public static function provideRuleFailureConditionData(): \Generator
38+
{
39+
$exception = EachKeyException::class;
40+
$message = '/Invalid key: The "(.*)" key should be of type "(.*)", "(.*)" given./';
41+
42+
yield 'invalid array element' => [
43+
new EachKey(new Validator(new Type('string'))),
44+
['key1' => 1, 'key2' => 2, 1 => 3],
45+
$exception,
46+
$message
47+
];
48+
yield 'invalid traversable element' => [
49+
new EachKey(new Validator(new Type('string'))),
50+
new \ArrayIterator(['key1' => 1, 'key2' => 2, 1 => 3]),
51+
$exception,
52+
$message
53+
];
54+
}
55+
56+
public static function provideRuleSuccessConditionData(): \Generator
57+
{
58+
yield 'array element' => [
59+
new EachKey(new Validator(new Type('string'))),
60+
['key1' => 1, 'key2' => 2, 'key3' => 3]
61+
];
62+
yield 'traversable element' => [
63+
new EachKey(new Validator(new Type('string'))),
64+
new \ArrayIterator(['key1' => 1, 'key2' => 2, 'key3' => 3])
65+
];
66+
}
67+
68+
public static function provideRuleMessageOptionData(): \Generator
69+
{
70+
yield 'message' => [
71+
new EachKey(
72+
validator: new Validator(new Type('string')),
73+
message: 'The "{{ name }}" key "{{ key }}" is invalid.'
74+
),
75+
['key1' => 1, 'key2' => 2, 1 => 3],
76+
'The "test" key "1" is invalid.'
77+
];
78+
}
79+
}

0 commit comments

Comments
 (0)