diff --git a/composer.json b/composer.json index 19cc1ac..452bc77 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "programmatordev/yet-another-php-validator", "description": "PHP validator with expressive error messages", "type": "library", - "keywords": ["PHP", "PHP8", "Validator", "Validation"], + "keywords": ["php", "php8", "validator", "validation"], "license": "MIT", "authors": [ { @@ -14,13 +14,14 @@ "require": { "php": ">=8.1", "egulias/email-validator": "^4.0", - "symfony/intl": "^6.3", + "symfony/intl": "^6.4", "symfony/polyfill-ctype": "^1.27", - "symfony/polyfill-intl-grapheme": "^1.29" + "symfony/polyfill-intl-grapheme": "^1.29", + "symfony/polyfill-intl-icu": "^1.29" }, "require-dev": { "phpunit/phpunit": "^10.0", - "symfony/var-dumper": "^6.3" + "symfony/var-dumper": "^6.4" }, "autoload": { "psr-4": { diff --git a/docs/03-rules.md b/docs/03-rules.md index 944b879..f127121 100644 --- a/docs/03-rules.md +++ b/docs/03-rules.md @@ -40,6 +40,7 @@ - [Choice](03-rules_choice.md) - [Country](03-rules_country.md) - [Language](03-rules_language.md) +- [Locale](03-rules_locale.md) ## Iterable Rules diff --git a/docs/03-rules_locale.md b/docs/03-rules_locale.md new file mode 100644 index 0000000..6c76743 --- /dev/null +++ b/docs/03-rules_locale.md @@ -0,0 +1,56 @@ +# Locale + +Validates that a value is a valid locale code. + +```php +Locale( + bool $canonicalize = false, + ?string $message = null +); +``` + +## Basic Usage + +```php +// by default, code should be the language code (pt, en, ...) +// or the language code followed by an underscore and the uppercased country code (pt_PT, en_US, ...) +Validator::locale()->validate('pt'); // true +Validator::locale()->validate('pt_PT'); // true +Validator::locale()->validate('pt_pt'); // false +Validator::locale()->validate('pt-PT'); // false +Validator::locale()->validate('pt_PRT'); // false + +// canonicalize value before validation +Validator::language(canonicalize: true)->validate('pt_pt'); // true +Validator::language(canonicalize: true)->validate('pt-PT'); // true +Validator::language(canonicalize: true)->validate('pt_PRT'); // true +Validator::language(canonicalize: true)->validate('PT-pt.utf8'); // true +``` + +> [!NOTE] +> An `UnexpectedValueException` will be thrown when the input value is not a `string`. + +## Options + +### `canonicalize` + +type: `bool` default: `false` + +If `true`, the input value will be normalized before validation, according to the following [documentation](https://unicode-org.github.io/icu/userguide/locale/#canonicalization). + +### `message` + +type: `?string` default: `The {{ name }} value is not a valid locale, {{ value }} given.` + +Message that will be shown if the input value is not a valid locale code. + +The following parameters are available: + +| Parameter | Description | +|---------------|---------------------------| +| `{{ value }}` | The current invalid value | +| `{{ name }}` | Name of the invalid value | + +## Changelog + +- `1.1.0` Created \ No newline at end of file diff --git a/src/ChainedValidatorInterface.php b/src/ChainedValidatorInterface.php index 10995de..c6c11bc 100644 --- a/src/ChainedValidatorInterface.php +++ b/src/ChainedValidatorInterface.php @@ -97,6 +97,11 @@ public function lessThanOrEqual( ?string $message = null ): ChainedValidatorInterface&Validator; + public function locale( + bool $canonicalize = false, + ?string $message = null + ): ChainedValidatorInterface&Validator; + public function notBlank( ?callable $normalizer = null, ?string $message = null diff --git a/src/Exception/LocaleException.php b/src/Exception/LocaleException.php new file mode 100644 index 0000000..20531af --- /dev/null +++ b/src/Exception/LocaleException.php @@ -0,0 +1,5 @@ +message = $message ?? $this->message; + } + + public function assert(mixed $value, ?string $name = null): void + { + if (!\is_string($value)) { + throw new UnexpectedTypeException('string', get_debug_type($value)); + } + + // keep original value for parameters + $input = $value; + + if ($this->canonicalize) { + $input = \Locale::canonicalize($input); + } + + if (!Locales::exists($input)) { + throw new LocaleException( + message: $this->message, + parameters: [ + 'name' => $name, + 'value' => $value + ] + ); + } + } +} \ No newline at end of file diff --git a/src/StaticValidatorInterface.php b/src/StaticValidatorInterface.php index 154db90..74d0992 100644 --- a/src/StaticValidatorInterface.php +++ b/src/StaticValidatorInterface.php @@ -96,6 +96,11 @@ public static function lessThanOrEqual( ?string $message = null ): ChainedValidatorInterface&Validator; + public static function locale( + bool $canonicalize = false, + ?string $message = null + ): ChainedValidatorInterface&Validator; + public static function notBlank( ?callable $normalizer = null, ?string $message = null diff --git a/tests/LocaleTest.php b/tests/LocaleTest.php new file mode 100644 index 0000000..932fe1f --- /dev/null +++ b/tests/LocaleTest.php @@ -0,0 +1,56 @@ + [new Locale(), 123, $unexpectedTypeMessage]; + } + + public static function provideRuleFailureConditionData(): \Generator + { + $exception = LocaleException::class; + $message = '/The (.*) value is not a valid locale, (.*) given\./'; + + yield 'invalid' => [new Locale(), 'invalid', $exception, $message]; + yield 'uncanonicalized 1' => [new Locale(), 'pt_pt', $exception, $message]; + yield 'uncanonicalized 2' => [new Locale(), 'pt-PT', $exception, $message]; + yield 'uncanonicalized 3' => [new Locale(), 'PT-pt.utf8', $exception, $message]; + } + + public static function provideRuleSuccessConditionData(): \Generator + { + yield 'language code' => [new Locale(), 'pt']; + yield 'language and country code' => [new Locale(), 'pt_PT']; + yield 'canonicalized 1' => [new Locale(canonicalize: true), 'pt_pt']; + yield 'canonicalized 2' => [new Locale(canonicalize: true), 'pt-PT']; + yield 'canonicalized 3' => [new Locale(canonicalize: true), 'PT-pt.utf8']; + } + + public static function provideRuleMessageOptionData(): \Generator + { + yield 'message' => [ + new Locale( + message: '{{ name }} {{ value }}' + ), + 'invalid', + 'test "invalid"' + ]; + } +} \ No newline at end of file