From 5df530a4d59e007bcfc1163fb6c26363180e4391 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:45:20 +0300 Subject: [PATCH 01/22] Update rector/rector requirement from `1.0.*` to `^1.2` (#725) Co-authored-by: Sergei Predvoditelev --- composer.json | 2 +- rector.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 2075f03a0..f9f8c3a41 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ "maglnet/composer-require-checker": "^4.3", "phpbench/phpbench": "^1.2", "phpunit/phpunit": "^9.5", - "rector/rector": "1.0.*", + "rector/rector": "^1.2", "roave/infection-static-analysis-plugin": "^1.25", "spatie/phpunit-watcher": "^1.23", "vimeo/psalm": "^4.30|^5.22", diff --git a/rector.php b/rector.php index 33c9c8517..f55daaed4 100644 --- a/rector.php +++ b/rector.php @@ -6,7 +6,6 @@ use Rector\Config\RectorConfig; use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector; use Rector\Set\ValueObject\LevelSetList; -use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; return static function (RectorConfig $rectorConfig): void { $rectorConfig->paths([ @@ -24,6 +23,5 @@ $rectorConfig->skip([ ClosureToArrowFunctionRector::class, - ReadOnlyPropertyRector::class, ]); }; From 295614e60a44aa2549191682a42ba19c95798015 Mon Sep 17 00:00:00 2001 From: Alexey Rogachev Date: Tue, 23 Jul 2024 11:00:53 +0500 Subject: [PATCH 02/22] Fix Psalm (#728) --- composer.json | 2 +- src/Rule/AbstractCompare.php | 4 ++++ src/Rule/AbstractNumber.php | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f9f8c3a41..75835a5e3 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "rector/rector": "^1.2", "roave/infection-static-analysis-plugin": "^1.25", "spatie/phpunit-watcher": "^1.23", - "vimeo/psalm": "^4.30|^5.22", + "vimeo/psalm": "^4.30|^5.25", "yiisoft/di": "^1.2", "yiisoft/test-support": "^3.0", "yiisoft/translator-message-php": "^1.1", diff --git a/src/Rule/AbstractCompare.php b/src/Rule/AbstractCompare.php index 3303cbea4..d34a6d41d 100644 --- a/src/Rule/AbstractCompare.php +++ b/src/Rule/AbstractCompare.php @@ -46,11 +46,13 @@ abstract class AbstractCompare implements /** * A default for {@see $incorrectInputMessage}. + * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ protected const DEFAULT_INCORRECT_INPUT_MESSAGE = 'The allowed types are integer, float, string, boolean, null ' . 'and object implementing \Stringable interface or \DateTimeInterface.'; /** * A default for {@see $incorrectDataSetTypeMessage}. + * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ protected const DEFAULT_INCORRECT_DATA_SET_TYPE_MESSAGE = 'The attribute value returned from a custom data set ' . 'must have one of the following types: integer, float, string, boolean, null or an object implementing ' . @@ -59,10 +61,12 @@ abstract class AbstractCompare implements * List of valid types. * * @see CompareType + * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ private const VALID_TYPES = [CompareType::ORIGINAL, CompareType::STRING, CompareType::NUMBER]; /** * Map of valid operators. It's used instead of a list for better performance. + * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ private const VALID_OPERATORS_MAP = [ '==' => 1, diff --git a/src/Rule/AbstractNumber.php b/src/Rule/AbstractNumber.php index 1bd260626..fc17055fe 100644 --- a/src/Rule/AbstractNumber.php +++ b/src/Rule/AbstractNumber.php @@ -38,14 +38,17 @@ abstract class AbstractNumber implements /** * A default for {@see $incorrectInputMessage}. + * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ protected const DEFAULT_INCORRECT_INPUT_MESSAGE = 'The allowed types are integer, float and string.'; /** * A default for {@see $lessThanMinMessage}. + * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ protected const DEFAULT_LESS_THAN_MIN_MESSAGE = 'Value must be no less than {min}.'; /** * A default for {@see $greaterThanMaxMessage}. + * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ protected const DEFAULT_GREATER_THAN_MAX_MESSAGE = 'Value must be no greater than {max}.'; From e49548399b8e12dc21335f8f899d462375cd1aef Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 26 Jul 2024 14:23:59 +0300 Subject: [PATCH 03/22] Add backend enum support to In() rule --- CHANGELOG.md | 2 +- src/Rule/In.php | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a2a8128c..72d62afa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.4.2 under development -- no changes in this release. +- Enh #xxx: Add backend enum support to In() rule (@samdark) ## 1.4.1 June 11, 2024 diff --git a/src/Rule/In.php b/src/Rule/In.php index dc88017ac..8728cc3fa 100644 --- a/src/Rule/In.php +++ b/src/Rule/In.php @@ -5,7 +5,9 @@ namespace Yiisoft\Validator\Rule; use Attribute; +use BackedEnum; use Closure; +use UnitEnum; use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait; use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait; use Yiisoft\Validator\Rule\Trait\WhenTrait; @@ -39,8 +41,8 @@ final class In implements RuleWithOptionsInterface, SkipOnErrorInterface, WhenIn use WhenTrait; /** - * @param iterable $values A set of values to check against. Nested arrays are supported too (the order of values in - * lists must match, the order of keys in associative arrays is not important). + * @param iterable|string $values A set of values to check against. Enum class names and nested arrays are + * supported too (the order of values in lists must match, the order of keys in associative arrays is not important). * @param bool $strict Whether the comparison to each value in the set is strict: * * - Strict mode uses `===` operator meaning the type and the value must both match. @@ -70,7 +72,7 @@ final class In implements RuleWithOptionsInterface, SkipOnErrorInterface, WhenIn * @psalm-param WhenType $when */ public function __construct( - private iterable $values, + private iterable|string $values, private bool $strict = false, private bool $not = false, private string $message = 'This value is not in the list of acceptable values.', @@ -92,6 +94,10 @@ public function getName(): string */ public function getValues(): iterable { + if (is_string($this->values) && is_subclass_of($this->values, BackedEnum::class)) { + return array_column($this->values::cases(), 'value'); + } + return $this->values; } From 52319088bb7d201ca01f968a50b1a4e9a60d6bc6 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Fri, 26 Jul 2024 11:24:26 +0000 Subject: [PATCH 04/22] Apply fixes from StyleCI --- src/Rule/In.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Rule/In.php b/src/Rule/In.php index 8728cc3fa..122f6cc85 100644 --- a/src/Rule/In.php +++ b/src/Rule/In.php @@ -7,7 +7,6 @@ use Attribute; use BackedEnum; use Closure; -use UnitEnum; use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait; use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait; use Yiisoft\Validator\Rule\Trait\WhenTrait; From 93c48f59e555f0d6534c95d9eec85e5b84eb7939 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 26 Jul 2024 14:34:33 +0300 Subject: [PATCH 05/22] Add number to changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72d62afa5..0d190cb96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.4.2 under development -- Enh #xxx: Add backend enum support to In() rule (@samdark) +- Enh #734: Add backend enum support to In() rule (@samdark) ## 1.4.1 June 11, 2024 From 6e25127fb35ad16fd3a0c660a84f45b7b07dc3f7 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Fri, 26 Jul 2024 23:54:27 +0300 Subject: [PATCH 06/22] Use separate InEmum rule --- CHANGELOG.md | 2 +- docs/guide/en/built-in-rules.md | 1 + src/Rule/In.php | 24 ++- src/Rule/InEnum.php | 160 +++++++++++++++++++ src/Rule/InEnumHandler.php | 33 ++++ tests/Rule/InEnumTest.php | 141 ++++++++++++++++ tests/Support/Data/Enum/BackedEnumStatus.php | 9 ++ tests/Support/Data/Enum/EnumStatus.php | 9 ++ 8 files changed, 364 insertions(+), 15 deletions(-) create mode 100644 src/Rule/InEnum.php create mode 100644 src/Rule/InEnumHandler.php create mode 100644 tests/Rule/InEnumTest.php create mode 100644 tests/Support/Data/Enum/BackedEnumStatus.php create mode 100644 tests/Support/Data/Enum/EnumStatus.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d190cb96..0baefbd2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 1.4.2 under development -- Enh #734: Add backend enum support to In() rule (@samdark) +- Enh #734: Add enum support (@samdark) ## 1.4.1 June 11, 2024 diff --git a/docs/guide/en/built-in-rules.md b/docs/guide/en/built-in-rules.md index 2b5bd73cb..52b182b63 100644 --- a/docs/guide/en/built-in-rules.md +++ b/docs/guide/en/built-in-rules.md @@ -44,6 +44,7 @@ Here is a list of all available built-in rules, divided by category. ### Set rules - [In](../../../src/Rule/In.php) +- [InEnum](../../../src/Rule/InEnum.php) - [Subset](../../../src/Rule/Subset.php) - [UniqueIterable](../../../src/Rule/UniqueIterable.php) diff --git a/src/Rule/In.php b/src/Rule/In.php index 122f6cc85..3d7100005 100644 --- a/src/Rule/In.php +++ b/src/Rule/In.php @@ -5,12 +5,11 @@ namespace Yiisoft\Validator\Rule; use Attribute; -use BackedEnum; use Closure; +use Yiisoft\Validator\DumpedRuleInterface; use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait; use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait; use Yiisoft\Validator\Rule\Trait\WhenTrait; -use Yiisoft\Validator\RuleWithOptionsInterface; use Yiisoft\Validator\SkipOnEmptyInterface; use Yiisoft\Validator\SkipOnErrorInterface; use Yiisoft\Validator\WhenInterface; @@ -33,15 +32,15 @@ * @psalm-import-type WhenType from WhenInterface */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class In implements RuleWithOptionsInterface, SkipOnErrorInterface, WhenInterface, SkipOnEmptyInterface +final class In implements DumpedRuleInterface, SkipOnErrorInterface, WhenInterface, SkipOnEmptyInterface { use SkipOnEmptyTrait; use SkipOnErrorTrait; use WhenTrait; /** - * @param iterable|string $values A set of values to check against. Enum class names and nested arrays are - * supported too (the order of values in lists must match, the order of keys in associative arrays is not important). + * @param iterable $values A set of values to check against. Nested arrays are supported too (the order of values in + * lists must match, the order of keys in associative arrays is not important). * @param bool $strict Whether the comparison to each value in the set is strict: * * - Strict mode uses `===` operator meaning the type and the value must both match. @@ -59,7 +58,7 @@ final class In implements RuleWithOptionsInterface, SkipOnErrorInterface, WhenIn * * You may use the following placeholders in the message: * - * - `{attribute}`: the name of the attribute. + * - `{property}`: the name of the property. * @param bool|callable|null $skipOnEmpty Whether to skip this rule if the value validated is empty. * See {@see SkipOnEmptyInterface}. * @param bool $skipOnError Whether to skip this rule if any of the previous rules gave an error. @@ -71,19 +70,20 @@ final class In implements RuleWithOptionsInterface, SkipOnErrorInterface, WhenIn * @psalm-param WhenType $when */ public function __construct( - private iterable|string $values, + private iterable $values, private bool $strict = false, private bool $not = false, - private string $message = 'This value is not in the list of acceptable values.', - private mixed $skipOnEmpty = null, + private string $message = '{Property} is not in the list of acceptable values.', + bool|callable|null $skipOnEmpty = null, private bool $skipOnError = false, private Closure|null $when = null, ) { + $this->skipOnEmpty = $skipOnEmpty; } public function getName(): string { - return 'inRange'; + return self::class; } /** @@ -93,10 +93,6 @@ public function getName(): string */ public function getValues(): iterable { - if (is_string($this->values) && is_subclass_of($this->values, BackedEnum::class)) { - return array_column($this->values::cases(), 'value'); - } - return $this->values; } diff --git a/src/Rule/InEnum.php b/src/Rule/InEnum.php new file mode 100644 index 000000000..41ea75ef9 --- /dev/null +++ b/src/Rule/InEnum.php @@ -0,0 +1,160 @@ +class) || !is_subclass_of($this->class, UnitEnum::class)) { + throw new InvalidArgumentException( + sprintf('Class should be an enum class string, %s provided.', get_debug_type($this->class)) + ); + } + } + + public function getName(): string + { + return 'inEnum'; + } + + /** + * Get a set of values to check against. + * + * @return iterable A set of values. + */ + public function getValues(): iterable + { + if (is_subclass_of($this->class, BackedEnum::class) && !$this->useNames) { + return array_column($this->class::cases(), 'value'); + } + + return array_column($this->class::cases(), 'name'); + } + + /** + * Whether the comparison is strict (both type and value must be the same). + * + * @return bool Whether the comparison is strict. + */ + public function isStrict(): bool + { + return $this->strict; + } + + /** + * Whether to invert the validation logic. Defaults to `false`. If set to `true`, the value must NOT + * be among the list of {@see $values}. + * + * @return bool Whether to invert the validation logic. + */ + public function isNot(): bool + { + return $this->not; + } + + /** + * Get error message when the value is not in a set of {@see $values}. + * + * @return string Error message. + */ + public function getMessage(): string + { + return $this->message; + } + + public function getOptions(): array + { + return [ + 'values' => $this->getValues(), + 'strict' => $this->strict, + 'not' => $this->not, + 'message' => [ + 'template' => $this->message, + 'parameters' => [], + ], + 'skipOnEmpty' => $this->getSkipOnEmptyOption(), + 'skipOnError' => $this->skipOnError, + ]; + } + + public function getHandler(): string + { + return InEnumHandler::class; + } +} diff --git a/src/Rule/InEnumHandler.php b/src/Rule/InEnumHandler.php new file mode 100644 index 000000000..2cf2bd54b --- /dev/null +++ b/src/Rule/InEnumHandler.php @@ -0,0 +1,33 @@ +isNot() === ArrayHelper::isIn($value, $rule->getValues(), $rule->isStrict())) { + $result->addError($rule->getMessage(), ['attribute' => $context->getTranslatedAttribute()]); + } + + return $result; + } +} diff --git a/tests/Rule/InEnumTest.php b/tests/Rule/InEnumTest.php new file mode 100644 index 000000000..36e495f16 --- /dev/null +++ b/tests/Rule/InEnumTest.php @@ -0,0 +1,141 @@ +assertSame('inEnum', $rule->getName()); + } + + public function dataOptions(): array + { + $values = array_column(EnumStatus::class::cases(), 'name'); + + return [ + [ + new InEnum(EnumStatus::class), + [ + 'values' => $values, + 'strict' => false, + 'not' => false, + 'message' => [ + 'template' => 'This value is not in the list of acceptable values.', + 'parameters' => [], + ], + 'skipOnEmpty' => false, + 'skipOnError' => false, + ], + ], + [ + new InEnum(EnumStatus::class, strict: true), + [ + 'values' => $values, + 'strict' => true, + 'not' => false, + 'message' => [ + 'template' => 'This value is not in the list of acceptable values.', + 'parameters' => [], + ], + 'skipOnEmpty' => false, + 'skipOnError' => false, + ], + ], + [ + new InEnum(EnumStatus::class, not: true), + [ + 'values' => $values, + 'strict' => false, + 'not' => true, + 'message' => [ + 'template' => 'This value is not in the list of acceptable values.', + 'parameters' => [], + ], + 'skipOnEmpty' => false, + 'skipOnError' => false, + ], + ], + ]; + } + + public function dataValidationPassed(): array + { + return [ + [1, [new In(range(1, 10))]], + [10, [new In(range(1, 10))]], + ['10', [new In(range(1, 10))]], + ['5', [new In(range(1, 10))]], + + [['a'], [new In([['a'], ['b']])]], + ['a', [new In(new ArrayObject(['a', 'b']))]], + + [1, [new In(range(1, 10), strict: true)]], + [5, [new In(range(1, 10), strict: true)]], + [10, [new In(range(1, 10), strict: true)]], + + [0, [new In(range(1, 10), not: true)]], + [11, [new In(range(1, 10), not: true)]], + [5.5, [new In(range(1, 10), not: true)]], + + 'arrays, simple case' => [[1, 2], [new In([[1, 2], [3, 4]])]], + 'arrays, non-strict equality (partially), non-strict mode' => [['1', 2], [new In([[1, 2], [3, 4]])]], + ]; + } + + public function dataValidationFailed(): array + { + $errors = ['' => ['This value is not in the list of acceptable values.']]; + + return [ + + + 'arrays, non-strict equality (partially), strict mode' => [ + ['1', 2], + [new In([[1, 2], [3, 4]], strict: true)], + $errors, + ], + 'arrays, non-strict equality (fully), strict mode' => [ + ['1', '2'], + [new In([[1, 2], [3, 4]], strict: true)], + $errors, + ], + ]; + } + + public function testSkipOnError(): void + { + $this->testSkipOnErrorInternal(new InEnum(EnumStatus::class), new InEnum(EnumStatus::class, skipOnError: true)); + } + + public function testWhen(): void + { + $when = static fn (mixed $value): bool => $value !== null; + $this->testWhenInternal(new InEnum(EnumStatus::class), new InEnum(EnumStatus::class, when: $when)); + } + + protected function getDifferentRuleInHandlerItems(): array + { + return [InEnum::class, InEnumHandler::class]; + } +} diff --git a/tests/Support/Data/Enum/BackedEnumStatus.php b/tests/Support/Data/Enum/BackedEnumStatus.php new file mode 100644 index 000000000..e6ee20a79 --- /dev/null +++ b/tests/Support/Data/Enum/BackedEnumStatus.php @@ -0,0 +1,9 @@ + Date: Fri, 26 Jul 2024 20:54:38 +0000 Subject: [PATCH 07/22] Apply fixes from StyleCI --- tests/Rule/InEnumTest.php | 3 --- tests/Support/Data/Enum/BackedEnumStatus.php | 2 ++ tests/Support/Data/Enum/EnumStatus.php | 2 ++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Rule/InEnumTest.php b/tests/Rule/InEnumTest.php index 36e495f16..d0cb4dd9a 100644 --- a/tests/Rule/InEnumTest.php +++ b/tests/Rule/InEnumTest.php @@ -8,7 +8,6 @@ use Yiisoft\Validator\Rule\In; use Yiisoft\Validator\Rule\InEnum; use Yiisoft\Validator\Rule\InEnumHandler; -use Yiisoft\Validator\Rule\InHandler; use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait; use Yiisoft\Validator\Tests\Rule\Base\RuleTestCase; use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait; @@ -108,8 +107,6 @@ public function dataValidationFailed(): array $errors = ['' => ['This value is not in the list of acceptable values.']]; return [ - - 'arrays, non-strict equality (partially), strict mode' => [ ['1', 2], [new In([[1, 2], [3, 4]], strict: true)], diff --git a/tests/Support/Data/Enum/BackedEnumStatus.php b/tests/Support/Data/Enum/BackedEnumStatus.php index e6ee20a79..0abaec280 100644 --- a/tests/Support/Data/Enum/BackedEnumStatus.php +++ b/tests/Support/Data/Enum/BackedEnumStatus.php @@ -1,5 +1,7 @@ Date: Sat, 27 Jul 2024 10:18:10 +0300 Subject: [PATCH 08/22] Fixes, expand tests --- src/Rule/InEnum.php | 12 +++++++----- src/Rule/InEnumHandler.php | 8 +++++++- tests/Rule/InEnumTest.php | 30 +++++++++++------------------- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/Rule/InEnum.php b/src/Rule/InEnum.php index 41ea75ef9..bd35c9879 100644 --- a/src/Rule/InEnum.php +++ b/src/Rule/InEnum.php @@ -12,7 +12,7 @@ use Yiisoft\Validator\Rule\Trait\SkipOnEmptyTrait; use Yiisoft\Validator\Rule\Trait\SkipOnErrorTrait; use Yiisoft\Validator\Rule\Trait\WhenTrait; -use Yiisoft\Validator\RuleWithOptionsInterface; +use Yiisoft\Validator\DumpedRuleInterface; use Yiisoft\Validator\SkipOnEmptyInterface; use Yiisoft\Validator\SkipOnErrorInterface; use Yiisoft\Validator\WhenInterface; @@ -34,7 +34,7 @@ * @psalm-import-type WhenType from WhenInterface */ #[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)] -final class InEnum implements RuleWithOptionsInterface, SkipOnErrorInterface, WhenInterface, SkipOnEmptyInterface +final class InEnum implements DumpedRuleInterface, SkipOnErrorInterface, WhenInterface, SkipOnEmptyInterface { use SkipOnEmptyTrait; use SkipOnErrorTrait; @@ -76,12 +76,14 @@ public function __construct( private bool $useNames = false, private bool $strict = false, private bool $not = false, - private string $message = 'This value is not in the list of acceptable values.', - private mixed $skipOnEmpty = null, + private string $message = '{Property} is not in the list of acceptable values.', + bool|callable|null $skipOnEmpty = null, private bool $skipOnError = false, private Closure|null $when = null, ) { - if (!is_string($this->class) || !is_subclass_of($this->class, UnitEnum::class)) { + $this->skipOnEmpty = $skipOnEmpty; + + if (!is_subclass_of($this->class, UnitEnum::class)) { throw new InvalidArgumentException( sprintf('Class should be an enum class string, %s provided.', get_debug_type($this->class)) ); diff --git a/src/Rule/InEnumHandler.php b/src/Rule/InEnumHandler.php index 2cf2bd54b..86a35393a 100644 --- a/src/Rule/InEnumHandler.php +++ b/src/Rule/InEnumHandler.php @@ -25,7 +25,13 @@ public function validate(mixed $value, object $rule, ValidationContext $context) $result = new Result(); if ($rule->isNot() === ArrayHelper::isIn($value, $rule->getValues(), $rule->isStrict())) { - $result->addError($rule->getMessage(), ['attribute' => $context->getTranslatedAttribute()]); + $result->addError( + $rule->getMessage(), + [ + 'property' => $context->getTranslatedProperty(), + 'Property' => $context->getCapitalizedTranslatedProperty(), + ], + ); } return $result; diff --git a/tests/Rule/InEnumTest.php b/tests/Rule/InEnumTest.php index d0cb4dd9a..6a86372b7 100644 --- a/tests/Rule/InEnumTest.php +++ b/tests/Rule/InEnumTest.php @@ -13,6 +13,7 @@ use Yiisoft\Validator\Tests\Rule\Base\RuleWithOptionsTestTrait; use Yiisoft\Validator\Tests\Rule\Base\SkipOnErrorTestTrait; use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait; +use Yiisoft\Validator\Tests\Support\Data\Enum\BackedEnumStatus; use Yiisoft\Validator\Tests\Support\Data\Enum\EnumStatus; final class InEnumTest extends RuleTestCase @@ -40,7 +41,7 @@ public function dataOptions(): array 'strict' => false, 'not' => false, 'message' => [ - 'template' => 'This value is not in the list of acceptable values.', + 'template' => '{Property} is not in the list of acceptable values.', 'parameters' => [], ], 'skipOnEmpty' => false, @@ -54,7 +55,7 @@ public function dataOptions(): array 'strict' => true, 'not' => false, 'message' => [ - 'template' => 'This value is not in the list of acceptable values.', + 'template' => '{Property} is not in the list of acceptable values.', 'parameters' => [], ], 'skipOnEmpty' => false, @@ -68,7 +69,7 @@ public function dataOptions(): array 'strict' => false, 'not' => true, 'message' => [ - 'template' => 'This value is not in the list of acceptable values.', + 'template' => '{Property} is not in the list of acceptable values.', 'parameters' => [], ], 'skipOnEmpty' => false, @@ -81,30 +82,21 @@ public function dataOptions(): array public function dataValidationPassed(): array { return [ - [1, [new In(range(1, 10))]], - [10, [new In(range(1, 10))]], - ['10', [new In(range(1, 10))]], - ['5', [new In(range(1, 10))]], + ['DRAFT', [new InEnum(EnumStatus::class)]], + ['PUBLISHED', [new InEnum(EnumStatus::class)]], - [['a'], [new In([['a'], ['b']])]], - ['a', [new In(new ArrayObject(['a', 'b']))]], + ['DRAFT', [new InEnum(BackedEnumStatus::class, useNames: true)]], + ['PUBLISHED', [new InEnum(BackedEnumStatus::class, useNames: true)]], - [1, [new In(range(1, 10), strict: true)]], - [5, [new In(range(1, 10), strict: true)]], - [10, [new In(range(1, 10), strict: true)]], - [0, [new In(range(1, 10), not: true)]], - [11, [new In(range(1, 10), not: true)]], - [5.5, [new In(range(1, 10), not: true)]], - - 'arrays, simple case' => [[1, 2], [new In([[1, 2], [3, 4]])]], - 'arrays, non-strict equality (partially), non-strict mode' => [['1', 2], [new In([[1, 2], [3, 4]])]], + ['draft', [new InEnum(BackedEnumStatus::class)]], + ['published', [new InEnum(BackedEnumStatus::class)]], ]; } public function dataValidationFailed(): array { - $errors = ['' => ['This value is not in the list of acceptable values.']]; + $errors = ['' => ['Value is not in the list of acceptable values.']]; return [ 'arrays, non-strict equality (partially), strict mode' => [ From ae6752c2d80040911ff5b9c73d9e6f8ea19544ac Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sat, 27 Jul 2024 07:18:33 +0000 Subject: [PATCH 09/22] Apply fixes from StyleCI --- tests/Rule/InEnumTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Rule/InEnumTest.php b/tests/Rule/InEnumTest.php index 6a86372b7..4fe146b8a 100644 --- a/tests/Rule/InEnumTest.php +++ b/tests/Rule/InEnumTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\Validator\Tests\Rule; -use ArrayObject; use Yiisoft\Validator\Rule\In; use Yiisoft\Validator\Rule\InEnum; use Yiisoft\Validator\Rule\InEnumHandler; From 06975b8b2a8cd522808cf6b4403b47194af6890a Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 27 Jul 2024 10:19:55 +0300 Subject: [PATCH 10/22] Remove unneeded changes --- rector.php | 2 ++ src/Rule/AbstractCompare.php | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/rector.php b/rector.php index f55daaed4..33c9c8517 100644 --- a/rector.php +++ b/rector.php @@ -6,6 +6,7 @@ use Rector\Config\RectorConfig; use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector; use Rector\Set\ValueObject\LevelSetList; +use Rector\Php81\Rector\Property\ReadOnlyPropertyRector; return static function (RectorConfig $rectorConfig): void { $rectorConfig->paths([ @@ -23,5 +24,6 @@ $rectorConfig->skip([ ClosureToArrowFunctionRector::class, + ReadOnlyPropertyRector::class, ]); }; diff --git a/src/Rule/AbstractCompare.php b/src/Rule/AbstractCompare.php index a876ea039..95525b2bd 100644 --- a/src/Rule/AbstractCompare.php +++ b/src/Rule/AbstractCompare.php @@ -46,13 +46,11 @@ abstract class AbstractCompare implements /** * A default for {@see $incorrectInputMessage}. - * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ protected const DEFAULT_INCORRECT_INPUT_MESSAGE = 'The allowed types are integer, float, string, boolean, null ' . 'and object implementing \Stringable interface or \DateTimeInterface.'; /** * A default for {@see $incorrectDataSetTypeMessage}. - * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ protected const DEFAULT_INCORRECT_DATA_SET_TYPE_MESSAGE = 'The property value returned from a custom data set ' . 'must have one of the following types: integer, float, string, boolean, null or an object implementing ' . @@ -61,12 +59,10 @@ abstract class AbstractCompare implements * List of valid types. * * @see CompareType - * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ private const VALID_TYPES = [CompareType::ORIGINAL, CompareType::STRING, CompareType::NUMBER]; /** * Map of valid operators. It's used instead of a list for better performance. - * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ private const VALID_OPERATORS_MAP = [ '==' => 1, From 23e77faefd92a09512853ed7a0a419dfb61436e2 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 27 Jul 2024 10:20:54 +0300 Subject: [PATCH 11/22] More unneeded changes --- src/Rule/AbstractNumber.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Rule/AbstractNumber.php b/src/Rule/AbstractNumber.php index 41a152dad..d11dbea48 100644 --- a/src/Rule/AbstractNumber.php +++ b/src/Rule/AbstractNumber.php @@ -38,17 +38,14 @@ abstract class AbstractNumber implements /** * A default for {@see $incorrectInputMessage}. - * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ protected const DEFAULT_INCORRECT_INPUT_MESSAGE = 'The allowed types are integer, float and string.'; /** * A default for {@see $lessThanMinMessage}. - * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ protected const DEFAULT_LESS_THAN_MIN_MESSAGE = '{Property} must be no less than {min}.'; /** * A default for {@see $greaterThanMaxMessage}. - * @psalm-suppress MissingClassConstType Add constant type after bump PHP version to 8.3. */ protected const DEFAULT_GREATER_THAN_MAX_MESSAGE = '{Property} must be no greater than {max}.'; From 2660334a36a7686cd133a611ca9aa45b5b8de384 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 27 Jul 2024 10:23:40 +0300 Subject: [PATCH 12/22] More tests --- tests/Rule/InEnumTest.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/Rule/InEnumTest.php b/tests/Rule/InEnumTest.php index 4fe146b8a..c782d28d2 100644 --- a/tests/Rule/InEnumTest.php +++ b/tests/Rule/InEnumTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\Validator\Tests\Rule; -use Yiisoft\Validator\Rule\In; use Yiisoft\Validator\Rule\InEnum; use Yiisoft\Validator\Rule\InEnumHandler; use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait; @@ -98,14 +97,19 @@ public function dataValidationFailed(): array $errors = ['' => ['Value is not in the list of acceptable values.']]; return [ - 'arrays, non-strict equality (partially), strict mode' => [ - ['1', 2], - [new In([[1, 2], [3, 4]], strict: true)], + [ + '42', + [new InEnum(EnumStatus::class)], $errors, ], - 'arrays, non-strict equality (fully), strict mode' => [ - ['1', '2'], - [new In([[1, 2], [3, 4]], strict: true)], + [ + 'DRAFT', + [new InEnum(BackedEnumStatus::class)], + $errors, + ], + [ + 'draft', + [new InEnum(BackedEnumStatus::class, useNames: true)], $errors, ], ]; From 51875935a7e9e58936c748b18a5c9753cdcb7c81 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 27 Jul 2024 10:30:52 +0300 Subject: [PATCH 13/22] Fix psalm --- src/Rule/InEnum.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Rule/InEnum.php b/src/Rule/InEnum.php index bd35c9879..4e0ffc2c1 100644 --- a/src/Rule/InEnum.php +++ b/src/Rule/InEnum.php @@ -106,7 +106,13 @@ public function getValues(): iterable return array_column($this->class::cases(), 'value'); } - return array_column($this->class::cases(), 'name'); + /** + * @psalm-suppress InvalidStringClass + * @psalm-var array $cases + */ + $cases = $this->class::cases(); + + return array_column($cases, 'name'); } /** From 9e3a24a4850d02cf188e8e3e78e6e0116de966f6 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 27 Jul 2024 11:02:10 +0300 Subject: [PATCH 14/22] Test for invalid class-string --- tests/Rule/InEnumTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/Rule/InEnumTest.php b/tests/Rule/InEnumTest.php index c782d28d2..57ee63708 100644 --- a/tests/Rule/InEnumTest.php +++ b/tests/Rule/InEnumTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\Validator\Tests\Rule; +use InvalidArgumentException; use Yiisoft\Validator\Rule\InEnum; use Yiisoft\Validator\Rule\InEnumHandler; use Yiisoft\Validator\Tests\Rule\Base\DifferentRuleInHandlerTestTrait; @@ -21,6 +22,12 @@ final class InEnumTest extends RuleTestCase use SkipOnErrorTestTrait; use WhenTestTrait; + public function testInvalidEnum(): void + { + $this->expectException(InvalidArgumentException::class); + new InEnum('test'); + } + public function testGetName(): void { $rule = new InEnum(EnumStatus::class); From f798521fb641f20ce76e3723c445861ed8700c09 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 28 Jul 2024 16:10:12 +0300 Subject: [PATCH 15/22] Extra tests --- tests/Rule/InEnumTest.php | 18 +++++++++++++++--- .../Support/Data/Enum/IntBackedEnumStatus.php | 11 +++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 tests/Support/Data/Enum/IntBackedEnumStatus.php diff --git a/tests/Rule/InEnumTest.php b/tests/Rule/InEnumTest.php index 57ee63708..fdc3aaeec 100644 --- a/tests/Rule/InEnumTest.php +++ b/tests/Rule/InEnumTest.php @@ -14,6 +14,7 @@ use Yiisoft\Validator\Tests\Rule\Base\WhenTestTrait; use Yiisoft\Validator\Tests\Support\Data\Enum\BackedEnumStatus; use Yiisoft\Validator\Tests\Support\Data\Enum\EnumStatus; +use Yiisoft\Validator\Tests\Support\Data\Enum\IntBackedEnumStatus; final class InEnumTest extends RuleTestCase { @@ -39,7 +40,7 @@ public function dataOptions(): array $values = array_column(EnumStatus::class::cases(), 'name'); return [ - [ + 'non-strict' => [ new InEnum(EnumStatus::class), [ 'values' => $values, @@ -53,7 +54,7 @@ public function dataOptions(): array 'skipOnError' => false, ], ], - [ + 'strict' => [ new InEnum(EnumStatus::class, strict: true), [ 'values' => $values, @@ -67,7 +68,7 @@ public function dataOptions(): array 'skipOnError' => false, ], ], - [ + 'not' => [ new InEnum(EnumStatus::class, not: true), [ 'values' => $values, @@ -96,6 +97,11 @@ public function dataValidationPassed(): array ['draft', [new InEnum(BackedEnumStatus::class)]], ['published', [new InEnum(BackedEnumStatus::class)]], + + [1, [new InEnum(IntBackedEnumStatus::class)]], + [2, [new InEnum(IntBackedEnumStatus::class)]], + ['1', [new InEnum(IntBackedEnumStatus::class)]], + ['2', [new InEnum(IntBackedEnumStatus::class)]], ]; } @@ -119,6 +125,12 @@ public function dataValidationFailed(): array [new InEnum(BackedEnumStatus::class, useNames: true)], $errors, ], + + [ + '1', + [new InEnum(IntBackedEnumStatus::class, strict: true)], + $errors, + ], ]; } diff --git a/tests/Support/Data/Enum/IntBackedEnumStatus.php b/tests/Support/Data/Enum/IntBackedEnumStatus.php new file mode 100644 index 000000000..6fcb3aa1a --- /dev/null +++ b/tests/Support/Data/Enum/IntBackedEnumStatus.php @@ -0,0 +1,11 @@ + Date: Sun, 28 Jul 2024 16:10:48 +0300 Subject: [PATCH 16/22] Adjust changelog type --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e6c962f..03ae7403d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.0.0 under development -- Enh #734: Add enum support (@samdark) +- New #734: Add enum support (@samdark) - New #630: Include attribute name in error messages when it's present (@dood-) - New #646, #653: Add `DateTime` rule (@pamparam83) - New #615: Add the `Each::PARAMETER_EACH_KEY` validation context parameter that available during `Each` rule handling From 0d893af4a0c60f5b13e5023336ff9b752143360e Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 28 Jul 2024 16:15:45 +0300 Subject: [PATCH 17/22] Adjustments --- src/Rule/InEnum.php | 16 +++++++--------- src/Rule/InEnumHandler.php | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Rule/InEnum.php b/src/Rule/InEnum.php index 4e0ffc2c1..5c3ed7ba4 100644 --- a/src/Rule/InEnum.php +++ b/src/Rule/InEnum.php @@ -25,9 +25,6 @@ * * In case of the validated value being a list, the order of values is important. * - * Nested arrays are supported in the validated value (the order of values in - * lists must match, the order of keys in associative arrays is not important). - * * @see InEnumHandler * * @psalm-import-type SkipOnEmptyValue from SkipOnEmptyInterface @@ -42,11 +39,11 @@ final class InEnum implements DumpedRuleInterface, SkipOnErrorInterface, WhenInt /** * @param string $class Class of the enum to user. - * @param bool $useNames Whether to use names for backend enums instead of value. + * @param bool $useNames Whether to use names for backed enums instead of value. * @param bool $strict Whether the comparison to each value in the set is strict: * - * - Strict mode uses `===` operator meaning the type and the value must both match. - * - Non-strict mode uses `==` operator meaning that type juggling is performed first before the comparison. You can + * - Strict mode meaning the type and the value must both match. + * - Non-strict mode meaning that type juggling is performed first before the comparison. You can * read more in the PHP docs: * * - {@link https://www.php.net/manual/en/language.operators.comparison.php} @@ -60,7 +57,8 @@ final class InEnum implements DumpedRuleInterface, SkipOnErrorInterface, WhenInt * * You may use the following placeholders in the message: * - * - `{attribute}`: the name of the attribute. + * - `{property}`: the name of the attribute. + * - `{Property}`: the capitalized name of the attribute. * @param bool|callable|null $skipOnEmpty Whether to skip this rule if the value validated is empty. * See {@see SkipOnEmptyInterface}. * @param bool $skipOnError Whether to skip this rule if any of the previous rules gave an error. @@ -98,9 +96,9 @@ public function getName(): string /** * Get a set of values to check against. * - * @return iterable A set of values. + * @return array A set of values. */ - public function getValues(): iterable + public function getValues(): array { if (is_subclass_of($this->class, BackedEnum::class) && !$this->useNames) { return array_column($this->class::cases(), 'value'); diff --git a/src/Rule/InEnumHandler.php b/src/Rule/InEnumHandler.php index 86a35393a..ba0535d10 100644 --- a/src/Rule/InEnumHandler.php +++ b/src/Rule/InEnumHandler.php @@ -24,7 +24,7 @@ public function validate(mixed $value, object $rule, ValidationContext $context) } $result = new Result(); - if ($rule->isNot() === ArrayHelper::isIn($value, $rule->getValues(), $rule->isStrict())) { + if ($rule->isNot() === in_array($value, $rule->getValues(), $rule->isStrict())) { $result->addError( $rule->getMessage(), [ From 908be1a5fcac3352dfc0b404588151c7fb2cada7 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sun, 28 Jul 2024 13:16:06 +0000 Subject: [PATCH 18/22] Apply fixes from StyleCI --- src/Rule/InEnumHandler.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Rule/InEnumHandler.php b/src/Rule/InEnumHandler.php index ba0535d10..fc8bc49d7 100644 --- a/src/Rule/InEnumHandler.php +++ b/src/Rule/InEnumHandler.php @@ -4,7 +4,6 @@ namespace Yiisoft\Validator\Rule; -use Yiisoft\Arrays\ArrayHelper; use Yiisoft\Validator\Exception\UnexpectedRuleException; use Yiisoft\Validator\Result; use Yiisoft\Validator\RuleHandlerInterface; From cd972447e9250a64d9e4777b85b4d05276b431eb Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 28 Jul 2024 16:17:03 +0300 Subject: [PATCH 19/22] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d714937..82d4d6d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,6 @@ ## 2.0.0 under development - New #734: Add enum support (@samdark) -- New #630: Include attribute name in error messages when it's present (@dood-) - New #630, #718: Include attribute name in error messages when it's present (@dood-, @arogachev) - New #646, #653: Add `DateTime` rule (@pamparam83) - New #615: Add the `Each::PARAMETER_EACH_KEY` validation context parameter that available during `Each` rule handling From 44af2736d062fdbbb46075d027b6ce2642dcdeed Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 28 Jul 2024 18:00:08 +0300 Subject: [PATCH 20/22] Update CHANGELOG.md Co-authored-by: Sergei Predvoditelev --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d4d6d0e..1bf3df7b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.0.0 under development -- New #734: Add enum support (@samdark) +- New #734: Add `InEnum` rule (@samdark) - New #630, #718: Include attribute name in error messages when it's present (@dood-, @arogachev) - New #646, #653: Add `DateTime` rule (@pamparam83) - New #615: Add the `Each::PARAMETER_EACH_KEY` validation context parameter that available during `Each` rule handling From e783f67d5aac1e2c211492e0198cc44bbb87b737 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 28 Jul 2024 18:00:46 +0300 Subject: [PATCH 21/22] Update src/Rule/InEnum.php Co-authored-by: Sergei Predvoditelev --- src/Rule/InEnum.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Rule/InEnum.php b/src/Rule/InEnum.php index 5c3ed7ba4..8f09dc9b7 100644 --- a/src/Rule/InEnum.php +++ b/src/Rule/InEnum.php @@ -23,8 +23,6 @@ * If the {@see In::$not} is set, the validation logic is inverted and the rule will ensure that the value * is NOT one of them. * - * In case of the validated value being a list, the order of values is important. - * * @see InEnumHandler * * @psalm-import-type SkipOnEmptyValue from SkipOnEmptyInterface From 3004b87a9e1a4b4af72dfea0ef75ee04160f1c5f Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sun, 28 Jul 2024 18:26:43 +0300 Subject: [PATCH 22/22] Add test for capitalized parameters --- tests/Rule/InEnumTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Rule/InEnumTest.php b/tests/Rule/InEnumTest.php index fdc3aaeec..a3127622a 100644 --- a/tests/Rule/InEnumTest.php +++ b/tests/Rule/InEnumTest.php @@ -15,6 +15,7 @@ use Yiisoft\Validator\Tests\Support\Data\Enum\BackedEnumStatus; use Yiisoft\Validator\Tests\Support\Data\Enum\EnumStatus; use Yiisoft\Validator\Tests\Support\Data\Enum\IntBackedEnumStatus; +use Yiisoft\Validator\ValidationContext; final class InEnumTest extends RuleTestCase { @@ -134,6 +135,18 @@ public function dataValidationFailed(): array ]; } + public function testValidationMessageContainsNecessaryParameters(): void + { + $rule = (new InEnum(EnumStatus::class)); + + $result = (new InEnumHandler())->validate('aaa', $rule, new ValidationContext()); + foreach ($result->getErrors() as $error) { + $parameters = $error->getParameters(); + $this->assertArrayHasKey('property', $parameters); + $this->assertArrayHasKey('Property', $parameters); + } + } + public function testSkipOnError(): void { $this->testSkipOnErrorInternal(new InEnum(EnumStatus::class), new InEnum(EnumStatus::class, skipOnError: true));