Skip to content

Commit f2ab556

Browse files
committed
Extract PropertyResolver strategy
Previously, the resolution of validators from property types and explicit new resolution strategies required editing the class, and a class-typed property annotated with #[Attributes] was validated twice — once by the explicit-attribute branch and again by the declared-type branch — which tripped the circular-reference guard on the second pass. This commit moves that responsibility to a dedicated PropertyResolver interface with composable implementations, so each strategy can be developed and composed independently. The composite collapses duplicate Attributes entries, ensuring each property is validated exactly once even when multiple resolution paths produce the same instance. The Attributes class is reduced to its single responsibility, and resolver chains become pluggable through the constructor.
1 parent 42f08d7 commit f2ab556

17 files changed

Lines changed: 621 additions & 46 deletions

docs/validators/Attributes.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
88
# Attributes
99

1010
- `Attributes()`
11+
- `Attributes(PropertyResolver $propertyResolver)`
1112

1213
Validates the PHP attributes defined in the properties of the input.
1314

src-dev/Commands/LintMixinCommand.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Respect\Validation\Mixins\Chain;
2323
use Respect\Validation\Validator;
2424
use Respect\Validation\ValidatorBuilder;
25+
use Respect\Validation\Validators\Attributes\PropertyResolver;
2526
use Symfony\Component\Console\Attribute\AsCommand;
2627
use Symfony\Component\Console\Command\Command;
2728
use Symfony\Component\Console\Input\InputInterface;
@@ -75,7 +76,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
7576
scanner: $scanner,
7677
methodBuilder: new MethodBuilder(
7778
excludedTypePrefixes: ['Sokil', 'Egulias'],
78-
excludedTypeNames: ['finfo'],
79+
excludedTypeNames: ['finfo', PropertyResolver::class],
7980
),
8081
interfaces: [
8182
new InterfaceConfig(

src/Validators/Attributes.php

Lines changed: 16 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,17 @@
1515
use Attribute;
1616
use ReflectionAttribute;
1717
use ReflectionClass;
18-
use ReflectionIntersectionType;
19-
use ReflectionNamedType;
2018
use ReflectionObject;
2119
use ReflectionProperty;
22-
use ReflectionUnionType;
2320
use Respect\Fluent\Attributes\Composable;
2421
use Respect\Validation\Id;
2522
use Respect\Validation\Message\Template;
2623
use Respect\Validation\Result;
2724
use Respect\Validation\Validator;
25+
use Respect\Validation\Validators\Attributes\CompositePropertyResolver;
26+
use Respect\Validation\Validators\Attributes\DeclaredTypePropertyResolver;
27+
use Respect\Validation\Validators\Attributes\ExplicitAttributePropertyResolver;
28+
use Respect\Validation\Validators\Attributes\PropertyResolver;
2829
use Respect\Validation\Validators\Core\Reducer;
2930

3031
use function spl_object_id;
@@ -43,6 +44,17 @@ final class Attributes implements Validator
4344
/** @var array<int, true> */
4445
private array $visited = [];
4546

47+
private readonly PropertyResolver $propertyResolver;
48+
49+
public function __construct(
50+
PropertyResolver|null $propertyResolver = null,
51+
) {
52+
$this->propertyResolver = $propertyResolver ?? new CompositePropertyResolver(
53+
new ExplicitAttributePropertyResolver(),
54+
new DeclaredTypePropertyResolver(),
55+
);
56+
}
57+
4658
public function evaluate(mixed $input): Result
4759
{
4860
$id = new Id('attributes');
@@ -87,7 +99,7 @@ private function getPropertyValidators(ReflectionObject $reflection): array
8799
{
88100
$validators = [];
89101
foreach ($this->getProperties($reflection) as $propertyName => $property) {
90-
$propertyValidators = $this->getPropertyInnerValidators($property);
102+
$propertyValidators = $this->propertyResolver->resolve($property, $this);
91103
if ($propertyValidators === []) {
92104
continue;
93105
}
@@ -101,47 +113,6 @@ private function getPropertyValidators(ReflectionObject $reflection): array
101113
return $validators;
102114
}
103115

104-
/** @return array<Validator> */
105-
private function getPropertyInnerValidators(ReflectionProperty $property): array
106-
{
107-
$propertyValidators = [];
108-
$hasExplicitAttributes = false;
109-
foreach ($property->getAttributes(Validator::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
110-
$propertyValidator = $attribute->getName() === self::class ? $this : $attribute->newInstance();
111-
$hasExplicitAttributes = $propertyValidator === $this;
112-
$propertyValidators[] = $propertyValidator;
113-
}
114-
115-
if ($hasExplicitAttributes) {
116-
return $propertyValidators;
117-
}
118-
119-
$type = $property->getType();
120-
if ($type instanceof ReflectionNamedType) {
121-
if (!$type->isBuiltin()) {
122-
$propertyValidators[] = $this;
123-
}
124-
}
125-
126-
if ($type instanceof ReflectionIntersectionType) {
127-
$propertyValidators[] = $this;
128-
}
129-
130-
if ($type instanceof ReflectionUnionType) {
131-
foreach ($type->getTypes() as $innerType) {
132-
if (!$innerType instanceof ReflectionNamedType || $innerType->isBuiltin()) {
133-
continue;
134-
}
135-
136-
/** @var class-string $class */
137-
$class = $innerType->getName();
138-
$propertyValidators[] = new Given(new Instance($class), $this);
139-
}
140-
}
141-
142-
return $propertyValidators;
143-
}
144-
145116
/** @return array<ReflectionProperty> */
146117
private function getProperties(ReflectionObject $reflection): array
147118
{
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Validation\Validators\Attributes;
12+
13+
use ReflectionProperty;
14+
use Respect\Validation\Validator;
15+
use Respect\Validation\Validators\Attributes;
16+
17+
use function array_values;
18+
use function in_array;
19+
20+
final class CompositePropertyResolver implements PropertyResolver
21+
{
22+
/** @var list<PropertyResolver> */
23+
private readonly array $resolvers;
24+
25+
public function __construct(PropertyResolver ...$resolvers)
26+
{
27+
$this->resolvers = array_values($resolvers);
28+
}
29+
30+
/** @return array<Validator> */
31+
public function resolve(ReflectionProperty $property, Attributes $attributes): array
32+
{
33+
$accumulated = [];
34+
35+
foreach ($this->resolvers as $resolver) {
36+
$validators = $resolver->resolve($property, $attributes);
37+
if ($validators === []) {
38+
continue;
39+
}
40+
41+
// When more than one resolver recognizes the same property (e.g. a
42+
// class-typed property annotated with #[Attributes], which both
43+
// DeclaredTypePropertyResolver and ExplicitAttributePropertyResolver
44+
// emit `$attributes` for), collapse duplicate `$attributes` entries
45+
// so the nested Attributes validator runs once instead of tripping
46+
// its circular-reference guard on the second pass.
47+
foreach ($validators as $validator) {
48+
if ($validator === $attributes && in_array($validator, $accumulated, true)) {
49+
continue;
50+
}
51+
52+
$accumulated[] = $validator;
53+
}
54+
}
55+
56+
return $accumulated;
57+
}
58+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Validation\Validators\Attributes;
12+
13+
use ReflectionIntersectionType;
14+
use ReflectionNamedType;
15+
use ReflectionProperty;
16+
use ReflectionUnionType;
17+
use Respect\Validation\Validator;
18+
use Respect\Validation\Validators\Attributes;
19+
use Respect\Validation\Validators\Given;
20+
use Respect\Validation\Validators\Instance;
21+
22+
final class DeclaredTypePropertyResolver implements PropertyResolver
23+
{
24+
/** @return array<Validator> */
25+
public function resolve(ReflectionProperty $property, Attributes $attributes): array
26+
{
27+
$type = $property->getType();
28+
29+
if ($type instanceof ReflectionNamedType) {
30+
if ($type->isBuiltin()) {
31+
return [];
32+
}
33+
34+
return [$attributes];
35+
}
36+
37+
if ($type instanceof ReflectionIntersectionType) {
38+
return [$attributes];
39+
}
40+
41+
if ($type instanceof ReflectionUnionType) {
42+
$validators = [];
43+
foreach ($type->getTypes() as $innerType) {
44+
if (!$innerType instanceof ReflectionNamedType || $innerType->isBuiltin()) {
45+
continue;
46+
}
47+
48+
/** @var class-string $class */
49+
$class = $innerType->getName();
50+
$validators[] = new Given(new Instance($class), $attributes);
51+
}
52+
53+
return $validators;
54+
}
55+
56+
return [];
57+
}
58+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Validation\Validators\Attributes;
12+
13+
use ReflectionAttribute;
14+
use ReflectionProperty;
15+
use Respect\Validation\Validator;
16+
use Respect\Validation\Validators\Attributes;
17+
18+
final class ExplicitAttributePropertyResolver implements PropertyResolver
19+
{
20+
/** @return array<Validator> */
21+
public function resolve(ReflectionProperty $property, Attributes $attributes): array
22+
{
23+
$validators = [];
24+
foreach ($property->getAttributes(Validator::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
25+
$propertyValidator = $attribute->getName() === Attributes::class ? $attributes : $attribute->newInstance();
26+
$validators[] = $propertyValidator;
27+
}
28+
29+
return $validators;
30+
}
31+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Validation\Validators\Attributes;
12+
13+
use ReflectionProperty;
14+
use Respect\Validation\Validator;
15+
use Respect\Validation\Validators\Attributes;
16+
17+
/**
18+
* Resolves validators from properties.
19+
*/
20+
interface PropertyResolver
21+
{
22+
/** @return array<Validator> */
23+
public function resolve(ReflectionProperty $property, Attributes $attributes): array;
24+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Validation\Test\Stubs;
12+
13+
use ReflectionProperty;
14+
use Respect\Validation\Validator;
15+
use Respect\Validation\Validators\Attributes;
16+
use Respect\Validation\Validators\Attributes\PropertyResolver;
17+
18+
use function array_values;
19+
20+
final class StubPropertyResolver implements PropertyResolver
21+
{
22+
/** @var list<Validator> */
23+
private readonly array $validators;
24+
25+
public function __construct(Validator ...$validators)
26+
{
27+
$this->validators = array_values($validators);
28+
}
29+
30+
/** @return list<Validator> */
31+
public function resolve(ReflectionProperty $property, Attributes $attributes): array
32+
{
33+
return $this->validators;
34+
}
35+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Validation\Test\Stubs;
12+
13+
final class WithClassTypedProperty
14+
{
15+
public function __construct(
16+
public NestedAddress $value,
17+
) {
18+
}
19+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* SPDX-License-Identifier: MIT
5+
* SPDX-FileCopyrightText: (c) Respect Project Contributors
6+
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
7+
*/
8+
9+
declare(strict_types=1);
10+
11+
namespace Respect\Validation\Test\Stubs;
12+
13+
use Respect\Validation\Validators as Rule;
14+
15+
final class WithExplicitAttributesAttributeProperty
16+
{
17+
public function __construct(
18+
#[Rule\Attributes]
19+
public NestedAddress $address,
20+
) {
21+
}
22+
}

0 commit comments

Comments
 (0)