Skip to content

Commit

Permalink
Merge pull request #60 from programmatordev/YAPV-34-create-collection…
Browse files Browse the repository at this point in the history
…-rule

Create Collection rule
  • Loading branch information
andrepimpao authored Apr 1, 2024
2 parents 1c2acad + e6bbfaa commit 120ee88
Show file tree
Hide file tree
Showing 10 changed files with 376 additions and 30 deletions.
1 change: 1 addition & 0 deletions docs/03-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@

## Iterable Rules

- [Collection](03-rules_collection.md)
- [EachValue](03-rules_each-value.md)
- [EachKey](03-rules_each-key.md)
123 changes: 123 additions & 0 deletions docs/03-rules_collection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Collection

Validates each key of an `array`, or object implementing `\Traversable`, with a set of validation constraints.

```php
/** @var array<mixed, Validator> $fields */
Collection(
array $fields,
bool $allowExtraFields = false,
?string $message = null,
?string $extraFieldsMessage = null,
?string $missingFieldsMessage = null
);
```

## Basic Usage

```php
Validator::collection(fields: [
'name' => Validator::notBlank(),
'age' => Validator::type('int')->greaterThanOrEqual(18)
])->validate([
'name' => 'Name',
'age' => 25
]); // true

Validator::collection(fields: [
'name' => Validator::notBlank(),
'age' => Validator::type('int')->greaterThanOrEqual(18)
])->validate([
'name' => '',
'age' => 25
]); // false ("name" is blank)

// by default, unknown keys are not allowed
Validator::collection(fields: [
'name' => Validator::notBlank(),
'age' => Validator::type('int')->greaterThanOrEqual(18)
])->validate([
'name' => 'Name',
'age' => 25,
'email' => 'mail@example.com'
]); // false ("email" field is not allowed)

// to allow extra fields, set option to true
Validator::collection(
fields: [
'name' => Validator::notBlank(),
'age' => Validator::type('int')->greaterThanOrEqual(18)
],
allowExtraFields: true
)->validate([
'name' => 'Name',
'age' => 25,
'email' => 'mail@example.com'
]); // true
```

> [!NOTE]
> An `UnexpectedValueException` will be thrown when a value in the `fields` associative array is not an instance of `Validator`.
> [!NOTE]
> An `UnexpectedValueException` will be thrown when the input value is not an `array` or an object implementing `\Traversable`.
## Options

### `fields`

type: `array<mixed, Validator>` `required`

Associative array with a set of validation constraints for each key.

### `allowExtraFields`

type: `bool` default: `false`

By default, it is not allowed to have fields (array keys) that are not defined in the `fields` option.
If set to `true`, it will be allowed (but not validated).

### `message`

type: `?string` default: `{{ message }}`

Message that will be shown when one of the fields is invalid.

The following parameters are available:

| Parameter | Description |
|-----------------|---------------------------------------|
| `{{ name }}` | Name of the invalid value |
| `{{ field }}` | Name of the invalid field (array key) |
| `{{ message }}` | The rule message of the invalid field |

### `extraFieldsMessage`

type: `?string` default: `The {{ field }} field is not allowed.`

Message that will be shown when the input value has a field that is not defined in the `fields` option
and `allowExtraFields` is set to `false`.

The following parameters are available:

| Parameter | Description |
|-----------------|---------------------------------------|
| `{{ name }}` | Name of the invalid value |
| `{{ field }}` | Name of the invalid field (array key) |

### `missingFieldsMessage`

type: `?string` default: `The {{ field }} field is missing.`

Message that will be shown when the input value *does not* have a field that is defined in the `fields` option.

The following parameters are available:

| Parameter | Description |
|-----------------|---------------------------------------|
| `{{ name }}` | Name of the invalid value |
| `{{ field }}` | Name of the invalid field (array key) |

## Changelog

- `1.0.0` Created
8 changes: 8 additions & 0 deletions src/ChainedValidatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@ public function choice(
?string $maxMessage = null
): ChainedValidatorInterface&Validator;

public function collection(
array $fields,
bool $allowExtraFields = false,
?string $message = null,
?string $extraFieldsMessage = null,
?string $missingFieldsMessage = null
): ChainedValidatorInterface&Validator;

public function count(
?int $min = null,
?int $max = null,
Expand Down
5 changes: 5 additions & 0 deletions src/Exception/CollectionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace ProgrammatorDev\Validator\Exception;

class CollectionException extends ValidationException {}
87 changes: 87 additions & 0 deletions src/Rule/Collection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace ProgrammatorDev\Validator\Rule;

use ProgrammatorDev\Validator\Exception\CollectionException;
use ProgrammatorDev\Validator\Exception\UnexpectedTypeException;
use ProgrammatorDev\Validator\Exception\UnexpectedValueException;
use ProgrammatorDev\Validator\Exception\ValidationException;
use ProgrammatorDev\Validator\Validator;

class Collection extends AbstractRule implements RuleInterface
{
private string $message = '{{ message }}';
private string $extraFieldsMessage = 'The {{ field }} field is not allowed.';
private string $missingFieldsMessage = 'The {{ field }} field is missing.';

/** @param array<mixed, Validator> $fields */
public function __construct(
private readonly array $fields,
private readonly bool $allowExtraFields = false,
?string $message = null,
?string $extraFieldsMessage = null,
?string $missingFieldsMessage = null
)
{
$this->message = $message ?? $this->message;
$this->extraFieldsMessage = $extraFieldsMessage ?? $this->extraFieldsMessage;
$this->missingFieldsMessage = $missingFieldsMessage ?? $this->missingFieldsMessage;
}

public function assert(mixed $value, ?string $name = null): void
{
try {
Validator::eachValue(
validator: Validator::type(Validator::class),
message: 'At field {{ key }}: {{ message }}'
)->assert($this->fields);
}
catch (ValidationException $exception) {
throw new UnexpectedValueException($exception->getMessage());
}

if (!\is_iterable($value)) {
throw new UnexpectedTypeException('array|\Traversable', get_debug_type($value));
}

foreach ($this->fields as $field => $validator) {
if (!isset($value[$field])) {
throw new CollectionException(
message: $this->missingFieldsMessage,
parameters: [
'name' => $name,
'field' => $field
]
);
}

try {
$validator->assert($value[$field], \sprintf('"%s"', $field));
}
catch (ValidationException $exception) {
throw new CollectionException(
message: $this->message,
parameters: [
'name' => $name,
'field' => $field,
'message' => $exception->getMessage()
]
);
}
}

if (!$this->allowExtraFields) {
foreach ($value as $field => $fieldValue) {
if (!isset($this->fields[$field])) {
throw new CollectionException(
message: $this->extraFieldsMessage,
parameters: [
'name' => $name,
'field' => $field
]
);
}
}
}
}
}
30 changes: 15 additions & 15 deletions src/Rule/EachKey.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,23 @@ public function assert(mixed $value, ?string $name = null): void
throw new UnexpectedTypeException('array|\Traversable', get_debug_type($value));
}

try {
foreach ($value as $key => $element) {
foreach ($value as $key => $element) {
try {
$this->validator->assert($key, $name);
}
}
catch (ValidationException $exception) {
throw new EachKeyException(
message: $this->message,
parameters: [
'value' => $value,
'name' => $name,
'key' => $key,
'element' => $element,
// Replaces string "value" with string "key value" to get a more intuitive error message
'message' => \str_replace(' value ', ' key value ', $exception->getMessage())
]
);
catch (ValidationException $exception) {
throw new EachKeyException(
message: $this->message,
parameters: [
'value' => $value,
'name' => $name,
'key' => $key,
'element' => $element,
// Replaces string "value" with string "key value" to get a more intuitive error message
'message' => \str_replace(' value ', ' key value ', $exception->getMessage())
]
);
}
}
}
}
28 changes: 14 additions & 14 deletions src/Rule/EachValue.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,22 @@ public function assert(mixed $value, ?string $name = null): void
throw new UnexpectedTypeException('array|\Traversable', get_debug_type($value));
}

try {
foreach ($value as $key => $element) {
foreach ($value as $key => $element) {
try {
$this->validator->assert($element, $name);
}
}
catch (ValidationException $exception) {
throw new EachValueException(
message: $this->message,
parameters: [
'value' => $value,
'name' => $name,
'key' => $key,
'element' => $element,
'message' => $exception->getMessage()
]
);
catch (ValidationException $exception) {
throw new EachValueException(
message: $this->message,
parameters: [
'value' => $value,
'name' => $name,
'key' => $key,
'element' => $element,
'message' => $exception->getMessage()
]
);
}
}
}
}
2 changes: 1 addition & 1 deletion src/Rule/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public function assert(mixed $value, ?string $name = null): void
}

if (!isset(self::TYPE_FUNCTIONS[$constraint]) && !\class_exists($constraint) && !\interface_exists($constraint)) {
throw new UnexpectedOptionException('constraint type', \array_keys(self::TYPE_FUNCTIONS), $constraint);
throw new UnexpectedOptionException('type', \array_keys(self::TYPE_FUNCTIONS), $constraint);
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/StaticValidatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ public static function choice(
?string $maxMessage = null
): ChainedValidatorInterface&Validator;

public static function collection(
array $fields,
bool $allowExtraFields = false,
?string $message = null,
?string $extraFieldsMessage = null,
?string $missingFieldsMessage = null
): ChainedValidatorInterface&Validator;

public static function count(
?int $min = null,
?int $max = null,
Expand Down
Loading

0 comments on commit 120ee88

Please sign in to comment.