From 513a62a0b9341c9c08a55425ff39604b75747c80 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 7 Sep 2024 11:44:27 +0300 Subject: [PATCH 1/3] Merge rules from PHP attributes with rules provided via `getRules()` method --- CHANGELOG.md | 1 + src/DataSet/ObjectDataSet.php | 51 +++++++++++++++---- tests/DataSet/ObjectDataSetTest.php | 12 +++-- .../Data/ObjectWithIterablePropertyRules.php | 30 +++++++++++ tests/ValidatorTest.php | 3 +- 5 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 tests/Support/Data/ObjectWithIterablePropertyRules.php diff --git a/CHANGELOG.md b/CHANGELOG.md index dcd662471..bceefe4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ `getRanges()`, `getNetworks()`, `isAllowed()` methods (@vjik) - Enh #746: Use `NEGATION_CHARACTER` constant from `network-utilities` package in `IpHandler` instead of declaring its own constant (@arogachev) +- Chg #747: Merge rules from PHP attributes with rules provided via `getRules()` method (@vjik) ## 2.0.0 August 02, 2024 diff --git a/src/DataSet/ObjectDataSet.php b/src/DataSet/ObjectDataSet.php index c5cfc796b..e1612286c 100644 --- a/src/DataSet/ObjectDataSet.php +++ b/src/DataSet/ObjectDataSet.php @@ -5,16 +5,22 @@ namespace Yiisoft\Validator\DataSet; use ReflectionProperty; +use Traversable; use Yiisoft\Validator\PropertyTranslatorInterface; use Yiisoft\Validator\PropertyTranslatorProviderInterface; use Yiisoft\Validator\DataSetInterface; use Yiisoft\Validator\DataWrapperInterface; use Yiisoft\Validator\Helper\ObjectParser; use Yiisoft\Validator\LabelsProviderInterface; +use Yiisoft\Validator\RuleInterface; use Yiisoft\Validator\RulesProvider\AttributesRulesProvider; use Yiisoft\Validator\RulesProviderInterface; use Yiisoft\Validator\ValidatorInterface; +use function array_unshift; +use function is_int; +use function is_iterable; + /** * A data set for object data. The object passed to this data set can provide rules and data by implementing * {@see RulesProviderInterface} and {@see DataSetInterface}. Alternatively this data set allows getting rules from PHP @@ -181,7 +187,7 @@ public function __construct( /** * @var object An object containing rules and data. */ - private object $object, + private readonly object $object, int $propertyVisibility = ReflectionProperty::IS_PRIVATE | ReflectionProperty::IS_PROTECTED | ReflectionProperty::IS_PUBLIC, @@ -197,10 +203,11 @@ public function __construct( } /** - * Returns {@see $object} rules specified via {@see RulesProviderInterface::getRules()} implementation or parsed + * Returns {@see $object} rules specified via {@see RulesProviderInterface::getRules()} implementation or/and parsed * from attributes attached to class properties and class itself. For the latter case repetitive calls utilize cache - * if it's enabled in {@see $useCache}. Rules provided via separate method have a higher priority over attributes, - * so, when used together, the latter ones will be ignored without exception. + * if it's enabled in {@see $useCache}. Rules provided via separate method have a lower priority over + * PHP attributes, so, when used together, all rules will be merged, but rules from PHP attributes will be applied + * first. * * @return iterable The resulting rules is an array with the following structure: * @@ -218,17 +225,41 @@ public function getRules(): iterable if ($this->rulesProvided) { /** @var RulesProviderInterface $object */ $object = $this->object; - - return $object->getRules(); + $rules = $object->getRules(); + } else { + $rules = []; } - // Providing data set assumes object has its own rules getting logic. So further parsing of rules is skipped - // intentionally. + // Providing data set assumes object has its own rules getting logic. + // So further parsing of rules is skipped intentionally. if ($this->dataSetProvided) { - return []; + return $rules; + } + + // Merge rules from `RulesProviderInterface` implementation and parsed from PHP attributes. + $rules = $rules instanceof Traversable ? iterator_to_array($rules) : $rules; + foreach ($this->parser->getRules() as $key => $value) { + if (is_int($key)) { + array_unshift($rules, $value); + continue; + } + + /** + * @psalm-var list $value If `$key` is string, then `$value` is array of rules + * @see ObjectParser::getRules() + */ + + if (!isset($rules[$key])) { + $rules[$key] = $value; + continue; + } + + $rules[$key] = is_iterable($rules[$key]) + ? [...$value, ...$rules[$key]] + : [...$value, $rules[$key]]; } - return $this->parser->getRules(); + return $rules; } /** diff --git a/tests/DataSet/ObjectDataSetTest.php b/tests/DataSet/ObjectDataSetTest.php index 30ee01cca..41551db32 100644 --- a/tests/DataSet/ObjectDataSetTest.php +++ b/tests/DataSet/ObjectDataSetTest.php @@ -14,6 +14,7 @@ use Yiisoft\Validator\Rule\Callback; use Yiisoft\Validator\Rule\Equal; use Yiisoft\Validator\Rule\Length; +use Yiisoft\Validator\Rule\Number; use Yiisoft\Validator\Rule\Required; use Yiisoft\Validator\RuleInterface; use Yiisoft\Validator\Tests\Support\Data\ObjectWithCallbackMethod\ObjectWithCallbackMethod; @@ -22,6 +23,7 @@ use Yiisoft\Validator\Tests\Support\Data\ObjectWithDataSetAndRulesProvider; use Yiisoft\Validator\Tests\Support\Data\ObjectWithDifferentPropertyVisibility; use Yiisoft\Validator\Tests\Support\Data\ObjectWithDynamicDataSet; +use Yiisoft\Validator\Tests\Support\Data\ObjectWithIterablePropertyRules; use Yiisoft\Validator\Tests\Support\Data\ObjectWithLabelsProvider; use Yiisoft\Validator\Tests\Support\Data\ObjectWithRulesProvider; use Yiisoft\Validator\Tests\Support\Data\Post; @@ -134,6 +136,7 @@ public function objectWithRulesProvider(): array [new ObjectDataSet(new ObjectWithRulesProvider())], // Not a duplicate. Used to test caching. [$dataSet], [$dataSet], // Not a duplicate. Used to test caching. + [new ObjectDataSet(new ObjectWithIterablePropertyRules())], ]; } @@ -151,10 +154,11 @@ public function testObjectWithRulesProvider(ObjectDataSet $dataSet): void $this->assertSame(42, $dataSet->getPropertyValue('number')); $this->assertNull($dataSet->getPropertyValue('non-exist')); - $this->assertSame(['age'], array_keys($rules)); - $this->assertCount(2, $rules['age']); - $this->assertInstanceOf(Required::class, $rules['age'][0]); - $this->assertInstanceOf(Equal::class, $rules['age'][1]); + $this->assertSame(['age', 'name', 'number'], array_keys($rules)); + $this->assertCount(3, $rules['age']); + $this->assertInstanceOf(Number::class, $rules['age'][0]); + $this->assertInstanceOf(Required::class, $rules['age'][1]); + $this->assertInstanceOf(Equal::class, $rules['age'][2]); } public function objectWithDataSetAndRulesProviderDataProvider(): array diff --git a/tests/Support/Data/ObjectWithIterablePropertyRules.php b/tests/Support/Data/ObjectWithIterablePropertyRules.php new file mode 100644 index 000000000..105e0c294 --- /dev/null +++ b/tests/Support/Data/ObjectWithIterablePropertyRules.php @@ -0,0 +1,30 @@ + new ArrayObject([new Required(), new Equal(25)]), + ]; + } +} diff --git a/tests/ValidatorTest.php b/tests/ValidatorTest.php index d7693a309..440efc418 100644 --- a/tests/ValidatorTest.php +++ b/tests/ValidatorTest.php @@ -127,7 +127,8 @@ public function dataDataAndRulesCombinations(): array ], 'rules-provider-object-and-no-rules' => [ [ - 'age' => ['Age must be equal to "25".'], + 'age' => ['Age must be no less than 21.', 'Age must be equal to "25".'], + 'name' => ['Name cannot be blank.'], ], new ObjectWithRulesProvider(), null, From 4b32bfeec11d3484842ba2dc21c7ae264a59aec6 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 7 Sep 2024 11:54:07 +0300 Subject: [PATCH 2/3] Kill mutant --- tests/DataSet/ObjectDataSetTest.php | 22 ++++++++++++++++++- .../Data/ObjectWithIterablePropertyRules.php | 2 ++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/tests/DataSet/ObjectDataSetTest.php b/tests/DataSet/ObjectDataSetTest.php index 41551db32..ec0e23bb0 100644 --- a/tests/DataSet/ObjectDataSetTest.php +++ b/tests/DataSet/ObjectDataSetTest.php @@ -13,6 +13,7 @@ use Yiisoft\Validator\Label; use Yiisoft\Validator\Rule\Callback; use Yiisoft\Validator\Rule\Equal; +use Yiisoft\Validator\Rule\GreaterThan; use Yiisoft\Validator\Rule\Length; use Yiisoft\Validator\Rule\Number; use Yiisoft\Validator\Rule\Required; @@ -136,7 +137,6 @@ public function objectWithRulesProvider(): array [new ObjectDataSet(new ObjectWithRulesProvider())], // Not a duplicate. Used to test caching. [$dataSet], [$dataSet], // Not a duplicate. Used to test caching. - [new ObjectDataSet(new ObjectWithIterablePropertyRules())], ]; } @@ -161,6 +161,26 @@ public function testObjectWithRulesProvider(ObjectDataSet $dataSet): void $this->assertInstanceOf(Equal::class, $rules['age'][2]); } + public function testObjectWithIterablePropertyRules(): void + { + $dataSet = (new ObjectDataSet(new ObjectWithIterablePropertyRules())); + $rules = $dataSet->getRules(); + + $this->assertSame(['name' => '', 'age' => 17, 'number' => 42], $dataSet->getData()); + + $this->assertSame('', $dataSet->getPropertyValue('name')); + $this->assertSame(17, $dataSet->getPropertyValue('age')); + $this->assertSame(42, $dataSet->getPropertyValue('number')); + $this->assertNull($dataSet->getPropertyValue('non-exist')); + + $this->assertSame(['age', 'name', 'number'], array_keys($rules)); + $this->assertCount(4, $rules['age']); + $this->assertInstanceOf(GreaterThan::class, $rules['age'][0]); + $this->assertInstanceOf(Number::class, $rules['age'][1]); + $this->assertInstanceOf(Required::class, $rules['age'][2]); + $this->assertInstanceOf(Equal::class, $rules['age'][3]); + } + public function objectWithDataSetAndRulesProviderDataProvider(): array { $dataSet = new ObjectDataSet(new ObjectWithDataSetAndRulesProvider()); diff --git a/tests/Support/Data/ObjectWithIterablePropertyRules.php b/tests/Support/Data/ObjectWithIterablePropertyRules.php index 105e0c294..6ea60770f 100644 --- a/tests/Support/Data/ObjectWithIterablePropertyRules.php +++ b/tests/Support/Data/ObjectWithIterablePropertyRules.php @@ -6,6 +6,7 @@ use ArrayObject; use Yiisoft\Validator\Rule\Equal; +use Yiisoft\Validator\Rule\GreaterThan; use Yiisoft\Validator\Rule\Number; use Yiisoft\Validator\Rule\Required; use Yiisoft\Validator\RulesProviderInterface; @@ -15,6 +16,7 @@ final class ObjectWithIterablePropertyRules implements RulesProviderInterface #[Required] public string $name = ''; + #[GreaterThan(5)] #[Number(min: 21)] protected int $age = 17; From 25a8ec5808592467c55a0be68e1af843af073085 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Sat, 7 Sep 2024 12:05:01 +0300 Subject: [PATCH 3/3] Kill mutant 2 --- tests/DataSet/ObjectDataSetTest.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/DataSet/ObjectDataSetTest.php b/tests/DataSet/ObjectDataSetTest.php index ec0e23bb0..29e6b24dc 100644 --- a/tests/DataSet/ObjectDataSetTest.php +++ b/tests/DataSet/ObjectDataSetTest.php @@ -415,8 +415,11 @@ public function objectWithLabelsProvider(): array #[Required] #[Label('Test label')] public string $property; + + #[Label('Test label 2')] + public string $property2; }), - ['property' => 'Test label'], + ['property' => 'Test label', 'property2' => 'Test label 2'], ], ]; }