diff --git a/docs/03-rules.md b/docs/03-rules.md index ddf9037..79dd7ba 100644 --- a/docs/03-rules.md +++ b/docs/03-rules.md @@ -56,4 +56,5 @@ ## Other Rules +- [AtLeastOneOf](03-rules_at-least-one-of.md) - [Optional](03-rules_optional.md) \ No newline at end of file diff --git a/docs/03-rules_at-least-one-of.md b/docs/03-rules_at-least-one-of.md new file mode 100644 index 0000000..9f84ed8 --- /dev/null +++ b/docs/03-rules_at-least-one-of.md @@ -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` `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 \ No newline at end of file diff --git a/src/ChainedValidatorInterface.php b/src/ChainedValidatorInterface.php index 6fb6d1c..8959be9 100644 --- a/src/ChainedValidatorInterface.php +++ b/src/ChainedValidatorInterface.php @@ -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 diff --git a/src/Exception/AtLeastOneOfException.php b/src/Exception/AtLeastOneOfException.php new file mode 100644 index 0000000..899db18 --- /dev/null +++ b/src/Exception/AtLeastOneOfException.php @@ -0,0 +1,5 @@ + $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)); + } +} \ No newline at end of file diff --git a/src/Exception/ValidationException.php b/src/Exception/ValidationException.php index 7184434..2acb64b 100644 --- a/src/Exception/ValidationException.php +++ b/src/Exception/ValidationException.php @@ -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)); - } } \ No newline at end of file diff --git a/src/Rule/AtLeastOneOf.php b/src/Rule/AtLeastOneOf.php new file mode 100644 index 0000000..94e5c73 --- /dev/null +++ b/src/Rule/AtLeastOneOf.php @@ -0,0 +1,55 @@ +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) + ] + ); + } +} \ No newline at end of file diff --git a/src/Rule/Length.php b/src/Rule/Length.php index a4a6e9a..60b7a31 100644 --- a/src/Rule/Length.php +++ b/src/Rule/Length.php @@ -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: [ diff --git a/src/StaticValidatorInterface.php b/src/StaticValidatorInterface.php index c6079a8..7b7ce50 100644 --- a/src/StaticValidatorInterface.php +++ b/src/StaticValidatorInterface.php @@ -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 diff --git a/tests/AtLeastOneOfTest.php b/tests/AtLeastOneOfTest.php new file mode 100644 index 0000000..a94d7b1 --- /dev/null +++ b/tests/AtLeastOneOfTest.php @@ -0,0 +1,73 @@ + [new AtLeastOneOf(['invalid']), 'string', $unexpectedTypeMessage]; + } + + public static function provideRuleFailureConditionData(): \Generator + { + $exception = AtLeastOneOfException::class; + $message = '/The (.*) value should satisfy at least one of the following constraints\: (.*)\./'; + + yield 'constraints' => [ + new AtLeastOneOf([ + new Validator(new IsFalse()), + new Validator(new Type('string'), new Length(10)), + ]), 'invalid', $exception, $message + ]; + } + + public static function provideRuleSuccessConditionData(): \Generator + { + yield 'constraints 1' => [ + new AtLeastOneOf([ + new Validator(new IsFalse()), + new Validator(new Type('string'), new Length(10)), + ]), + false + ]; + yield 'constraints 2' => [ + new AtLeastOneOf([ + new Validator(new IsFalse()), + new Validator(new Type('string'), new Length(10)), + ]), + 'valid string' + ]; + } + + public static function provideRuleMessageOptionData(): \Generator + { + yield 'message' => [ + new AtLeastOneOf( + constraints: [new Validator(new Blank())], + message: '{{ name }} | {{ value }} | {{ messages }}' + ), + 'string', + 'test | "string" | [1] The test value should be blank, "string" given.' + ]; + } +} \ No newline at end of file