Skip to content

Commit aa921c5

Browse files
authored
Merge rules from PHP attributes with rules provided via getRules() method (#747)
1 parent a24f551 commit aa921c5

File tree

5 files changed

+108
-16
lines changed

5 files changed

+108
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
`getRanges()`, `getNetworks()`, `isAllowed()` methods (@vjik)
77
- Enh #746: Use `NEGATION_CHARACTER` constant from `network-utilities` package in `IpHandler` instead of declaring its own
88
constant (@arogachev)
9+
- Chg #747: Merge rules from PHP attributes with rules provided via `getRules()` method (@vjik)
910

1011
## 2.0.0 August 02, 2024
1112

src/DataSet/ObjectDataSet.php

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@
55
namespace Yiisoft\Validator\DataSet;
66

77
use ReflectionProperty;
8+
use Traversable;
89
use Yiisoft\Validator\PropertyTranslatorInterface;
910
use Yiisoft\Validator\PropertyTranslatorProviderInterface;
1011
use Yiisoft\Validator\DataSetInterface;
1112
use Yiisoft\Validator\DataWrapperInterface;
1213
use Yiisoft\Validator\Helper\ObjectParser;
1314
use Yiisoft\Validator\LabelsProviderInterface;
15+
use Yiisoft\Validator\RuleInterface;
1416
use Yiisoft\Validator\RulesProvider\AttributesRulesProvider;
1517
use Yiisoft\Validator\RulesProviderInterface;
1618
use Yiisoft\Validator\ValidatorInterface;
1719

20+
use function array_unshift;
21+
use function is_int;
22+
use function is_iterable;
23+
1824
/**
1925
* A data set for object data. The object passed to this data set can provide rules and data by implementing
2026
* {@see RulesProviderInterface} and {@see DataSetInterface}. Alternatively this data set allows getting rules from PHP
@@ -181,7 +187,7 @@ public function __construct(
181187
/**
182188
* @var object An object containing rules and data.
183189
*/
184-
private object $object,
190+
private readonly object $object,
185191
int $propertyVisibility = ReflectionProperty::IS_PRIVATE |
186192
ReflectionProperty::IS_PROTECTED |
187193
ReflectionProperty::IS_PUBLIC,
@@ -197,10 +203,11 @@ public function __construct(
197203
}
198204

199205
/**
200-
* Returns {@see $object} rules specified via {@see RulesProviderInterface::getRules()} implementation or parsed
206+
* Returns {@see $object} rules specified via {@see RulesProviderInterface::getRules()} implementation or/and parsed
201207
* from attributes attached to class properties and class itself. For the latter case repetitive calls utilize cache
202-
* if it's enabled in {@see $useCache}. Rules provided via separate method have a higher priority over attributes,
203-
* so, when used together, the latter ones will be ignored without exception.
208+
* if it's enabled in {@see $useCache}. Rules provided via separate method have a lower priority over
209+
* PHP attributes, so, when used together, all rules will be merged, but rules from PHP attributes will be applied
210+
* first.
204211
*
205212
* @return iterable The resulting rules is an array with the following structure:
206213
*
@@ -218,17 +225,41 @@ public function getRules(): iterable
218225
if ($this->rulesProvided) {
219226
/** @var RulesProviderInterface $object */
220227
$object = $this->object;
221-
222-
return $object->getRules();
228+
$rules = $object->getRules();
229+
} else {
230+
$rules = [];
223231
}
224232

225-
// Providing data set assumes object has its own rules getting logic. So further parsing of rules is skipped
226-
// intentionally.
233+
// Providing data set assumes object has its own rules getting logic.
234+
// So further parsing of rules is skipped intentionally.
227235
if ($this->dataSetProvided) {
228-
return [];
236+
return $rules;
237+
}
238+
239+
// Merge rules from `RulesProviderInterface` implementation and parsed from PHP attributes.
240+
$rules = $rules instanceof Traversable ? iterator_to_array($rules) : $rules;
241+
foreach ($this->parser->getRules() as $key => $value) {
242+
if (is_int($key)) {
243+
array_unshift($rules, $value);
244+
continue;
245+
}
246+
247+
/**
248+
* @psalm-var list<RuleInterface> $value If `$key` is string, then `$value` is array of rules
249+
* @see ObjectParser::getRules()
250+
*/
251+
252+
if (!isset($rules[$key])) {
253+
$rules[$key] = $value;
254+
continue;
255+
}
256+
257+
$rules[$key] = is_iterable($rules[$key])
258+
? [...$value, ...$rules[$key]]
259+
: [...$value, $rules[$key]];
229260
}
230261

231-
return $this->parser->getRules();
262+
return $rules;
232263
}
233264

234265
/**

tests/DataSet/ObjectDataSetTest.php

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
use Yiisoft\Validator\Label;
1414
use Yiisoft\Validator\Rule\Callback;
1515
use Yiisoft\Validator\Rule\Equal;
16+
use Yiisoft\Validator\Rule\GreaterThan;
1617
use Yiisoft\Validator\Rule\Length;
18+
use Yiisoft\Validator\Rule\Number;
1719
use Yiisoft\Validator\Rule\Required;
1820
use Yiisoft\Validator\RuleInterface;
1921
use Yiisoft\Validator\Tests\Support\Data\ObjectWithCallbackMethod\ObjectWithCallbackMethod;
@@ -22,6 +24,7 @@
2224
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDataSetAndRulesProvider;
2325
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDifferentPropertyVisibility;
2426
use Yiisoft\Validator\Tests\Support\Data\ObjectWithDynamicDataSet;
27+
use Yiisoft\Validator\Tests\Support\Data\ObjectWithIterablePropertyRules;
2528
use Yiisoft\Validator\Tests\Support\Data\ObjectWithLabelsProvider;
2629
use Yiisoft\Validator\Tests\Support\Data\ObjectWithRulesProvider;
2730
use Yiisoft\Validator\Tests\Support\Data\Post;
@@ -151,10 +154,31 @@ public function testObjectWithRulesProvider(ObjectDataSet $dataSet): void
151154
$this->assertSame(42, $dataSet->getPropertyValue('number'));
152155
$this->assertNull($dataSet->getPropertyValue('non-exist'));
153156

154-
$this->assertSame(['age'], array_keys($rules));
155-
$this->assertCount(2, $rules['age']);
156-
$this->assertInstanceOf(Required::class, $rules['age'][0]);
157-
$this->assertInstanceOf(Equal::class, $rules['age'][1]);
157+
$this->assertSame(['age', 'name', 'number'], array_keys($rules));
158+
$this->assertCount(3, $rules['age']);
159+
$this->assertInstanceOf(Number::class, $rules['age'][0]);
160+
$this->assertInstanceOf(Required::class, $rules['age'][1]);
161+
$this->assertInstanceOf(Equal::class, $rules['age'][2]);
162+
}
163+
164+
public function testObjectWithIterablePropertyRules(): void
165+
{
166+
$dataSet = (new ObjectDataSet(new ObjectWithIterablePropertyRules()));
167+
$rules = $dataSet->getRules();
168+
169+
$this->assertSame(['name' => '', 'age' => 17, 'number' => 42], $dataSet->getData());
170+
171+
$this->assertSame('', $dataSet->getPropertyValue('name'));
172+
$this->assertSame(17, $dataSet->getPropertyValue('age'));
173+
$this->assertSame(42, $dataSet->getPropertyValue('number'));
174+
$this->assertNull($dataSet->getPropertyValue('non-exist'));
175+
176+
$this->assertSame(['age', 'name', 'number'], array_keys($rules));
177+
$this->assertCount(4, $rules['age']);
178+
$this->assertInstanceOf(GreaterThan::class, $rules['age'][0]);
179+
$this->assertInstanceOf(Number::class, $rules['age'][1]);
180+
$this->assertInstanceOf(Required::class, $rules['age'][2]);
181+
$this->assertInstanceOf(Equal::class, $rules['age'][3]);
158182
}
159183

160184
public function objectWithDataSetAndRulesProviderDataProvider(): array
@@ -391,8 +415,11 @@ public function objectWithLabelsProvider(): array
391415
#[Required]
392416
#[Label('Test label')]
393417
public string $property;
418+
419+
#[Label('Test label 2')]
420+
public string $property2;
394421
}),
395-
['property' => 'Test label'],
422+
['property' => 'Test label', 'property2' => 'Test label 2'],
396423
],
397424
];
398425
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Validator\Tests\Support\Data;
6+
7+
use ArrayObject;
8+
use Yiisoft\Validator\Rule\Equal;
9+
use Yiisoft\Validator\Rule\GreaterThan;
10+
use Yiisoft\Validator\Rule\Number;
11+
use Yiisoft\Validator\Rule\Required;
12+
use Yiisoft\Validator\RulesProviderInterface;
13+
14+
final class ObjectWithIterablePropertyRules implements RulesProviderInterface
15+
{
16+
#[Required]
17+
public string $name = '';
18+
19+
#[GreaterThan(5)]
20+
#[Number(min: 21)]
21+
protected int $age = 17;
22+
23+
#[Number(max: 100)]
24+
private int $number = 42;
25+
26+
public function getRules(): iterable
27+
{
28+
return [
29+
'age' => new ArrayObject([new Required(), new Equal(25)]),
30+
];
31+
}
32+
}

tests/ValidatorTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ public function dataDataAndRulesCombinations(): array
127127
],
128128
'rules-provider-object-and-no-rules' => [
129129
[
130-
'age' => ['Age must be equal to "25".'],
130+
'age' => ['Age must be no less than 21.', 'Age must be equal to "25".'],
131+
'name' => ['Name cannot be blank.'],
131132
],
132133
new ObjectWithRulesProvider(),
133134
null,

0 commit comments

Comments
 (0)