Skip to content

Commit 75fd75e

Browse files
authored
Fix #243: Fix attributes collecting (#249)
1 parent 0c2d954 commit 75fd75e

33 files changed

Lines changed: 600 additions & 321 deletions

README.md

Lines changed: 102 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,15 @@ Library could be used in two ways: validating a single value and validating a se
4646
### Validating a single value
4747

4848
```php
49+
use Yiisoft\Validator\ValidatorInterface;
4950
use Yiisoft\Validator\RuleSet;
5051
use Yiisoft\Validator\Rule\Required;
5152
use Yiisoft\Validator\Rule\Number;
5253
use Yiisoft\Validator\Result;
5354

55+
// Usually obtained from container
56+
$validator = $container->get(ValidatorInterface::class);
57+
5458
$rules = [
5559
new Required(),
5660
new Number(min: 10),
@@ -65,7 +69,6 @@ $rules = [
6569
return $result;
6670
}
6771
];
68-
$validator = getValidator(); // Usually obtained from container
6972

7073
$result = $validator->validate(41, $rules);
7174
if (!$result->isValid()) {
@@ -80,6 +83,7 @@ if (!$result->isValid()) {
8083
```php
8184
use Yiisoft\Validator\DataSetInterface;
8285
use Yiisoft\Validator\Validator;
86+
use Yiisoft\Validator\ValidatorInterface;
8387
use Yiisoft\Validator\Rule\Number;
8488
use Yiisoft\Validator\Result;
8589

@@ -100,7 +104,9 @@ final class MoneyTransfer implements DataSetInterface
100104
}
101105
}
102106

103-
$validator = getValidator(); // Usually obtained from container
107+
// Usually obtained from container
108+
$validator = $container->get(ValidatorInterface::class);
109+
104110
$moneyTransfer = new MoneyTransfer(142);
105111
$rules = [
106112
'amount' => [
@@ -152,12 +158,15 @@ In many cases there is a need to validate related data in addition to current en
152158
for this purpose:
153159

154160
```php
161+
use Yiisoft\Validator\ValidatorInterface;
155162
use Yiisoft\Validator\Rule\HasLength;
156163
use Yiisoft\Validator\Rule\Nested;
157164
use Yiisoft\Validator\Rule\Number;
158165
use Yiisoft\Validator\Rule\Required;
159166

160-
$validator = getValidator(); // Usually obtained from container
167+
// Usually obtained from container
168+
$validator = $container->get(ValidatorInterface::class);
169+
161170
$data = ['author' => ['name' => 'Alexey', 'age' => '31']];
162171
$rule = new Nested([
163172
'title' => [new Required()],
@@ -210,12 +219,15 @@ A more complex real-life example is a chart that is made of points. This data is
210219
combined with `Each` rule to validate such similar structures:
211220

212221
```php
222+
use Yiisoft\Validator\ValidatorInterface;
213223
use Yiisoft\Validator\Rule\Count;
214224
use Yiisoft\Validator\Rule\Each;
215225
use Yiisoft\Validator\Rule\Nested;
216226
use Yiisoft\Validator\RuleSet;
217227

218-
$validator = getValidator(); // Usually obtained from container
228+
// Usually obtained from container
229+
$validator = $container->get(ValidatorInterface::class);
230+
219231
$data = [
220232
'charts' => [
221233
[
@@ -274,31 +286,33 @@ $errors = [
274286

275287
##### Basic usage
276288

277-
You can also use attributes as an alternative. Declare the DTOs, relations and rules:
289+
Common flow is the same as you would use usual classes:
290+
1. Declare property
291+
2. Add rules to it
278292

279293
```php
280-
use Yiisoft\Validator\Attribute\HasMany;
281-
use Yiisoft\Validator\Attribute\HasOne;
294+
use Yiisoft\Validator\Attribute\Embedded;
295+
use Yiisoft\Validator\Rule\Count;
296+
use Yiisoft\Validator\Rule\Each;
282297
use Yiisoft\Validator\Rule\Number;
283298

284-
final class ChartsData
285-
{
286-
#[HasMany(Chart::class)]
287-
private array $charts;
288-
}
289-
290299
final class Chart
291300
{
292-
#[HasMany(Point::class)]
301+
#[Each([
302+
new Embedded(Point::class),
303+
])]
293304
private array $points;
294305
}
295306

296307
final class Point
297308
{
298-
#[HasOne(Coordinates::class)]
309+
#[Embedded(Coordinates::class)]
299310
private $coordinates;
300-
#[Number(min: 0, max: 255)]
301-
private array $rgb; // A flat array, the "Number" rule will be applied to each array element.
311+
#[Count(exactly: 3)]
312+
#[Each([
313+
new Number(min: 0, max: 255),
314+
])]
315+
private array $rgb;
302316
}
303317

304318
final class Coordinates
@@ -310,44 +324,25 @@ final class Coordinates
310324
}
311325
```
312326

313-
To combine both flat rules and "each" rules, specify `Each` explicitly and place flat rules above "each" ones:
314-
315-
```php
316-
use Yiisoft\Validator\Rule\Count;
317-
use Yiisoft\Validator\Rule\Each;
318-
use Yiisoft\Validator\Attribute\HasMany;
319-
use Yiisoft\Validator\Attribute\HasOne;
320-
use Yiisoft\Validator\Rule\Number;
321-
322-
final class Point
323-
{
324-
#[HasOne(Coordinates::class)]
325-
private $coordinates;
326-
#[Count(exactly: 3)]
327-
#[Each()]
328-
#[Number(min: 0, max: 255)]
329-
private array $rgb;
330-
}
331-
```
332-
333-
In this example `Count` will be applied to the whole value and `Number` - for each item.
334-
335327
Here are some technical details:
336328

337-
- `HasOne` uses `Nested` rule.
338-
- `HasMany` uses combination of `Each` and `Nested` rules.
339-
- In case of a flat array `Point::$rgb`, a property type `array` needs to be declared. It uses `Each` rule internally.
329+
- `Embedded` creates a reference to the referenced class and uses a `GroupRule` under the hood to make represent
330+
referenced class rules as it owns.
331+
- In case of a flat array `Point::$rgb`, a property type `array` needs to be declared.
340332

341333
Pass the base DTO to `AttributeDataSet` and use it for validation.
342334

343335
```php
344336
use Yiisoft\Validator\DataSet\AttributeDataSet;
337+
use Yiisoft\Validator\ValidatorInterface;
338+
339+
// Usually obtained from container
340+
$validator = $container->get(ValidatorInterface::class);
345341

346342
$data = [
347343
// ...
348344
];
349345
$dataSet = new AttributeDataSet(new ChartsData(), $data);
350-
$validator = getValidator(); // Usually obtained from container
351346
$errors = $validator->validate($dataSet)->getErrorMessagesIndexedByPath();
352347
```
353348

@@ -372,40 +367,6 @@ final class Post
372367

373368
##### Limitations
374369

375-
This approach has some limitations.
376-
377-
###### `Each` and `Nested` rules
378-
379-
`Each` and `Nested` rules are not supported directly. Use `HasOne` and `HasMany` attributes for declaring relations (or
380-
property type `array` for flat rules) instead. Use `Each` and `Nested` rules in addition for custom configuration if
381-
needed.
382-
383-
```php
384-
use Yiisoft\Validator\Attribute\HasMany;
385-
use Yiisoft\Validator\Attribute\HasOne;
386-
use Yiisoft\Validator\Rule\Each;
387-
use Yiisoft\Validator\Rule\Nested;
388-
use Yiisoft\Validator\Rule\Number;
389-
390-
final class ChartsData
391-
{
392-
#[Each(incorrectInputMessage: 'Custom message 1.', message: 'Custom message 2.')]
393-
#[Nested(errorWhenPropertyPathIsNotFound: true, propertyPathIsNotFoundMessage: 'Custom message 3.')]
394-
#[HasMany(Chart::class)]
395-
private array $charts;
396-
}
397-
398-
final class Point
399-
{
400-
#[Nested(errorWhenPropertyPathIsNotFound: true, propertyPathIsNotFoundMessage: 'Custom message 4.')]
401-
#[HasOne(Coordinates::class)]
402-
private $coordinates;
403-
#[Each(incorrectInputMessage: 'Custom message 5.', message: 'Custom message 6.')]
404-
#[Number(min: 0, max: 255)]
405-
private array $rgb;
406-
}
407-
```
408-
409370
###### `Callback` rule and `callable` type
410371

411372
`Callback` rule is not supported, also you can't use `callable` type with attributes. Use custom rule instead.
@@ -419,7 +380,7 @@ use Yiisoft\Validator\RuleHandlerInterface;
419380
use \Yiisoft\Validator\RuleInterface;
420381
use Yiisoft\Validator\ValidationContext;
421382

422-
#[Attribute(Attribute::TARGET_PROPERTY)]
383+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
423384
final class ValidateXRule implements RuleInterface
424385
{
425386
public function __construct()
@@ -454,8 +415,7 @@ final class Coordinates
454415

455416
###### `GroupRule`
456417

457-
`GroupRule` is not supported, but it's unnecessary since multiple attributes can be used for one property (except they
458-
must be of different type).
418+
`GroupRule` is not supported, but it's unnecessary since multiple attributes can be used for one property.
459419

460420
```php
461421
use Yiisoft\Validator\Rule\HasLength;
@@ -469,6 +429,63 @@ final class UserData
469429
}
470430
```
471431

432+
###### Nested attributes
433+
434+
PHP 8.0 supports attributes, but nested declaration is allowed only in PHP 8.1 and above.
435+
436+
So such attributes as `Each`, `Nested` and `Composite` are not allowed in PHP 8.0.
437+
438+
The following example is not allowed in PHP 8.0:
439+
440+
```php
441+
use Yiisoft\Validator\Rule\Each;
442+
use Yiisoft\Validator\Rule\Number;
443+
444+
final class Color
445+
{
446+
#[Each([
447+
new Number(min: 0, max: 255),
448+
])]
449+
private array $values;
450+
}
451+
```
452+
453+
But you can do this by creating a new composite rule from it.
454+
455+
```php
456+
namespace App\Validator\Rule;
457+
458+
use Attribute;
459+
use Yiisoft\Validator\Rule\Each;
460+
use Yiisoft\Validator\Rule\GroupRule;
461+
462+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
463+
final class RgbRule extends GroupRule
464+
{
465+
public function getRuleSet(): array
466+
{
467+
return [
468+
new Each([
469+
new Number(min: 0, max: 255),
470+
]),
471+
];
472+
}
473+
}
474+
```
475+
476+
And use it after as attribute.
477+
478+
```php
479+
use App\Validator\Rule\RgbRule;
480+
481+
final class Color
482+
{
483+
#[RgbRule]
484+
private array $values;
485+
}
486+
487+
```
488+
472489
###### Function / method calls
473490

474491
You can't use a function / method call result with attributes. Like with `Callback` rule and callable, this problem can
@@ -493,7 +510,7 @@ final class CustomFormatter implements FormatterInterface
493510
}
494511
}
495512

496-
#[Attribute(Attribute::TARGET_PROPERTY)]
513+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
497514
final class ValidateXRule implements RuleInterface
498515
{
499516
public function __construct(
@@ -758,9 +775,12 @@ Then it could be used like the following:
758775

759776
```php
760777
use Yiisoft\Validator\Validator;
778+
use Yiisoft\Validator\ValidatorInterface;
761779
use Yiisoft\Validator\Rule\Email;
762780

763-
$validator = getValidator(); // Usually obtained from container
781+
// Usually obtained from container
782+
$validator = $container->get(ValidatorInterface::class);
783+
764784
$rules = [
765785
'username' => new UsernameRule(),
766786
'email' => [new Email()],

phpunit.xml.dist

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,13 @@
1818
<testsuites>
1919
<testsuite name="Validator">
2020
<directory>./tests</directory>
21+
<exclude>./tests/DataSet/PHP81</exclude>
2122
</testsuite>
22-
</testsuites>
2323

24+
<testsuite name="Validator 81">
25+
<directory phpVersion="8.1.0" phpVersionOperator=">=">./tests/DataSet/PHP81</directory>
26+
</testsuite>
27+
</testsuites>
2428
<coverage>
2529
<include>
2630
<directory>./src</directory>

src/Attribute/Embedded.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\Validator\Attribute;
6+
7+
use Attribute;
8+
use Closure;
9+
use ReflectionAttribute;
10+
use ReflectionClass;
11+
use Yiisoft\Validator\Rule\GroupRule;
12+
use Yiisoft\Validator\RuleInterface;
13+
use Yiisoft\Validator\ValidationContext;
14+
15+
/**
16+
* Collects all attributes from the reference and represents it as its own.
17+
*/
18+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
19+
final class Embedded extends GroupRule
20+
{
21+
/**
22+
* @psalm-param Closure(mixed, ValidationContext):bool|null $when
23+
*/
24+
public function __construct(
25+
private string $referenceClassName,
26+
string $message = 'This value is not a valid.',
27+
bool $skipOnEmpty = false,
28+
bool $skipOnError = false,
29+
?Closure $when = null,
30+
) {
31+
parent::__construct($message, $skipOnEmpty, $skipOnError, $when);
32+
}
33+
34+
public function getRuleSet(): array
35+
{
36+
$classMeta = new ReflectionClass($this->referenceClassName);
37+
38+
return $this->collectAttributes($classMeta);
39+
}
40+
41+
// TODO: use Generator to collect attributes
42+
private function collectAttributes(ReflectionClass $classMeta): array
43+
{
44+
$rules = [];
45+
foreach ($classMeta->getProperties() as $property) {
46+
$attributes = $property->getAttributes(RuleInterface::class, ReflectionAttribute::IS_INSTANCEOF);
47+
foreach ($attributes as $attribute) {
48+
$rules[$property->getName()][] = $attribute->newInstance();
49+
}
50+
}
51+
52+
return $rules;
53+
}
54+
}

0 commit comments

Comments
 (0)