diff --git a/library/Rules/AllOf.php b/library/Rules/AllOf.php index 19452103c..63db80047 100644 --- a/library/Rules/AllOf.php +++ b/library/Rules/AllOf.php @@ -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', diff --git a/library/Rules/AnyOf.php b/library/Rules/AnyOf.php index feea7b80a..82be395fc 100644 --- a/library/Rules/AnyOf.php +++ b/library/Rules/AnyOf.php @@ -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', diff --git a/library/Rules/Attributes.php b/library/Rules/Attributes.php index c89d5bf9a..7aad2c868 100644 --- a/library/Rules/Attributes.php +++ b/library/Rules/Attributes.php @@ -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(); diff --git a/library/Rules/Call.php b/library/Rules/Call.php index 1d27a58b1..c71f7117b 100644 --- a/library/Rules/Call.php +++ b/library/Rules/Call.php @@ -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}}', diff --git a/library/Rules/Circuit.php b/library/Rules/Circuit.php index 11bca0346..7a7dfa8e8 100644 --- a/library/Rules/Circuit.php +++ b/library/Rules/Circuit.php @@ -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 diff --git a/library/Rules/Lazy.php b/library/Rules/Lazy.php index 3f6527263..8c9bf42de 100644 --- a/library/Rules/Lazy.php +++ b/library/Rules/Lazy.php @@ -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 */ diff --git a/library/Rules/Named.php b/library/Rules/Named.php index 51bd75977..bebb85093 100644 --- a/library/Rules/Named.php +++ b/library/Rules/Named.php @@ -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( diff --git a/library/Rules/NoneOf.php b/library/Rules/NoneOf.php index 5b1b571db..81ad3f639 100644 --- a/library/Rules/NoneOf.php +++ b/library/Rules/NoneOf.php @@ -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', diff --git a/library/Rules/Not.php b/library/Rules/Not.php index 7221c33b1..fc6cc73bd 100644 --- a/library/Rules/Not.php +++ b/library/Rules/Not.php @@ -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 diff --git a/library/Rules/OneOf.php b/library/Rules/OneOf.php index b47375bd3..0b558fa39 100644 --- a/library/Rules/OneOf.php +++ b/library/Rules/OneOf.php @@ -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', diff --git a/library/Rules/Templated.php b/library/Rules/Templated.php index 27bb9d085..3af0df640 100644 --- a/library/Rules/Templated.php +++ b/library/Rules/Templated.php @@ -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 $parameters */ diff --git a/library/Rules/When.php b/library/Rules/When.php index 3bd0bb7ec..4e3348f94 100644 --- a/library/Rules/When.php +++ b/library/Rules/When.php @@ -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; diff --git a/tests/feature/Rules/AttributesTest.php b/tests/feature/Rules/AttributesTest.php index 497f25234..4d855452a 100644 --- a/tests/feature/Rules/AttributesTest.php +++ b/tests/feature/Rules/AttributesTest.php @@ -10,14 +10,14 @@ use Respect\Validation\Test\Stubs\WithAttributes; test('Default', expectAll( - fn() => v::attributes()->assert(new WithAttributes('', 'john.doe@gmail.com', '2024-06-23')), + fn() => v::attributes()->assert(new WithAttributes('', '2024-06-23', 'john.doe@gmail.com')), '`.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', 'john.doe@gmail.com', '2024-06-23', '+1234567890')), + fn() => v::attributes()->assert(new WithAttributes('John Doe', '2024-06-23', 'john.doe@gmail.com', '+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'], @@ -31,39 +31,56 @@ )); test('Nullable', expectAll( - fn() => v::attributes()->assert(new WithAttributes('John Doe', 'john.doe@gmail.com', '2024-06-23', 'not a phone number')), + fn() => v::attributes()->assert(new WithAttributes('John Doe', '2024-06-23', 'john.doe@gmail.com', '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', 'john.doe@gmail.com', '22 years ago')), + fn() => v::attributes()->assert(new WithAttributes('John Doe', '22 years ago', 'john.doe@gmail.com')), '`.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"'], diff --git a/tests/fixtures/data-provider.php b/tests/fixtures/data-provider.php index 80d3df00d..e3053f385 100644 --- a/tests/fixtures/data-provider.php +++ b/tests/fixtures/data-provider.php @@ -140,7 +140,7 @@ 'tags' => ['objectType', 'withoutAttributes'], ], 'object with Rule attributes' => [ - 'value' => [new WithAttributes('John Doe', 'john.doe@gmail.com', '1912-06-23')], + 'value' => [new WithAttributes('John Doe', '1912-06-23', 'john.doe@gmail.com')], 'tags' => ['objectType', 'withAttributes'], ], 'anonymous class' => [ diff --git a/tests/library/Stubs/WithAttributes.php b/tests/library/Stubs/WithAttributes.php index 19f49a6fe..4f1996489 100644 --- a/tests/library/Stubs/WithAttributes.php +++ b/tests/library/Stubs/WithAttributes.php @@ -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, ) { diff --git a/tests/unit/Rules/AttributesTest.php b/tests/unit/Rules/AttributesTest.php index f52b5728e..748cc7196 100644 --- a/tests/unit/Rules/AttributesTest.php +++ b/tests/unit/Rules/AttributesTest.php @@ -55,13 +55,13 @@ public static function providerForObjectsWithValidPropertyValues(): array 'All' => [ new WithAttributes( 'John Doe', - 'john.doe@gmail.com', '2020-06-23', + 'john.doe@gmail.com', '+31206241111', 'Amstel 1 1011 PN AMSTERDAM Noord-Holland' ), ], - 'Only required' => [new WithAttributes('Jane Doe', 'janedoe@yahoo.com', '2017-11-30')], + 'Only required' => [new WithAttributes('Jane Doe', '2017-11-30', 'janedoe@yahoo.com')], ]; } @@ -69,11 +69,12 @@ public static function providerForObjectsWithValidPropertyValues(): array public static function providerForObjectsWithInvalidPropertyValues(): array { return [ - [new WithAttributes('', 'not an email', 'not a date', 'not a phone number')], - [new WithAttributes('', 'john.doe@gmail.com', '1912-06-23', '+1234567890')], - [new WithAttributes('John Doe', 'not an email', '1912-06-23', '+1234567890')], - [new WithAttributes('John Doe', 'john.doe@gmail.com', 'not a date', '+1234567890')], - [new WithAttributes('John Doe', 'john.doe@gmail.com', '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', 'john.doe@gmail.com', '+1234567890')], + [new WithAttributes('John Doe', '1912-06-23', 'not an email', '+1234567890')], + [new WithAttributes('John Doe', 'not a date', 'john.doe@gmail.com', '+1234567890')], + [new WithAttributes('John Doe', '1912-06-23', 'john.doe@gmail.com', 'not a phone number')], ]; } }