Skip to content

Commit

Permalink
Allow to use certain rules as class attributes
Browse files Browse the repository at this point in the history
There are a few cases in which we want to validate the object as a
whole, and that validation could be attached to the class as a PHP
attribute. This commit enables that capability and changes a few rules
to be class attributes.
  • Loading branch information
henriquemoody committed Jan 16, 2025
1 parent 848724e commit 042c8ee
Show file tree
Hide file tree
Showing 16 changed files with 62 additions and 41 deletions.
2 changes: 1 addition & 1 deletion library/Rules/AllOf.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
use function array_reduce;
use function count;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
#[Template(
'{{name}} must pass the rules',
'{{name}} must pass the rules',
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/AnyOf.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use function array_map;
use function array_reduce;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
#[Template(
'{{name}} must pass at least one of the rules',
'{{name}} must pass at least one of the rules',
Expand Down
6 changes: 5 additions & 1 deletion library/Rules/Attributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ public function evaluate(mixed $input): Result
}

$rules = [];
foreach ((new ReflectionObject($input))->getProperties() as $property) {
$reflection = new ReflectionObject($input);
foreach ($reflection->getAttributes(Rule::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$rules[] = $attribute->newInstance();
}
foreach ($reflection->getProperties() as $property) {
$childrenRules = [];
foreach ($property->getAttributes(Rule::class, ReflectionAttribute::IS_INSTANCEOF) as $attribute) {
$childrenRules[] = $attribute->newInstance();
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/Call.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use function restore_error_handler;
use function set_error_handler;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
#[Template(
'{{input}} must be a suitable argument for {{callable}}',
'{{input}} must not be a suitable argument for {{callable}}',
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/Circuit.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Composite;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Circuit extends Composite
{
public function evaluate(mixed $input): Result
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/Lazy.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

use function call_user_func;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Lazy extends Standard
{
/** @var callable(mixed): Rule */
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/Named.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use Respect\Validation\Rules\Core\Nameable;
use Respect\Validation\Rules\Core\Wrapper;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Named extends Wrapper implements Nameable
{
public function __construct(
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/NoneOf.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
use function array_reduce;
use function count;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
#[Template(
'{{name}} must pass the rules',
'{{name}} must pass the rules',
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/Not.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
use Respect\Validation\Result;
use Respect\Validation\Rules\Core\Wrapper;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Not extends Wrapper
{
public function evaluate(mixed $input): Result
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/OneOf.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use function count;
use function usort;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
#[Template(
'{{name}} must pass one of the rules',
'{{name}} must pass one of the rules',
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/Templated.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use Respect\Validation\Rule;
use Respect\Validation\Rules\Core\Wrapper;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class Templated extends Wrapper
{
/** @param array<string, mixed> $parameters */
Expand Down
2 changes: 1 addition & 1 deletion library/Rules/When.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
use Respect\Validation\Rule;
use Respect\Validation\Rules\Core\Standard;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::IS_REPEATABLE)]
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
final class When extends Standard
{
private readonly Rule $else;
Expand Down
35 changes: 26 additions & 9 deletions tests/feature/Rules/AttributesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
use Respect\Validation\Test\Stubs\WithAttributes;

test('Default', expectAll(
fn() => v::attributes()->assert(new WithAttributes('', '[email protected]', '2024-06-23')),
fn() => v::attributes()->assert(new WithAttributes('', '2024-06-23', '[email protected]')),
'`.name` must not be empty',
'- `.name` must not be empty',
['name' => '`.name` must not be empty'],
));

test('Inverted', expectAll(
fn() => v::attributes()->assert(new WithAttributes('John Doe', '[email protected]', '2024-06-23', '+1234567890')),
fn() => v::attributes()->assert(new WithAttributes('John Doe', '2024-06-23', '[email protected]', '+1234567890')),
'`.phone` must be a valid telephone number or must be null',
'- `.phone` must be a valid telephone number or must be null',
['phone' => '`.phone` must be a valid telephone number or must be null'],
Expand All @@ -31,39 +31,56 @@
));

test('Nullable', expectAll(
fn() => v::attributes()->assert(new WithAttributes('John Doe', '[email protected]', '2024-06-23', 'not a phone number')),
fn() => v::attributes()->assert(new WithAttributes('John Doe', '2024-06-23', '[email protected]', 'not a phone number')),
'`.phone` must be a valid telephone number or must be null',
'- `.phone` must be a valid telephone number or must be null',
['phone' => '`.phone` must be a valid telephone number or must be null'],
));

test('Multiple attributes, all failed', expectAll(
fn() => v::attributes()->assert(new WithAttributes('', 'not an email', 'not a date', 'not a phone number')),
fn() => v::attributes()->assert(new WithAttributes('', 'not a date', 'not an email', 'not a phone number')),
'`.name` must not be empty',
<<<'FULL_MESSAGE'
- `Respect\Validation\Test\Stubs\WithAttributes { +$name="" +$email="not an email" +$birthdate="not a date" +$phone ... }` must pass all the rules
- `Respect\Validation\Test\Stubs\WithAttributes { +$name="" +$birthdate="not a date" +$email="not an email" +$phone ... }` must pass the rules
- `.name` must not be empty
- `.email` must be a valid email address
- `.birthdate` must pass all the rules
- `.birthdate` must be a valid date in the format "2005-12-30"
- For comparison with now, `.birthdate` must be a valid datetime
- `.email` must be a valid email address or must be null
- `.phone` must be a valid telephone number or must be null
FULL_MESSAGE,
[
'__root__' => '`Respect\Validation\Test\Stubs\WithAttributes { +$name="" +$email="not an email" +$birthdate="not a date" +$phone ... }` must pass all the rules',
'__root__' => '`Respect\Validation\Test\Stubs\WithAttributes { +$name="" +$birthdate="not a date" +$email="not an email" +$phone ... }` must pass the rules',
'name' => '`.name` must not be empty',
'email' => '`.email` must be a valid email address',
'birthdate' => [
'__root__' => '`.birthdate` must pass all the rules',
'date' => '`.birthdate` must be a valid date in the format "2005-12-30"',
'dateTimeDiffLessThanOrEqual' => 'For comparison with now, `.birthdate` must be a valid datetime',
],
'email' => '`.email` must be a valid email address or must be null',
'phone' => '`.phone` must be a valid telephone number or must be null',
],
));

test('Failed attributes on the class', expectAll(
fn() => v::attributes()->assert(new WithAttributes('John Doe', '2024-06-23')),
'`.email` must be defined',
<<<'FULL_MESSAGE'
- `Respect\Validation\Test\Stubs\WithAttributes { +$name="John Doe" +$birthdate="2024-06-23" +$email=null +$phone=n ... }` must pass at least one of the rules
- `.email` must be defined
- `.phone` must be defined
FULL_MESSAGE,
[
'anyOf' => [
'__root__' => '`Respect\Validation\Test\Stubs\WithAttributes { +$name="John Doe" +$birthdate="2024-06-23" +$email=null +$phone=n ... }` must pass at least one of the rules',
'email' => '`.email` must be defined',
'phone' => '`.phone` must be defined',
],
],
));

test('Multiple attributes, one failed', expectAll(
fn() => v::attributes()->assert(new WithAttributes('John Doe', '[email protected]', '22 years ago')),
fn() => v::attributes()->assert(new WithAttributes('John Doe', '22 years ago', '[email protected]')),
'`.birthdate` must be a valid date in the format "2005-12-30"',
'- `.birthdate` must be a valid date in the format "2005-12-30"',
['birthdate' => '`.birthdate` must be a valid date in the format "2005-12-30"'],
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/data-provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@
'tags' => ['objectType', 'withoutAttributes'],
],
'object with Rule attributes' => [
'value' => [new WithAttributes('John Doe', '[email protected]', '1912-06-23')],
'value' => [new WithAttributes('John Doe', '1912-06-23', '[email protected]')],
'tags' => ['objectType', 'withAttributes'],
],
'anonymous class' => [
Expand Down
23 changes: 11 additions & 12 deletions tests/library/Stubs/WithAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,23 @@

namespace Respect\Validation\Test\Stubs;

use Respect\Validation\Rules\Date;
use Respect\Validation\Rules\DateTimeDiff;
use Respect\Validation\Rules\Email;
use Respect\Validation\Rules\LessThanOrEqual;
use Respect\Validation\Rules\NotEmpty;
use Respect\Validation\Rules\Phone;
use Respect\Validation\Rules as Rule;

#[Rule\AnyOf(
new Rule\Property('email', new Rule\NotUndef()),
new Rule\Property('phone', new Rule\NotUndef()),
)]
final class WithAttributes
{
public function __construct(
#[NotEmpty]
#[Rule\NotEmpty]
public string $name,
#[Email]
public string $email,
#[Date('Y-m-d')]
#[DateTimeDiff('years', new LessThanOrEqual(25))]
#[Rule\Date('Y-m-d')]
#[Rule\DateTimeDiff('years', new Rule\LessThanOrEqual(25))]
public string $birthdate,
#[Phone]
#[Rule\Email]
public ?string $email = null,
#[Rule\Phone]
public ?string $phone = null,
public ?string $address = null,
) {
Expand Down
15 changes: 8 additions & 7 deletions tests/unit/Rules/AttributesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,26 @@ public static function providerForObjectsWithValidPropertyValues(): array
'All' => [
new WithAttributes(
'John Doe',
'[email protected]',
'2020-06-23',
'[email protected]',
'+31206241111',
'Amstel 1 1011 PN AMSTERDAM Noord-Holland'
),
],
'Only required' => [new WithAttributes('Jane Doe', '[email protected]', '2017-11-30')],
'Only required' => [new WithAttributes('Jane Doe', '2017-11-30', '[email protected]')],
];
}

/** @return array<array{object}> */
public static function providerForObjectsWithInvalidPropertyValues(): array
{
return [
[new WithAttributes('', 'not an email', 'not a date', 'not a phone number')],
[new WithAttributes('', '[email protected]', '1912-06-23', '+1234567890')],
[new WithAttributes('John Doe', 'not an email', '1912-06-23', '+1234567890')],
[new WithAttributes('John Doe', '[email protected]', 'not a date', '+1234567890')],
[new WithAttributes('John Doe', '[email protected]', '1912-06-23', 'not a phone number')],
[new WithAttributes('Jane Doe', '2017-11-30')],
[new WithAttributes('', 'not a date', 'not an email', 'not a phone number')],
[new WithAttributes('', '1912-06-23', '[email protected]', '+1234567890')],
[new WithAttributes('John Doe', '1912-06-23', 'not an email', '+1234567890')],
[new WithAttributes('John Doe', 'not a date', '[email protected]', '+1234567890')],
[new WithAttributes('John Doe', '1912-06-23', '[email protected]', 'not a phone number')],
];
}
}

0 comments on commit 042c8ee

Please sign in to comment.