Skip to content

Commit

Permalink
feat: added atLeastOneOf rule
Browse files Browse the repository at this point in the history
  • Loading branch information
andrepimpao committed Aug 12, 2024
1 parent 3d3e3e8 commit a4a0325
Show file tree
Hide file tree
Showing 10 changed files with 293 additions and 74 deletions.
1 change: 1 addition & 0 deletions docs/03-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@

## Other Rules

- [AtLeastOneOf](03-rules_at-least-one-of.md)
- [Optional](03-rules_optional.md)
65 changes: 65 additions & 0 deletions docs/03-rules_at-least-one-of.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# AtLeastOneOf

Checks that the value satisfies at least one of the given constraints.

```php
/** Validator[] $constraints */
Choice(
array $constraints,
?string $message = null
);
```

## Basic Usage

```php
Validator::atLeastOneOf([
Validator::isFalse(),
Validator::type('int')->greaterThanOrEqual(18)
])->validate(false); // true

Validator::atLeastOneOf([
Validator::isFalse(),
Validator::type('int')->greaterThanOrEqual(18)
])->validate(20); // true

Validator::atLeastOneOf([
Validator::isFalse(),
Validator::type('int')->greaterThanOrEqual(18)
])->validate(true); // false

Validator::atLeastOneOf([
Validator::isFalse(),
Validator::type('int')->greaterThanOrEqual(18)
])->validate(16); // false
```

> [!NOTE]
> An `UnexpectedValueException` will be thrown when a value in the `constraints` array is not an instance of `Validator`.
## Options

### `constraints`

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

Collection of constraints to be validated against the input value.
If at least one given constraint is valid, the validation is considered successful.

### `message`

type: `?string` default: `The {{ name }} value should satisfy at least one of the following constraints: {{ messages }}`

Message that will be shown if all given constraints are not valid.

The following parameters are available:

| Parameter | Description |
|------------------|-------------------------------------------------|
| `{{ value }}` | The current invalid value |
| `{{ name }}` | Name of the invalid value |
| `{{ messages }}` | List of error messages based on the constraints |

## Changelog

- `1.3.0` Created
5 changes: 5 additions & 0 deletions src/ChainedValidatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@

interface ChainedValidatorInterface
{
public function atLeastOneOf(
array $constraints,
?string $message = null
): ChainedValidatorInterface&Validator;

public function blank(
?callable $normalizer = null,
?string $message = null
Expand Down
5 changes: 5 additions & 0 deletions src/Exception/AtLeastOneOfException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

namespace ProgrammatorDev\Validator\Exception;

class AtLeastOneOfException extends ValidationException {}
79 changes: 79 additions & 0 deletions src/Exception/Util/MessageTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

namespace ProgrammatorDev\Validator\Exception\Util;

trait MessageTrait
{
private function formatMessage(string $message, array $parameters = []): string
{
// if a name was not given, remove it from the message template but keep it intuitive
if (empty($parameters['name'])) {
$message = \str_replace(' {{ name }} ', ' ', $message);
unset($parameters['name']);
}

foreach ($parameters as $parameter => $value) {
// format values (with some exceptions to avoid adding unnecessary quotation marks)
$message = \str_replace(
\sprintf('{{ %s }}', $parameter),
(\in_array($parameter, ['name', 'message', 'messages'])) ? $value : $this->formatValue($value),
$message
);
}

return $message;
}

private function formatValue(mixed $value): string
{
if ($value instanceof \DateTimeInterface) {
return $value->format('Y-m-d H:i:s');
}

if (\is_object($value)) {
if ($value instanceof \Stringable) {
return $value->__toString();
}

return 'object';
}

if (\is_array($value)) {
return $this->formatValues($value);
}

if (\is_string($value)) {
// replace line breaks and tabs with single space
$value = \str_replace(["\n", "\r", "\t", "\v", "\x00"], ' ', $value);

return \sprintf('"%s"', $value);
}

if (\is_resource($value)) {
return 'resource';
}

if ($value === null) {
return 'null';
}

if ($value === false) {
return 'false';
}

if ($value === true) {
return 'true';
}

return (string) $value;
}

private function formatValues(array $values): string
{
foreach ($values as $key => $value) {
$values[$key] = $this->formatValue($value);
}

return \sprintf('[%s]', \implode(', ', $values));
}
}
77 changes: 4 additions & 73 deletions src/Exception/ValidationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,85 +2,16 @@

namespace ProgrammatorDev\Validator\Exception;

use ProgrammatorDev\Validator\Exception\Util\MessageTrait;

class ValidationException extends \Exception
{
use MessageTrait;

public function __construct(string $message, array $parameters = [])
{
$message = $this->formatMessage($message, $parameters);

parent::__construct($message);
}

private function formatMessage(string $message, array $parameters = []): string
{
// If a name was not given, remove it from the message template but keep it intuitive
if (empty($parameters['name'])) {
$message = \str_replace(' {{ name }} ', ' ', $message);
unset($parameters['name']);
}

foreach ($parameters as $parameter => $value) {
// Format values (with some exceptions [name, message] to avoid adding unnecessary quotation marks)
$message = \str_replace(
\sprintf('{{ %s }}', $parameter),
(\in_array($parameter, ['name', 'message'])) ? $value : $this->formatValue($value),
$message
);
}

return $message;
}

private function formatValue(mixed $value): string
{
if ($value instanceof \DateTimeInterface) {
return $value->format('Y-m-d H:i:s');
}

if (\is_object($value)) {
if ($value instanceof \Stringable) {
return $value->__toString();
}

return 'object';
}

if (\is_array($value)) {
return $this->formatValues($value);
}

if (\is_string($value)) {
// Replace line breaks and tabs with single space
$value = \str_replace(["\n", "\r", "\t", "\v", "\x00"], ' ', $value);

return \sprintf('"%s"', $value);
}

if (\is_resource($value)) {
return 'resource';
}

if ($value === null) {
return 'null';
}

if ($value === false) {
return 'false';
}

if ($value === true) {
return 'true';
}

return (string) $value;
}

private function formatValues(array $values): string
{
foreach ($values as $key => $value) {
$values[$key] = $this->formatValue($value);
}

return \sprintf('[%s]', \implode(', ', $values));
}
}
55 changes: 55 additions & 0 deletions src/Rule/AtLeastOneOf.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace ProgrammatorDev\Validator\Rule;

use ProgrammatorDev\Validator\Exception\AtLeastOneOfException;
use ProgrammatorDev\Validator\Exception\UnexpectedValueException;
use ProgrammatorDev\Validator\Exception\ValidationException;
use ProgrammatorDev\Validator\Validator;

class AtLeastOneOf extends AbstractRule implements RuleInterface
{
private string $message = 'The {{ name }} value should satisfy at least one of the following constraints: {{ messages }}';

/** @param Validator[] $constraints */
public function __construct(
private readonly array $constraints,
?string $message = null
)
{
$this->message = $message ?? $this->message;
}

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

$messages = [];

foreach ($this->constraints as $key => $constraint) {
try {
$constraint->assert($value, $name);
return;
}
catch (ValidationException|UnexpectedValueException $exception) {
$messages[] = \sprintf('[%d] %s', ($key + 1), $exception->getMessage());
}
}

throw new AtLeastOneOfException(
message: $this->message,
parameters: [
'value' => $value,
'name' => $name,
'messages' => \implode(' ', $messages)
]
);
}
}
2 changes: 1 addition & 1 deletion src/Rule/Length.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public function assert(mixed $value, ?string $name = null): void
$value = ($this->normalizer)($value);
}

if (!mb_check_encoding($value, $this->charset)) {
if (!\mb_check_encoding($value, $this->charset)) {
throw new LengthException(
message: $this->charsetMessage,
parameters: [
Expand Down
5 changes: 5 additions & 0 deletions src/StaticValidatorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@

interface StaticValidatorInterface
{
public static function atLeastOneOf(
array $constraints,
?string $message = null
): ChainedValidatorInterface&Validator;

public static function blank(
?callable $normalizer = null,
?string $message = null
Expand Down
Loading

0 comments on commit a4a0325

Please sign in to comment.