Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge rules from PHP attributes with rules provided via getRules() method #747

Merged
merged 3 commits into from
Sep 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
51 changes: 41 additions & 10 deletions src/DataSet/ObjectDataSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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:
*
Expand All @@ -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<RuleInterface> $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])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $rules[$key] isn't iterable, why does it mean that $value is?

Btw, this line must be benchmarked. I'd suggest to use foreach loop with []= operator

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If $rules[$key] isn't iterable, why does it mean that $value is?

$value is always list of rules here. See https://github.com/yiisoft/validator/blob/master/src/Helper/ObjectParser.php#L197

Btw, this line must be benchmarked. I'd suggest to use foreach loop with []= operator

https://github.com/vjik/php-unpacking-vs-foreach-bench

I think in this case better use unpacking.

? [...$value, ...$rules[$key]]
: [...$value, $rules[$key]];
}

return $this->parser->getRules();
return $rules;
}

/**
Expand Down
37 changes: 32 additions & 5 deletions tests/DataSet/ObjectDataSetTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
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;
use Yiisoft\Validator\RuleInterface;
use Yiisoft\Validator\Tests\Support\Data\ObjectWithCallbackMethod\ObjectWithCallbackMethod;
Expand All @@ -22,6 +24,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;
Expand Down Expand Up @@ -151,10 +154,31 @@ 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 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
Expand Down Expand Up @@ -391,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'],
],
];
}
Expand Down
32 changes: 32 additions & 0 deletions tests/Support/Data/ObjectWithIterablePropertyRules.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Validator\Tests\Support\Data;

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;

final class ObjectWithIterablePropertyRules implements RulesProviderInterface
{
#[Required]
public string $name = '';

#[GreaterThan(5)]
#[Number(min: 21)]
protected int $age = 17;

#[Number(max: 100)]
private int $number = 42;

public function getRules(): iterable
{
return [
'age' => new ArrayObject([new Required(), new Equal(25)]),
];
}
}
3 changes: 2 additions & 1 deletion tests/ValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading