From 975652da4a46e992994b81f8d07842dda4f5740a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Mon, 14 Aug 2023 12:11:48 +0100 Subject: [PATCH 01/17] chore: parse line breaks and tab chars on error messages --- src/Exception/ValidationException.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Exception/ValidationException.php b/src/Exception/ValidationException.php index ed2f549..29c0490 100644 --- a/src/Exception/ValidationException.php +++ b/src/Exception/ValidationException.php @@ -38,7 +38,8 @@ private function formatValue(mixed $value): string } if (\is_string($value)) { - return $value; + // Replace line breaks and tabs with single space + return str_replace(["\n", "\r", "\t", "\v", "\x00"], ' ', $value); } if (\is_resource($value)) { From 4de3d23f620b93ff67f0cd1f8bb9a6c60313208d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Mon, 14 Aug 2023 18:26:15 +0100 Subject: [PATCH 02/17] chore: require ctype polyfill for Type rule --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 761a36a..89da303 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ } ], "require": { - "php": ">=8.1" + "php": ">=8.1", + "symfony/polyfill-ctype": "^1.27" }, "require-dev": { "phpunit/phpunit": "^10.0", From b7e1580c7d47a2855427e4f824abeb51b445488f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Mon, 14 Aug 2023 18:57:27 +0100 Subject: [PATCH 03/17] feat: added Type rule --- src/Exception/TypeException.php | 5 ++ src/Rule/Type.php | 80 ++++++++++++++++++++++ tests/TypeTest.php | 117 ++++++++++++++++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 src/Exception/TypeException.php create mode 100644 src/Rule/Type.php create mode 100644 tests/TypeTest.php diff --git a/src/Exception/TypeException.php b/src/Exception/TypeException.php new file mode 100644 index 0000000..042e880 --- /dev/null +++ b/src/Exception/TypeException.php @@ -0,0 +1,5 @@ + 'is_bool', + 'boolean' => 'is_bool', + 'int' => 'is_int', + 'integer' => 'is_int', + 'long' => 'is_int', + 'float' => 'is_float', + 'double' => 'is_float', + 'real' => 'is_float', + 'numeric' => 'is_numeric', + 'string' => 'is_string', + 'scalar' => 'is_scalar', + 'array' => 'is_array', + 'iterable' => 'is_iterable', + 'countable' => 'is_countable', + 'callable' => 'is_callable', + 'object' => 'is_object', + 'resource' => 'is_resource', + 'null' => 'is_null', + 'alphanumeric' => 'ctype_alnum', + 'alpha' => 'ctype_alpha', + 'digit' => 'ctype_digit', + 'control' => 'ctype_cntrl', + 'punctuation' => 'ctype_punct', + 'hexadecimal' => 'ctype_xdigit', + 'graph' => 'ctype_graph', + 'printable' => 'ctype_print', + 'whitespace' => 'ctype_space', + 'lowercase' => 'ctype_lower', + 'uppercase' => 'ctype_upper' + ]; + + public function __construct( + private readonly string|array $constraint, + private readonly string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.' + ) {} + + public function assert(mixed $value, string $name): void + { + $constraints = (array) $this->constraint; + + foreach ($constraints as $constraint) { + if (isset(self::TYPE_FUNCTIONS[$constraint]) && (self::TYPE_FUNCTIONS[$constraint])($value)) { + return; + } + + if ($value instanceof $constraint) { + return; + } + + if (!isset(self::TYPE_FUNCTIONS[$constraint]) && !class_exists($constraint) && !interface_exists($constraint)) { + throw new UnexpectedValueException( + \sprintf( + 'Invalid constraint type "%s". Accepted values are: "%s"', + $constraint, + implode(', ', array_keys(self::TYPE_FUNCTIONS)) + ) + ); + } + } + + throw new TypeException( + message: $this->message, + parameters: [ + 'name' => $name, + 'value' => $value, + 'constraint' => $this->constraint + ] + ); + } +} \ No newline at end of file diff --git a/tests/TypeTest.php b/tests/TypeTest.php new file mode 100644 index 0000000..fbe559e --- /dev/null +++ b/tests/TypeTest.php @@ -0,0 +1,117 @@ + [new Type('invalid'), 'string', $message]; + } + + public static function provideRuleFailureConditionData(): \Generator + { + $exception = TypeException::class; + $message = '/The "(.*)" value should be of type "(.*)", "(.*)" given./'; + + yield 'bool' => [new Type('bool'), 'invalid', $exception, $message]; + yield 'boolean' => [new Type('boolean'), 'invalid', $exception, $message]; + yield 'int' => [new Type('int'), 'invalid', $exception, $message]; + yield 'integer' => [new Type('integer'), 'invalid', $exception, $message]; + yield 'long' => [new Type('long'), 'invalid', $exception, $message]; + yield 'float' => [new Type('float'), 'invalid', $exception, $message]; + yield 'double' => [new Type('double'), 'invalid', $exception, $message]; + yield 'real' => [new Type('real'), 'invalid', $exception, $message]; + yield 'numeric' => [new Type('numeric'), 'invalid', $exception, $message]; + yield 'string' => [new Type('string'), 123, $exception, $message]; + yield 'scalar' => [new Type('scalar'), [], $exception, $message]; + yield 'array' => [new Type('array'), 'invalid', $exception, $message]; + yield 'iterable' => [new Type('iterable'), 'invalid', $exception, $message]; + yield 'countable' => [new Type('countable'), 'invalid', $exception, $message]; + yield 'callable' => [new Type('callable'), 'invalid', $exception, $message]; + yield 'object' => [new Type('object'), 'invalid', $exception, $message]; + yield 'resource' => [new Type('resource'), 'invalid', $exception, $message]; + yield 'null' => [new Type('null'), 'invalid', $exception, $message]; + yield 'alphanumeric' => [new Type('alphanumeric'), 'foo!#$bar', $exception, $message]; + yield 'alpha' => [new Type('alpha'), 'arf12', $exception, $message]; + yield 'digit' => [new Type('digit'), 'invalid', $exception, $message]; + yield 'control' => [new Type('control'), 'arf12', $exception, $message]; + yield 'punctuation' => [new Type('punctuation'), 'ABasdk!@!$#', $exception, $message]; + yield 'hexadecimal' => [new Type('hexadecimal'), 'AR1012', $exception, $message]; + yield 'graph' => [new Type('graph'), "asdf\n\r\t", $exception, $message]; + yield 'printable' => [new Type('printable'), "asdf\n\r\t", $exception, $message]; + yield 'whitespace' => [new Type('whitespace'), "\narf12", $exception, $message]; + yield 'lowercase' => [new Type('lowercase'), 'Invalid', $exception, $message]; + yield 'uppercase' => [new Type('uppercase'), 'invalid', $exception, $message]; + + yield 'class' => [new Type(\DateTime::class), 'invalid', $exception, $message]; + yield 'interface' => [new Type(\DateTimeInterface::class), 'invalid', $exception, $message]; + + yield 'multiple types' => [new Type(['digit', 'numeric']), 'invalid', $exception, $message]; + } + + public static function provideRuleSuccessConditionData(): \Generator + { + yield 'bool' => [new Type('bool'), true]; + yield 'boolean' => [new Type('boolean'), false]; + yield 'int' => [new Type('int'), 1]; + yield 'integer' => [new Type('integer'), 2]; + yield 'long' => [new Type('long'), 3]; + yield 'float' => [new Type('float'), 1.1]; + yield 'double' => [new Type('double'), 1.2]; + yield 'real' => [new Type('real'), 1.3]; + yield 'numeric' => [new Type('numeric'), 123]; + yield 'string' => [new Type('string'), 'string']; + yield 'scalar' => [new Type('scalar'), 'string']; + yield 'array' => [new Type('array'), [1, 2, 3]]; + yield 'iterable' => [new Type('iterable'), new \ArrayIterator([1, 2, 3])]; + yield 'countable' => [new Type('countable'), new \ArrayIterator([1, 2, 3])]; + yield 'callable' => [new Type('callable'), 'trim']; + yield 'object' => [new Type('object'), new \stdClass()]; + yield 'resource' => [new Type('resource'), fopen('php://stdout', 'r')]; + yield 'null' => [new Type('null'), null]; + yield 'alphanumeric' => [new Type('alphanumeric'), 'abc123']; + yield 'alpha' => [new Type('alpha'), 'abc']; + yield 'digit' => [new Type('digit'), '123']; + yield 'control' => [new Type('control'), "\n\r\t"]; + yield 'punctuation' => [new Type('punctuation'), '*&$()']; + yield 'hexadecimal' => [new Type('hexadecimal'), 'AB10BC99']; + yield 'graph' => [new Type('graph'), 'LKA#@%.54']; + yield 'printable' => [new Type('printable'), 'LKA#@%.54']; + yield 'whitespace' => [new Type('whitespace'), "\n\r\t"]; + yield 'lowercase' => [new Type('lowercase'), 'string']; + yield 'uppercase' => [new Type('uppercase'), 'STRING']; + + yield 'class' => [new Type(\DateTime::class), new \DateTime()]; + yield 'interface' => [new Type(\DateTimeInterface::class), new \DateTime()]; + + yield 'multiple types' => [new Type(['alpha', 'numeric']), '123']; + } + + public static function provideRuleMessageOptionData(): \Generator + { + yield 'message' => [ + new Type( + constraint: 'int', + message: 'The "{{ name }}" value is not of type "{{ constraint }}".' + ), + 'string', + 'The "test" value is not of type "int".' + ]; + } + +} \ No newline at end of file From 1187adced609873a2a995c8b6ee9449f62750c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Mon, 14 Aug 2023 20:32:38 +0100 Subject: [PATCH 04/17] feat: added Type rule documentation --- docs/03-rules.md | 1 + docs/03x-rules-type.md | 87 +++++++++++++++++++++++++++++++ src/ChainedValidatorInterface.php | 5 ++ src/StaticValidatorInterface.php | 5 ++ 4 files changed, 98 insertions(+) create mode 100644 docs/03x-rules-type.md diff --git a/docs/03-rules.md b/docs/03-rules.md index 2cf68da..2db5910 100644 --- a/docs/03-rules.md +++ b/docs/03-rules.md @@ -8,6 +8,7 @@ ## Basic Rules - [NotBlank](03x-rules-not-blank.md) +- [Type](03x-rules-type.md) ## Comparison Rules diff --git a/docs/03x-rules-type.md b/docs/03x-rules-type.md new file mode 100644 index 0000000..1337a3b --- /dev/null +++ b/docs/03x-rules-type.md @@ -0,0 +1,87 @@ +# Type + +Validates that a value is of a specific type. + +If an array with multiple types is provided, it will validate if the value is of at least one of the given types. +For example, if `['alpha', 'numeric']` is provided, it will validate if the value is of type `alpha` or of type `numeric`. + +```php +Type( + string|array $constraint, + string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.' +) +``` + +## Basic Usage + +```php +// Single data type +Validator::type('string')->validate('green'); // true +Validator::type('alphanumeric')->validate('gr33n'); // true + +// Multiple data types +// Validates if value is of at least one of the provided types +Validator::type(['alpha', 'numeric'])->validate('green'); // true (alpha) +Validator::type(['alpha', 'numeric'])->validate('33'); // true (numeric) +Validator::type(['alpha', 'numeric'])->validate('gr33n'); // false (not alpha nor numeric) + +// Class or interface type +Validator::type(\DateTime::class)->validate(new \DateTime()); // true +Validator::type(\DateTimeInterface::class)->validate(new \DateTime()); // true +``` + +> **Note** +> An `UnexpectedValueException` will be thrown when a constraint type, class and interface is invalid. + +## Options + +### `constraints` + +type: `string`|`array` `required` + +Type(s) to validate the input value type. +Can validate instances of classes and interfaces. + +If an array with multiple types is provided, it will validate if the value is of at least one of the given types. +For example, if `['alpha', 'numeric']` is provided, it will validate if the value is of type `alpha` or of type `numeric`. + +Available constraint types: + +- [`bool`](https://www.php.net/manual/en/function.is-bool.php), [`boolean`](https://www.php.net/manual/en/function.is-bool.php) +- [`int`](https://www.php.net/manual/en/function.is-int.php), [`integer`](https://www.php.net/manual/en/function.is-int.php), [`long`](https://www.php.net/manual/en/function.is-int.php) +- [`float`](https://www.php.net/manual/en/function.is-float.php), [`double`](https://www.php.net/manual/en/function.is-float.php), [`real`](https://www.php.net/manual/en/function.is-float.php) +- [`numeric`](https://www.php.net/manual/en/function.is-numeric.php) +- [`string`](https://www.php.net/manual/en/function.is-string.php) +- [`scalar`](https://www.php.net/manual/en/function.is-scalar.php) +- [`array`](https://www.php.net/manual/en/function.is-array.php) +- [`iterable`](https://www.php.net/manual/en/function.is-iterable.php) +- [`countable`](https://www.php.net/manual/en/function.is-countable.php) +- [`callable`](https://www.php.net/manual/en/function.is-callable.php) +- [`object`](https://www.php.net/manual/en/function.is-object.php) +- [`resource`](https://www.php.net/manual/en/function.is-resource.php) +- [`null`](https://www.php.net/manual/en/function.is-null.php) +- [`alphanumeric`](https://www.php.net/manual/en/function.ctype-alnum) +- [`alpha`](https://www.php.net/manual/en/function.ctype-alpha.php) +- [`digit`](https://www.php.net/manual/en/function.ctype-digit.php) +- [`control`](https://www.php.net/manual/en/function.ctype-cntrl.php) +- [`punctuation`](https://www.php.net/manual/en/function.ctype-punct.php) +- [`hexadecimal`](https://www.php.net/manual/en/function.ctype-xdigit.php) +- [`graph`](https://www.php.net/manual/en/function.ctype-graph.php) +- [`printable`](https://www.php.net/manual/en/function.ctype-print.php) +- [`whitespace`](https://www.php.net/manual/en/function.ctype-space.php) +- [`lowercase`](https://www.php.net/manual/en/function.ctype-lower.php) +- [`uppercase`](https://www.php.net/manual/en/function.ctype-upper.php) + +### `message` + +type `string` default: `The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.` + +Message that will be shown if input value is not of a specific type. + +The following parameters are available: + +| Parameter | Description | +|---------------------|---------------------------| +| `{{ value }}` | The current invalid value | +| `{{ name }}` | Name of the invalid value | +| `{{ constraints }}` | The valid type(s) | \ No newline at end of file diff --git a/src/ChainedValidatorInterface.php b/src/ChainedValidatorInterface.php index 020a43b..92e026e 100644 --- a/src/ChainedValidatorInterface.php +++ b/src/ChainedValidatorInterface.php @@ -64,4 +64,9 @@ public function range( ): ChainedValidatorInterface; public function rule(RuleInterface $constraint): ChainedValidatorInterface; + + public function type( + string|array $constraints, + string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.' + ): ChainedValidatorInterface; } \ No newline at end of file diff --git a/src/StaticValidatorInterface.php b/src/StaticValidatorInterface.php index 166df1d..730cf4a 100644 --- a/src/StaticValidatorInterface.php +++ b/src/StaticValidatorInterface.php @@ -54,4 +54,9 @@ public static function range( ): ChainedValidatorInterface; public static function rule(RuleInterface $constraint): ChainedValidatorInterface; + + public static function type( + string|array $constraints, + string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.' + ): ChainedValidatorInterface; } \ No newline at end of file From 2ec5740b8d8c5bfecb6af6248fb9b62ae1a17866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Mon, 14 Aug 2023 20:34:10 +0100 Subject: [PATCH 05/17] fix: wrong Type argument name --- docs/03x-rules-type.md | 12 ++++++------ src/ChainedValidatorInterface.php | 2 +- src/StaticValidatorInterface.php | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/03x-rules-type.md b/docs/03x-rules-type.md index 1337a3b..70877a8 100644 --- a/docs/03x-rules-type.md +++ b/docs/03x-rules-type.md @@ -35,7 +35,7 @@ Validator::type(\DateTimeInterface::class)->validate(new \DateTime()); // true ## Options -### `constraints` +### `constraint` type: `string`|`array` `required` @@ -80,8 +80,8 @@ Message that will be shown if input value is not of a specific type. The following parameters are available: -| Parameter | Description | -|---------------------|---------------------------| -| `{{ value }}` | The current invalid value | -| `{{ name }}` | Name of the invalid value | -| `{{ constraints }}` | The valid type(s) | \ No newline at end of file +| Parameter | Description | +|--------------------|---------------------------| +| `{{ value }}` | The current invalid value | +| `{{ name }}` | Name of the invalid value | +| `{{ constraint }}` | The valid type(s) | \ No newline at end of file diff --git a/src/ChainedValidatorInterface.php b/src/ChainedValidatorInterface.php index 92e026e..4bfff66 100644 --- a/src/ChainedValidatorInterface.php +++ b/src/ChainedValidatorInterface.php @@ -66,7 +66,7 @@ public function range( public function rule(RuleInterface $constraint): ChainedValidatorInterface; public function type( - string|array $constraints, + string|array $constraint, string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.' ): ChainedValidatorInterface; } \ No newline at end of file diff --git a/src/StaticValidatorInterface.php b/src/StaticValidatorInterface.php index 730cf4a..d216f95 100644 --- a/src/StaticValidatorInterface.php +++ b/src/StaticValidatorInterface.php @@ -56,7 +56,7 @@ public static function range( public static function rule(RuleInterface $constraint): ChainedValidatorInterface; public static function type( - string|array $constraints, + string|array $constraint, string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.' ): ChainedValidatorInterface; } \ No newline at end of file From fcb647003da3003fe507068902ac6d1bf479a422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 11:28:31 +0100 Subject: [PATCH 06/17] chore: improved Type rule documentation --- docs/03x-rules-type.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/03x-rules-type.md b/docs/03x-rules-type.md index 70877a8..a4ba140 100644 --- a/docs/03x-rules-type.md +++ b/docs/03x-rules-type.md @@ -15,11 +15,11 @@ Type( ## Basic Usage ```php -// Single data type +// Single type Validator::type('string')->validate('green'); // true Validator::type('alphanumeric')->validate('gr33n'); // true -// Multiple data types +// Multiple types // Validates if value is of at least one of the provided types Validator::type(['alpha', 'numeric'])->validate('green'); // true (alpha) Validator::type(['alpha', 'numeric'])->validate('33'); // true (numeric) @@ -31,7 +31,7 @@ Validator::type(\DateTimeInterface::class)->validate(new \DateTime()); // true ``` > **Note** -> An `UnexpectedValueException` will be thrown when a constraint type, class and interface is invalid. +> An `UnexpectedValueException` will be thrown when a constraint type, class or interface is invalid. ## Options @@ -45,7 +45,7 @@ Can validate instances of classes and interfaces. If an array with multiple types is provided, it will validate if the value is of at least one of the given types. For example, if `['alpha', 'numeric']` is provided, it will validate if the value is of type `alpha` or of type `numeric`. -Available constraint types: +Available data type constraints: - [`bool`](https://www.php.net/manual/en/function.is-bool.php), [`boolean`](https://www.php.net/manual/en/function.is-bool.php) - [`int`](https://www.php.net/manual/en/function.is-int.php), [`integer`](https://www.php.net/manual/en/function.is-int.php), [`long`](https://www.php.net/manual/en/function.is-int.php) @@ -60,6 +60,9 @@ Available constraint types: - [`object`](https://www.php.net/manual/en/function.is-object.php) - [`resource`](https://www.php.net/manual/en/function.is-resource.php) - [`null`](https://www.php.net/manual/en/function.is-null.php) + +Available character type constraints: + - [`alphanumeric`](https://www.php.net/manual/en/function.ctype-alnum) - [`alpha`](https://www.php.net/manual/en/function.ctype-alpha.php) - [`digit`](https://www.php.net/manual/en/function.ctype-digit.php) From 6e04a28700ac97f04aca4a704ba01d058d935db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 14:14:54 +0100 Subject: [PATCH 07/17] chore: added Type constants --- src/Rule/Type.php | 92 +++++++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/src/Rule/Type.php b/src/Rule/Type.php index a81d50f..a902558 100644 --- a/src/Rule/Type.php +++ b/src/Rule/Type.php @@ -7,36 +7,66 @@ class Type extends AbstractRule implements RuleInterface { + public const BOOL = 'bool'; + public const BOOLEAN = 'boolean'; + public const INT = 'int'; + public const INTEGER = 'integer'; + public const LONG = 'long'; + public const FLOAT = 'float'; + public const DOUBLE = 'double'; + public const REAL = 'real'; + public const NUMERIC = 'numeric'; + public const STRING = 'string'; + public const SCALAR = 'scalar'; + public const ARRAY = 'array'; + public const ITERABLE = 'iterable'; + public const COUNTABLE = 'countable'; + public const CALLABLE = 'callable'; + public const OBJECT = 'object'; + public const RESOURCE = 'resource'; + public const NULL = 'null'; + public const ALPHANUMERIC = 'alphanumeric'; + public const ALPHA = 'alpha'; + public const DIGIT = 'digit'; + public const CONTROL = 'control'; + public const PUNCTUATION = 'punctuation'; + public const HEXADECIMAL = 'hexadecimal'; + public const GRAPH = 'graph'; + public const PRINTABLE = 'printable'; + public const WHITESPACE = 'whitespace'; + public const LOWERCASE = 'lowercase'; + public const UPPERCASE = 'uppercase'; + private const TYPE_FUNCTIONS = [ - 'bool' => 'is_bool', - 'boolean' => 'is_bool', - 'int' => 'is_int', - 'integer' => 'is_int', - 'long' => 'is_int', - 'float' => 'is_float', - 'double' => 'is_float', - 'real' => 'is_float', - 'numeric' => 'is_numeric', - 'string' => 'is_string', - 'scalar' => 'is_scalar', - 'array' => 'is_array', - 'iterable' => 'is_iterable', - 'countable' => 'is_countable', - 'callable' => 'is_callable', - 'object' => 'is_object', - 'resource' => 'is_resource', - 'null' => 'is_null', - 'alphanumeric' => 'ctype_alnum', - 'alpha' => 'ctype_alpha', - 'digit' => 'ctype_digit', - 'control' => 'ctype_cntrl', - 'punctuation' => 'ctype_punct', - 'hexadecimal' => 'ctype_xdigit', - 'graph' => 'ctype_graph', - 'printable' => 'ctype_print', - 'whitespace' => 'ctype_space', - 'lowercase' => 'ctype_lower', - 'uppercase' => 'ctype_upper' + self::BOOL => 'is_bool', + self::BOOLEAN => 'is_bool', + self::INT => 'is_int', + self::INTEGER => 'is_int', + self::LONG => 'is_int', + self::FLOAT => 'is_float', + self::DOUBLE => 'is_float', + self::REAL => 'is_float', + self::NUMERIC => 'is_numeric', + self::STRING => 'is_string', + self::SCALAR => 'is_scalar', + self::ARRAY => 'is_array', + self::ITERABLE => 'is_iterable', + self::COUNTABLE => 'is_countable', + self::CALLABLE => 'is_callable', + self::OBJECT => 'is_object', + self::RESOURCE => 'is_resource', + self::NULL => 'is_null', + self::ALPHANUMERIC => 'ctype_alnum', + self::ALPHA => 'ctype_alpha', + self::DIGIT => 'ctype_digit', + self::CONTROL => 'ctype_cntrl', + self::PUNCTUATION => 'ctype_punct', + self::HEXADECIMAL => 'ctype_xdigit', + self::GRAPH => 'ctype_graph', + self::PRINTABLE => 'ctype_print', + self::WHITESPACE => 'ctype_space', + self::LOWERCASE => 'ctype_lower', + self::UPPERCASE => 'ctype_upper' ]; public function __construct( @@ -57,12 +87,12 @@ public function assert(mixed $value, string $name): void return; } - if (!isset(self::TYPE_FUNCTIONS[$constraint]) && !class_exists($constraint) && !interface_exists($constraint)) { + if (!isset(self::TYPE_FUNCTIONS[$constraint]) && !\class_exists($constraint) && !\interface_exists($constraint)) { throw new UnexpectedValueException( \sprintf( 'Invalid constraint type "%s". Accepted values are: "%s"', $constraint, - implode(', ', array_keys(self::TYPE_FUNCTIONS)) + \implode('", "', \array_keys(self::TYPE_FUNCTIONS)) ) ); } From bc3b803f7ac15af68198236fdfc52450cd9200d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 14:17:02 +0100 Subject: [PATCH 08/17] chore: simplified message var to be coherent with other tests --- tests/NotBlankTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/NotBlankTest.php b/tests/NotBlankTest.php index 554860d..663c151 100644 --- a/tests/NotBlankTest.php +++ b/tests/NotBlankTest.php @@ -17,15 +17,15 @@ class NotBlankTest extends AbstractTest public static function provideRuleFailureConditionData(): \Generator { $exception = NotBlankException::class; - $exceptionMessage = '/The "(.*)" value should not be blank, "(.*)" given./'; + $message = '/The "(.*)" value should not be blank, "(.*)" given./'; - yield 'null' => [new NotBlank(), null, $exception, $exceptionMessage]; - yield 'false' => [new NotBlank(), false, $exception, $exceptionMessage]; - yield 'blank string' => [new NotBlank(), '', $exception, $exceptionMessage]; - yield 'blank array' => [new NotBlank(), [], $exception, $exceptionMessage]; + yield 'null' => [new NotBlank(), null, $exception, $message]; + yield 'false' => [new NotBlank(), false, $exception, $message]; + yield 'blank string' => [new NotBlank(), '', $exception, $message]; + yield 'blank array' => [new NotBlank(), [], $exception, $message]; - yield 'normalizer whitespace' => [new NotBlank(normalizer: 'trim'), ' ', $exception, $exceptionMessage]; - yield 'normalizer whitespace function' => [new NotBlank(normalizer: fn($value) => trim($value)), ' ', $exception, $exceptionMessage]; + yield 'normalizer whitespace' => [new NotBlank(normalizer: 'trim'), ' ', $exception, $message]; + yield 'normalizer whitespace function' => [new NotBlank(normalizer: fn($value) => trim($value)), ' ', $exception, $message]; } public static function provideRuleSuccessConditionData(): \Generator From b82e0655d0336b117979f838096611a97a596622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 14:18:34 +0100 Subject: [PATCH 09/17] feat: added Country rule --- composer.json | 1 + src/Exception/CountryException.php | 5 +++ src/Rule/Country.php | 59 ++++++++++++++++++++++++++++++ tests/CountryTest.php | 57 +++++++++++++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 src/Exception/CountryException.php create mode 100644 src/Rule/Country.php create mode 100644 tests/CountryTest.php diff --git a/composer.json b/composer.json index 89da303..295abf5 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ ], "require": { "php": ">=8.1", + "symfony/intl": "^6.3", "symfony/polyfill-ctype": "^1.27" }, "require-dev": { diff --git a/src/Exception/CountryException.php b/src/Exception/CountryException.php new file mode 100644 index 0000000..7c75655 --- /dev/null +++ b/src/Exception/CountryException.php @@ -0,0 +1,5 @@ +code, self::CODE_OPTIONS)) { + throw new UnexpectedValueException( + \sprintf( + 'Invalid code "%s". Accepted values are: "%s".', + $this->code, + \implode(", ", self::CODE_OPTIONS) + ) + ); + } + + if (!\is_string($value)) { + throw new UnexpectedValueException( + \sprintf('Expected value of type "string", "%s" given.', get_debug_type($value)) + ); + } + + // Keep original value for parameters + $input = strtoupper($value); + + if ( + ($this->code === self::ALPHA2_CODE && !Countries::exists($input)) + || ($this->code === self::ALPHA3_CODE && !Countries::alpha3CodeExists($input)) + ) { + throw new CountryException( + message: $this->message, + parameters: [ + 'name' => $name, + 'value' => $value, + 'code' => $this->code + ] + ); + } + } +} \ No newline at end of file diff --git a/tests/CountryTest.php b/tests/CountryTest.php new file mode 100644 index 0000000..6c65fa2 --- /dev/null +++ b/tests/CountryTest.php @@ -0,0 +1,57 @@ + [new Country('invalid'), 'PT', $codeMessage]; + yield 'invalid type' => [new Country(), 123, $typeMessage]; + } + + public static function provideRuleFailureConditionData(): \Generator + { + $exception = CountryException::class; + $message = '/The "(.*)" value is not a valid "(.*)" country code, "(.*)" given./'; + + yield 'default' => [new Country(), 'PRT', $exception, $message]; + yield 'alpha2' => [new Country(code: 'alpha2'), 'PRT', $exception, $message]; + yield 'alpha3' => [new Country(code: 'alpha3'), 'PT', $exception, $message]; + } + + public static function provideRuleSuccessConditionData(): \Generator + { + yield 'default' => [new Country(), 'PT']; + yield 'alpha2' => [new Country(code: 'alpha2'), 'PT']; + yield 'alpha2 lowercase' => [new Country(code: 'alpha2'), 'pt']; + yield 'alpha3' => [new Country(code: 'alpha3'), 'PRT']; + yield 'alpha3 lowercase' => [new Country(code: 'alpha3'), 'prt']; + } + + public static function provideRuleMessageOptionData(): \Generator + { + yield 'message' => [ + new Country( + message: 'The "{{ name }}" value "{{ value }}" is not a valid "{{ code }}" country code.' + ), + 'invalid', + 'The "test" value "invalid" is not a valid "alpha2" country code.' + ]; + } +} \ No newline at end of file From 9f4ec8fdd80c53650d3ecfa88e2794cc5923dd96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 16:00:57 +0100 Subject: [PATCH 10/17] chore: changed Country error message --- src/Rule/Country.php | 2 +- tests/CountryTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rule/Country.php b/src/Rule/Country.php index 57f26fe..5dcaf0f 100644 --- a/src/Rule/Country.php +++ b/src/Rule/Country.php @@ -18,7 +18,7 @@ class Country extends AbstractRule implements RuleInterface public function __construct( private readonly string $code = self::ALPHA2_CODE, - private readonly string $message = 'The "{{ name }}" value is not a valid "{{ code }}" country code, "{{ value }}" given.' + private readonly string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.' ) {} public function assert(mixed $value, string $name): void diff --git a/tests/CountryTest.php b/tests/CountryTest.php index 6c65fa2..62bb910 100644 --- a/tests/CountryTest.php +++ b/tests/CountryTest.php @@ -28,7 +28,7 @@ public static function provideRuleUnexpectedValueData(): \Generator public static function provideRuleFailureConditionData(): \Generator { $exception = CountryException::class; - $message = '/The "(.*)" value is not a valid "(.*)" country code, "(.*)" given./'; + $message = '/The "(.*)" value is not a valid country code, "(.*)" given./'; yield 'default' => [new Country(), 'PRT', $exception, $message]; yield 'alpha2' => [new Country(code: 'alpha2'), 'PRT', $exception, $message]; From 201f97ab0c1391d5acb8ab47e54ca409ffc188ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 16:41:53 +0100 Subject: [PATCH 11/17] chore: added missing semi-colon in docs --- docs/03x-rules-type.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/03x-rules-type.md b/docs/03x-rules-type.md index a4ba140..977f3c5 100644 --- a/docs/03x-rules-type.md +++ b/docs/03x-rules-type.md @@ -9,7 +9,7 @@ For example, if `['alpha', 'numeric']` is provided, it will validate if the valu Type( string|array $constraint, string $message = 'The "{{ name }}" value should be of type "{{ constraint }}", "{{ value }}" given.' -) +); ``` ## Basic Usage From e99c9956da012259663723e6c1b54aa2307f8748 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 16:46:24 +0100 Subject: [PATCH 12/17] chore: improved Choice docs --- docs/03x-rules-choice.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/03x-rules-choice.md b/docs/03x-rules-choice.md index 41e78c5..5339b3b 100644 --- a/docs/03x-rules-choice.md +++ b/docs/03x-rules-choice.md @@ -40,7 +40,7 @@ Validator::choice(['red', 'green', 'blue'], multiple: true, minConstraint: 2, ma ``` > **Note** -> An `UnexpectedValueException` will be thrown when `multiple` is `true` and value to be validated is not an `array`. +> An `UnexpectedValueException` will be thrown when `multiple` is `true` and the input value is not an `array`. > **Note** > An `UnexpectedValueException` will be thrown when the `minConstraint` value is greater than or equal to the `maxConstraint` value. From d3ce67209aab887de582c28f7cb55605e9e05cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 16:56:23 +0100 Subject: [PATCH 13/17] chore: changed constant names --- src/Rule/Country.php | 14 +++++++------- tests/CountryTest.php | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Rule/Country.php b/src/Rule/Country.php index 5dcaf0f..b875c89 100644 --- a/src/Rule/Country.php +++ b/src/Rule/Country.php @@ -8,16 +8,16 @@ class Country extends AbstractRule implements RuleInterface { - public const ALPHA2_CODE = 'alpha2'; - public const ALPHA3_CODE = 'alpha3'; + public const ALPHA_2_CODE = 'alpha-2'; + public const ALPHA_3_CODE = 'alpha-3'; private const CODE_OPTIONS = [ - self::ALPHA2_CODE, - self::ALPHA3_CODE + self::ALPHA_2_CODE, + self::ALPHA_3_CODE ]; public function __construct( - private readonly string $code = self::ALPHA2_CODE, + private readonly string $code = self::ALPHA_2_CODE, private readonly string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.' ) {} @@ -43,8 +43,8 @@ public function assert(mixed $value, string $name): void $input = strtoupper($value); if ( - ($this->code === self::ALPHA2_CODE && !Countries::exists($input)) - || ($this->code === self::ALPHA3_CODE && !Countries::alpha3CodeExists($input)) + ($this->code === self::ALPHA_2_CODE && !Countries::exists($input)) + || ($this->code === self::ALPHA_3_CODE && !Countries::alpha3CodeExists($input)) ) { throw new CountryException( message: $this->message, diff --git a/tests/CountryTest.php b/tests/CountryTest.php index 62bb910..bc8965d 100644 --- a/tests/CountryTest.php +++ b/tests/CountryTest.php @@ -31,17 +31,17 @@ public static function provideRuleFailureConditionData(): \Generator $message = '/The "(.*)" value is not a valid country code, "(.*)" given./'; yield 'default' => [new Country(), 'PRT', $exception, $message]; - yield 'alpha2' => [new Country(code: 'alpha2'), 'PRT', $exception, $message]; - yield 'alpha3' => [new Country(code: 'alpha3'), 'PT', $exception, $message]; + yield 'alpha2' => [new Country(code: 'alpha-2'), 'PRT', $exception, $message]; + yield 'alpha3' => [new Country(code: 'alpha-3'), 'PT', $exception, $message]; } public static function provideRuleSuccessConditionData(): \Generator { yield 'default' => [new Country(), 'PT']; - yield 'alpha2' => [new Country(code: 'alpha2'), 'PT']; - yield 'alpha2 lowercase' => [new Country(code: 'alpha2'), 'pt']; - yield 'alpha3' => [new Country(code: 'alpha3'), 'PRT']; - yield 'alpha3 lowercase' => [new Country(code: 'alpha3'), 'prt']; + yield 'alpha2' => [new Country(code: 'alpha-2'), 'PT']; + yield 'alpha2 lowercase' => [new Country(code: 'alpha-2'), 'pt']; + yield 'alpha3' => [new Country(code: 'alpha-3'), 'PRT']; + yield 'alpha3 lowercase' => [new Country(code: 'alpha-3'), 'prt']; } public static function provideRuleMessageOptionData(): \Generator @@ -51,7 +51,7 @@ public static function provideRuleMessageOptionData(): \Generator message: 'The "{{ name }}" value "{{ value }}" is not a valid "{{ code }}" country code.' ), 'invalid', - 'The "test" value "invalid" is not a valid "alpha2" country code.' + 'The "test" value "invalid" is not a valid "alpha-2" country code.' ]; } } \ No newline at end of file From 95d1f761754022e6817abcec9815f92784904987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 16:56:40 +0100 Subject: [PATCH 14/17] feat: added Country rule documentation --- docs/03x-rules-country.md | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/03x-rules-country.md diff --git a/docs/03x-rules-country.md b/docs/03x-rules-country.md new file mode 100644 index 0000000..ac24076 --- /dev/null +++ b/docs/03x-rules-country.md @@ -0,0 +1,55 @@ +# Country + +Validates that a value is a valid country code. + +```php +Country( + string $code = 'alpha-2', + string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.' +); +``` + +## Basic Usage + +```php +// Default alpha-2 code +Validator::country()->validate('PT'); // true +Validator::country(code: 'alpha-2')->validate('PT'); // true + +// Alpha-3 code +Validator::country(code: 'alpha-3')->validate('PRT'); // true +``` + +> **Note** +> An `UnexpectedValueException` will be thrown when the `code` value is not a valid option. + +> **Note** +> An `UnexpectedValueException` will be thrown when the input value is not a `string`. + +## Options + +### `code` + +type: `string` default: `alpha-2` + +Set code type to validate the country. +Check the [official country codes](https://en.wikipedia.org/wiki/ISO_3166-1#Current_codes) list for more information. + +Available options: + +- `alpha-2`: two-letter code +- `alpha-3`: three-letter code + +### `message` + +type `string` default: `The "{{ name }}" value is not a valid country code, "{{ value }}" given.` + +Message that will be shown if the input value is not a valid country code. + +The following parameters are available: + +| Parameter | Description | +|---------------|---------------------------| +| `{{ value }}` | The current invalid value | +| `{{ name }}` | Name of the invalid value | +| `{{ code }}` | Selected code type | \ No newline at end of file From d1c75d5ddf7943a143f4ae0f99a1ca977ea3557d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 17:01:43 +0100 Subject: [PATCH 15/17] chore: small docs fix --- docs/03x-rules-country.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/03x-rules-country.md b/docs/03x-rules-country.md index ac24076..a65858e 100644 --- a/docs/03x-rules-country.md +++ b/docs/03x-rules-country.md @@ -42,7 +42,7 @@ Available options: ### `message` -type `string` default: `The "{{ name }}" value is not a valid country code, "{{ value }}" given.` +type: `string` default: `The "{{ name }}" value is not a valid country code, "{{ value }}" given.` Message that will be shown if the input value is not a valid country code. From a3338c2532cb95a248f475b9e20a3be09bc1d298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 17:06:58 +0100 Subject: [PATCH 16/17] chore: added Country rule to list page --- docs/03-rules.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/03-rules.md b/docs/03-rules.md index 2db5910..664f28c 100644 --- a/docs/03-rules.md +++ b/docs/03-rules.md @@ -21,6 +21,7 @@ ## Choice Rules - [Choice](03x-rules-choice.md) +- [Country](03x-rules-country.md) ## Other Rules From 6aef7647438c3e420d2989cbede74ffa73215e77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=CC=81=20Pimpa=CC=83o?= Date: Wed, 16 Aug 2023 17:10:39 +0100 Subject: [PATCH 17/17] chore: added Country rule to interfaces --- src/ChainedValidatorInterface.php | 5 +++++ src/StaticValidatorInterface.php | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/ChainedValidatorInterface.php b/src/ChainedValidatorInterface.php index 4bfff66..283d31b 100644 --- a/src/ChainedValidatorInterface.php +++ b/src/ChainedValidatorInterface.php @@ -32,6 +32,11 @@ public function choice( string $maxMessage = 'The "{{ name }}" value must have at most {{ maxConstraint }} choices, {{ numValues }} choices given.' ): ChainedValidatorInterface; + public function country( + string $code = 'alpha-2', + string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.' + ): ChainedValidatorInterface; + public function greaterThan( mixed $constraint, string $message = 'The "{{ name }}" value should be greater than "{{ constraint }}", "{{ value }}" given.' diff --git a/src/StaticValidatorInterface.php b/src/StaticValidatorInterface.php index d216f95..6cd8298 100644 --- a/src/StaticValidatorInterface.php +++ b/src/StaticValidatorInterface.php @@ -22,6 +22,11 @@ public static function choice( string $maxMessage = 'The "{{ name }}" value must have at most {{ maxConstraint }} choices, {{ numValues }} choices given.' ): ChainedValidatorInterface; + public static function country( + string $code = 'alpha-2', + string $message = 'The "{{ name }}" value is not a valid country code, "{{ value }}" given.' + ): ChainedValidatorInterface; + public static function greaterThan( mixed $constraint, string $message = 'The "{{ name }}" value should be greater than "{{ constraint }}", "{{ value }}" given.'