From 20cc1a66b15a86a5ff45a85eff682d0ed6a276ba Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Sat, 8 Mar 2025 22:53:15 +0100 Subject: [PATCH 01/27] [12.x] introduce `Rule::oneOf()` (#https://github.com/laravel/framework/discussions/54880) --- .../Translation/lang/en/validation.php | 1 + src/Illuminate/Validation/Rule.php | 12 ++ src/Illuminate/Validation/Rules/OneOf.php | 99 +++++++++ tests/Validation/ValidationOneOfRuleTest.php | 196 ++++++++++++++++++ 4 files changed, 308 insertions(+) create mode 100644 src/Illuminate/Validation/Rules/OneOf.php create mode 100644 tests/Validation/ValidationOneOfRuleTest.php diff --git a/src/Illuminate/Translation/lang/en/validation.php b/src/Illuminate/Translation/lang/en/validation.php index f19bd64ed6c9..5f0c32ea7d32 100644 --- a/src/Illuminate/Translation/lang/en/validation.php +++ b/src/Illuminate/Translation/lang/en/validation.php @@ -117,6 +117,7 @@ 'not_in' => 'The selected :attribute is invalid.', 'not_regex' => 'The :attribute field format is invalid.', 'numeric' => 'The :attribute field must be a number.', + 'oneof' => 'The :attribute field does not match any of the allowed rule sets.', 'password' => [ 'letters' => 'The :attribute field must contain at least one letter.', 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.', diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index 44bb2b4347b5..26fd2875758e 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -18,6 +18,7 @@ use Illuminate\Validation\Rules\In; use Illuminate\Validation\Rules\NotIn; use Illuminate\Validation\Rules\Numeric; +use Illuminate\Validation\Rules\OneOf; use Illuminate\Validation\Rules\ProhibitedIf; use Illuminate\Validation\Rules\RequiredIf; use Illuminate\Validation\Rules\Unique; @@ -246,6 +247,17 @@ public static function numeric() return new Numeric; } + /** + * Get a oneof rule builder instance. + * + * @param \Illuminate\Contracts\Validation\ValidationRule[] $values + * @return \Illuminate\Validation\Rules\OneOf + */ + public static function oneOf($rules) + { + return new OneOf($rules); + } + /** * Compile a set of rules for an attribute. * diff --git a/src/Illuminate/Validation/Rules/OneOf.php b/src/Illuminate/Validation/Rules/OneOf.php new file mode 100644 index 000000000000..b7871307b390 --- /dev/null +++ b/src/Illuminate/Validation/Rules/OneOf.php @@ -0,0 +1,99 @@ +ruleSets = $ruleSets; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + $this->messages = []; + + foreach ($this->ruleSets as $ruleSet) { + $this->validator->setRules($ruleSet); + $this->validator->setData($value); + if ($this->validator->passes()) return true; + } + + array_push($this->messages, "The {$attribute} field does not match any of the allowed rule sets."); + return false; + } + + /** + * Get the validation error message. + * + * @return array + */ + public function message() + { + return $this->messages; + } + + /** + * Set the current validator. + * + * @param \Illuminate\Contracts\Validation\Validator $validator + * @return $this + */ + public function setValidator($validator) + { + $this->validator = $validator; + + return $this; + } +} diff --git a/tests/Validation/ValidationOneOfRuleTest.php b/tests/Validation/ValidationOneOfRuleTest.php new file mode 100644 index 000000000000..f06c4bbac75c --- /dev/null +++ b/tests/Validation/ValidationOneOfRuleTest.php @@ -0,0 +1,196 @@ +translator = $this->getIlluminateArrayTranslator(); + + $this->rules = [ + [ + "p1" => ["required", Rule::in([ArrayKeysBacked::key_1])], + "p2" => ["required"], + "p3" => ["required", "url:http,https"], + "p4" => ["sometimes", "required"] + ], + [ + "p1" => ["required", Rule::in([ArrayKeysBacked::key_2])], + "p2" => ["required", "url:http,https"], + ], + [ + "p1" => ["required", Rule::in([ArrayKeysBacked::key_3])], + "p2" => ["required"] + ], + [ + "p1" => ["required", Rule::in([StringStatus::pending])], + "p2" => ["required", "numeric"], + "p3" => ["nullable", "string"] + ], + [ + "p1" => ["required", Rule::in([StringStatus::done])], + "p2" => ["required", "email"], + "p3" => ["nullable", "alpha"] + ], + ]; + } + + public function testThrowsTypeErrorForInvalidInput() + { + $this->expectException(TypeError::class); + $v = new Validator($this->translator, ['foo' => 'not an array'], ['foo' => Rule::oneOf($this->rules)]); + $v->validate(); + } + + public function testValidatesSuccessfullyWithKey2AndValidP2() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => ArrayKeysBacked::key_2->value, + "p2" => "http://localhost:8000/v1" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertTrue($validator->passes()); + } + + public function testFailsOnMissingP1() + { + $validator = new Validator($this->translator, ['foo' => [ + "p2" => "http://localhost:8000/v1" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertFalse($validator->passes()); + } + + public function testFailsWhenRequiredP2IsMissingForKey3() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => ArrayKeysBacked::key_3->value + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertFalse($validator->passes()); + } + + public function testValidatesSuccessfullyWithKey3AndP2AsString() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => ArrayKeysBacked::key_3->value, + "p2" => "is a string" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertTrue($validator->passes()); + } + + public function testFailsWithInvalidP1Value() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => "invalid_key", + "p2" => "valid value" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertFalse($validator->passes()); + } + + public function testFailsWithInvalidURLForP2() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => ArrayKeysBacked::key_2->value, + "p2" => "not_a_valid_url" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertFalse($validator->passes()); + } + + public function testFailsWhenP3IsRequiredButInvalid() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => ArrayKeysBacked::key_1->value, + "p2" => "required_value", + "p3" => "invalid_url" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertFalse($validator->passes()); + } + + public function testPassesWhenP3IsValidHttpURL() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => ArrayKeysBacked::key_1->value, + "p2" => "required_value", + "p3" => "http://example.com" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertTrue($validator->passes()); + } + + public function testPassesWithOptionalP4Field() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => ArrayKeysBacked::key_1->value, + "p2" => "required_value", + "p3" => "http://example.com", + "p4" => "optional_value" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertTrue($validator->passes()); + } + + public function testPassesWithoutOptionalP4Field() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => ArrayKeysBacked::key_1->value, + "p2" => "required_value", + "p3" => "https://example.com" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertTrue($validator->passes()); + } + + public function testFailsWithNonNumericP2ForKey4() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => StringStatus::pending->value, + "p2" => "not_a_number" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertFalse($validator->passes()); + } + + public function testPassesWithValidNumericP2ForKey4() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => StringStatus::pending->value, + "p2" => 12345 + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertTrue($validator->passes()); + } + + public function testFailsWithInvalidEmailForKey5() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => StringStatus::done->value, + "p2" => "not_an_email" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertFalse($validator->passes()); + } + + public function testPassesWithValidEmailForKey5() + { + $validator = new Validator($this->translator, ['foo' => [ + "p1" => StringStatus::done->value, + "p2" => "test@example.com" + ]], ['foo' => Rule::oneOf($this->rules)]); + $this->assertTrue($validator->passes()); + } + + + private function getIlluminateArrayTranslator(): Translator + { + return new Translator(new ArrayLoader, 'en'); + } +} From fe882b37f6e661b6373f7ec622d61099f72a3f26 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Sat, 8 Mar 2025 23:12:57 +0100 Subject: [PATCH 02/27] chore: apply styleCI --- src/Illuminate/Validation/Rule.php | 2 +- src/Illuminate/Validation/Rules/OneOf.php | 14 +-- tests/Validation/ValidationOneOfRuleTest.php | 91 ++++++++++---------- 3 files changed, 54 insertions(+), 53 deletions(-) diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index 26fd2875758e..9fbf78fb46b8 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -250,7 +250,7 @@ public static function numeric() /** * Get a oneof rule builder instance. * - * @param \Illuminate\Contracts\Validation\ValidationRule[] $values + * @param \Illuminate\Contracts\Validation\ValidationRule[] $values * @return \Illuminate\Validation\Rules\OneOf */ public static function oneOf($rules) diff --git a/src/Illuminate/Validation/Rules/OneOf.php b/src/Illuminate/Validation/Rules/OneOf.php index b7871307b390..e5f42c9db6b8 100644 --- a/src/Illuminate/Validation/Rules/OneOf.php +++ b/src/Illuminate/Validation/Rules/OneOf.php @@ -4,7 +4,7 @@ use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\ValidatorAwareRule; -use \Illuminate\Validation\Validator; +use Illuminate\Validation\Validator; use InvalidArgumentException; class OneOf implements Rule, ValidatorAwareRule @@ -16,7 +16,6 @@ class OneOf implements Rule, ValidatorAwareRule */ protected $rule = 'oneof'; - /** * The validator performing the validation. * @@ -32,14 +31,14 @@ class OneOf implements Rule, ValidatorAwareRule protected $messages = []; /** - * The rules to match against + * The rules to match against. * * @var Illuminate\Contracts\Validation\ValidationRule[][] */ private array $ruleSets = []; /** - * Sets the validation rules to match against + * Sets the validation rules to match against. * * @param Illuminate\Contracts\Validation\ValidationRule[][] $ruleSets * @@ -47,7 +46,7 @@ class OneOf implements Rule, ValidatorAwareRule */ public function __construct($ruleSets) { - if (!is_array($ruleSets)) { + if (! is_array($ruleSets)) { throw new InvalidArgumentException('The provided value must be an array of validation rules.'); } $this->ruleSets = $ruleSets; @@ -67,10 +66,13 @@ public function passes($attribute, $value) foreach ($this->ruleSets as $ruleSet) { $this->validator->setRules($ruleSet); $this->validator->setData($value); - if ($this->validator->passes()) return true; + if ($this->validator->passes()) { + return true; + } } array_push($this->messages, "The {$attribute} field does not match any of the allowed rule sets."); + return false; } diff --git a/tests/Validation/ValidationOneOfRuleTest.php b/tests/Validation/ValidationOneOfRuleTest.php index f06c4bbac75c..235d3bbad8b1 100644 --- a/tests/Validation/ValidationOneOfRuleTest.php +++ b/tests/Validation/ValidationOneOfRuleTest.php @@ -26,28 +26,28 @@ protected function setUp(): void $this->rules = [ [ - "p1" => ["required", Rule::in([ArrayKeysBacked::key_1])], - "p2" => ["required"], - "p3" => ["required", "url:http,https"], - "p4" => ["sometimes", "required"] + 'p1' => ['required', Rule::in([ArrayKeysBacked::key_1])], + 'p2' => ['required'], + 'p3' => ['required', 'url:http,https'], + 'p4' => ['sometimes', 'required'], ], [ - "p1" => ["required", Rule::in([ArrayKeysBacked::key_2])], - "p2" => ["required", "url:http,https"], + 'p1' => ['required', Rule::in([ArrayKeysBacked::key_2])], + 'p2' => ['required', 'url:http,https'], ], [ - "p1" => ["required", Rule::in([ArrayKeysBacked::key_3])], - "p2" => ["required"] + 'p1' => ['required', Rule::in([ArrayKeysBacked::key_3])], + 'p2' => ['required'], ], [ - "p1" => ["required", Rule::in([StringStatus::pending])], - "p2" => ["required", "numeric"], - "p3" => ["nullable", "string"] + 'p1' => ['required', Rule::in([StringStatus::pending])], + 'p2' => ['required', 'numeric'], + 'p3' => ['nullable', 'string'], ], [ - "p1" => ["required", Rule::in([StringStatus::done])], - "p2" => ["required", "email"], - "p3" => ["nullable", "alpha"] + 'p1' => ['required', Rule::in([StringStatus::done])], + 'p2' => ['required', 'email'], + 'p3' => ['nullable', 'alpha'], ], ]; } @@ -62,8 +62,8 @@ public function testThrowsTypeErrorForInvalidInput() public function testValidatesSuccessfullyWithKey2AndValidP2() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => ArrayKeysBacked::key_2->value, - "p2" => "http://localhost:8000/v1" + 'p1' => ArrayKeysBacked::key_2->value, + 'p2' => 'http://localhost:8000/v1', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertTrue($validator->passes()); } @@ -71,7 +71,7 @@ public function testValidatesSuccessfullyWithKey2AndValidP2() public function testFailsOnMissingP1() { $validator = new Validator($this->translator, ['foo' => [ - "p2" => "http://localhost:8000/v1" + 'p2' => 'http://localhost:8000/v1', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertFalse($validator->passes()); } @@ -79,7 +79,7 @@ public function testFailsOnMissingP1() public function testFailsWhenRequiredP2IsMissingForKey3() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => ArrayKeysBacked::key_3->value + 'p1' => ArrayKeysBacked::key_3->value, ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertFalse($validator->passes()); } @@ -87,8 +87,8 @@ public function testFailsWhenRequiredP2IsMissingForKey3() public function testValidatesSuccessfullyWithKey3AndP2AsString() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => ArrayKeysBacked::key_3->value, - "p2" => "is a string" + 'p1' => ArrayKeysBacked::key_3->value, + 'p2' => 'is a string', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertTrue($validator->passes()); } @@ -96,8 +96,8 @@ public function testValidatesSuccessfullyWithKey3AndP2AsString() public function testFailsWithInvalidP1Value() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => "invalid_key", - "p2" => "valid value" + 'p1' => 'invalid_key', + 'p2' => 'valid value', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertFalse($validator->passes()); } @@ -105,8 +105,8 @@ public function testFailsWithInvalidP1Value() public function testFailsWithInvalidURLForP2() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => ArrayKeysBacked::key_2->value, - "p2" => "not_a_valid_url" + 'p1' => ArrayKeysBacked::key_2->value, + 'p2' => 'not_a_valid_url', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertFalse($validator->passes()); } @@ -114,9 +114,9 @@ public function testFailsWithInvalidURLForP2() public function testFailsWhenP3IsRequiredButInvalid() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => ArrayKeysBacked::key_1->value, - "p2" => "required_value", - "p3" => "invalid_url" + 'p1' => ArrayKeysBacked::key_1->value, + 'p2' => 'required_value', + 'p3' => 'invalid_url', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertFalse($validator->passes()); } @@ -124,9 +124,9 @@ public function testFailsWhenP3IsRequiredButInvalid() public function testPassesWhenP3IsValidHttpURL() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => ArrayKeysBacked::key_1->value, - "p2" => "required_value", - "p3" => "http://example.com" + 'p1' => ArrayKeysBacked::key_1->value, + 'p2' => 'required_value', + 'p3' => 'http://example.com', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertTrue($validator->passes()); } @@ -134,10 +134,10 @@ public function testPassesWhenP3IsValidHttpURL() public function testPassesWithOptionalP4Field() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => ArrayKeysBacked::key_1->value, - "p2" => "required_value", - "p3" => "http://example.com", - "p4" => "optional_value" + 'p1' => ArrayKeysBacked::key_1->value, + 'p2' => 'required_value', + 'p3' => 'http://example.com', + 'p4' => 'optional_value', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertTrue($validator->passes()); } @@ -145,9 +145,9 @@ public function testPassesWithOptionalP4Field() public function testPassesWithoutOptionalP4Field() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => ArrayKeysBacked::key_1->value, - "p2" => "required_value", - "p3" => "https://example.com" + 'p1' => ArrayKeysBacked::key_1->value, + 'p2' => 'required_value', + 'p3' => 'https://example.com', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertTrue($validator->passes()); } @@ -155,8 +155,8 @@ public function testPassesWithoutOptionalP4Field() public function testFailsWithNonNumericP2ForKey4() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => StringStatus::pending->value, - "p2" => "not_a_number" + 'p1' => StringStatus::pending->value, + 'p2' => 'not_a_number', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertFalse($validator->passes()); } @@ -164,8 +164,8 @@ public function testFailsWithNonNumericP2ForKey4() public function testPassesWithValidNumericP2ForKey4() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => StringStatus::pending->value, - "p2" => 12345 + 'p1' => StringStatus::pending->value, + 'p2' => 12345, ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertTrue($validator->passes()); } @@ -173,8 +173,8 @@ public function testPassesWithValidNumericP2ForKey4() public function testFailsWithInvalidEmailForKey5() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => StringStatus::done->value, - "p2" => "not_an_email" + 'p1' => StringStatus::done->value, + 'p2' => 'not_an_email', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertFalse($validator->passes()); } @@ -182,13 +182,12 @@ public function testFailsWithInvalidEmailForKey5() public function testPassesWithValidEmailForKey5() { $validator = new Validator($this->translator, ['foo' => [ - "p1" => StringStatus::done->value, - "p2" => "test@example.com" + 'p1' => StringStatus::done->value, + 'p2' => 'test@example.com', ]], ['foo' => Rule::oneOf($this->rules)]); $this->assertTrue($validator->passes()); } - private function getIlluminateArrayTranslator(): Translator { return new Translator(new ArrayLoader, 'en'); From 2aa5ef9fc7638a0163cccb317b4fdf52f47c6152 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Sat, 8 Mar 2025 23:45:38 +0100 Subject: [PATCH 03/27] feat: add nested oneOf validation test --- tests/Validation/ValidationOneOfRuleTest.php | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Validation/ValidationOneOfRuleTest.php b/tests/Validation/ValidationOneOfRuleTest.php index 235d3bbad8b1..bfe91d85d1c9 100644 --- a/tests/Validation/ValidationOneOfRuleTest.php +++ b/tests/Validation/ValidationOneOfRuleTest.php @@ -17,6 +17,7 @@ class ValidationOneOfRuleTest extends TestCase use CreatesApplication; private array $rules; + private array $nestedRules; private Translator $translator; protected function setUp(): void @@ -50,6 +51,19 @@ protected function setUp(): void 'p3' => ['nullable', 'alpha'], ], ]; + + $this->nestedRules = [ + [ + 'p1' => ['required', Rule::oneOf([ + [ + 'p2' => ['required', 'string'], + 'p3' => ['required', Rule::oneOf([[ + 'p4' => ['nullable', 'string'], + ]])], + ] + ])], + ], + ]; } public function testThrowsTypeErrorForInvalidInput() @@ -59,6 +73,19 @@ public function testThrowsTypeErrorForInvalidInput() $v->validate(); } + public function testValidatesPossibleNesting() + { + $validator = new Validator($this->translator, ['foo' => [ + 'p1' => [ + 'p2' => 'a_string', + 'p3' => [ + 'p4' => 'a_string' + ] + ] + ]], ['foo' => Rule::oneOf($this->nestedRules)]); + $this->assertTrue($validator->passes()); + } + public function testValidatesSuccessfullyWithKey2AndValidP2() { $validator = new Validator($this->translator, ['foo' => [ From e9d7b83683a08e05b5525e90ac03341d3080f98c Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Sat, 8 Mar 2025 23:46:55 +0100 Subject: [PATCH 04/27] chore: apply styleCI --- tests/Validation/ValidationOneOfRuleTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Validation/ValidationOneOfRuleTest.php b/tests/Validation/ValidationOneOfRuleTest.php index bfe91d85d1c9..140ecb111848 100644 --- a/tests/Validation/ValidationOneOfRuleTest.php +++ b/tests/Validation/ValidationOneOfRuleTest.php @@ -60,7 +60,7 @@ protected function setUp(): void 'p3' => ['required', Rule::oneOf([[ 'p4' => ['nullable', 'string'], ]])], - ] + ], ])], ], ]; @@ -79,9 +79,9 @@ public function testValidatesPossibleNesting() 'p1' => [ 'p2' => 'a_string', 'p3' => [ - 'p4' => 'a_string' - ] - ] + 'p4' => 'a_string', + ], + ], ]], ['foo' => Rule::oneOf($this->nestedRules)]); $this->assertTrue($validator->passes()); } From a274de9d010620ddfb1f21e16ba094321a4ee915 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Thu, 13 Mar 2025 15:51:00 +0100 Subject: [PATCH 05/27] refactor: rename `oneof` into `anyof` to fit implementation --- .../Translation/lang/en/validation.php | 2 +- src/Illuminate/Validation/Rule.php | 10 +- .../Validation/Rules/{OneOf.php => AnyOf.php} | 24 +- tests/Validation/ValidationAnyOfRuleTest.php | 121 ++++++++++ tests/Validation/ValidationOneOfRuleTest.php | 222 ------------------ 5 files changed, 138 insertions(+), 241 deletions(-) rename src/Illuminate/Validation/Rules/{OneOf.php => AnyOf.php} (80%) create mode 100644 tests/Validation/ValidationAnyOfRuleTest.php delete mode 100644 tests/Validation/ValidationOneOfRuleTest.php diff --git a/src/Illuminate/Translation/lang/en/validation.php b/src/Illuminate/Translation/lang/en/validation.php index 5f0c32ea7d32..55f75d1e8f9c 100644 --- a/src/Illuminate/Translation/lang/en/validation.php +++ b/src/Illuminate/Translation/lang/en/validation.php @@ -117,7 +117,7 @@ 'not_in' => 'The selected :attribute is invalid.', 'not_regex' => 'The :attribute field format is invalid.', 'numeric' => 'The :attribute field must be a number.', - 'oneof' => 'The :attribute field does not match any of the allowed rule sets.', + 'anyof' => 'The :attribute field does not match any of the allowed rule sets.', 'password' => [ 'letters' => 'The :attribute field must contain at least one letter.', 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.', diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index 9fbf78fb46b8..1f4c880f97dc 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -18,7 +18,7 @@ use Illuminate\Validation\Rules\In; use Illuminate\Validation\Rules\NotIn; use Illuminate\Validation\Rules\Numeric; -use Illuminate\Validation\Rules\OneOf; +use Illuminate\Validation\Rules\AnyOf; use Illuminate\Validation\Rules\ProhibitedIf; use Illuminate\Validation\Rules\RequiredIf; use Illuminate\Validation\Rules\Unique; @@ -248,14 +248,14 @@ public static function numeric() } /** - * Get a oneof rule builder instance. + * Get a anyof rule builder instance. * * @param \Illuminate\Contracts\Validation\ValidationRule[] $values - * @return \Illuminate\Validation\Rules\OneOf + * @return \Illuminate\Validation\Rules\AnyOf */ - public static function oneOf($rules) + public static function anyOf($rules) { - return new OneOf($rules); + return new AnyOf($rules); } /** diff --git a/src/Illuminate/Validation/Rules/OneOf.php b/src/Illuminate/Validation/Rules/AnyOf.php similarity index 80% rename from src/Illuminate/Validation/Rules/OneOf.php rename to src/Illuminate/Validation/Rules/AnyOf.php index e5f42c9db6b8..d10b03130d72 100644 --- a/src/Illuminate/Validation/Rules/OneOf.php +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -4,18 +4,11 @@ use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\ValidatorAwareRule; -use Illuminate\Validation\Validator; +use Illuminate\Support\Facades\Validator; use InvalidArgumentException; -class OneOf implements Rule, ValidatorAwareRule +class AnyOf implements Rule, ValidatorAwareRule { - /** - * The name of the rule. - * - * @var string - */ - protected $rule = 'oneof'; - /** * The validator performing the validation. * @@ -64,14 +57,19 @@ public function passes($attribute, $value) $this->messages = []; foreach ($this->ruleSets as $ruleSet) { - $this->validator->setRules($ruleSet); - $this->validator->setData($value); - if ($this->validator->passes()) { + $validator = Validator::make( + $value, + $ruleSet, + $this->validator->customMessages, + $this->validator->customAttributes + ); + + if ($validator->passes()) { return true; } } - array_push($this->messages, "The {$attribute} field does not match any of the allowed rule sets."); + $this->validator->addFailure($attribute, 'oneof'); return false; } diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php new file mode 100644 index 000000000000..81b7a62b74b7 --- /dev/null +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -0,0 +1,121 @@ +expectException(TypeError::class); + $v = new Validator(resolve('translator'), ['foo' => 'not an array'], ['foo' => Rule::anyOf($this->ruleSet1)]); + $v->validate(); + } + + public function testValidatesPossibleNesting() + { + $validator = new Validator(resolve('translator'), ['foo' => [ + 'p1' => [ + 'p2' => 'a_string', + 'p3' => [ + 'p4' => 'a_string', + ], + ], + ]], ['foo' => Rule::anyOf($this->nestedRules)]); + $this->assertTrue($validator->passes()); + } + + public function testValidatesSuccessfullyWithKey2AndValidP2() + { + $validator = new Validator(resolve('translator'), ['foo' => [ + 'p1' => ArrayKeysBacked::key_2->value, + 'p2' => 'http://localhost:8000/v1', + ]], ['foo' => Rule::anyOf($this->ruleSet1)]); + $this->assertTrue($validator->passes()); + } + + public function testFailsOnMissingP1() + { + $validator = new Validator(resolve('translator'), ['foo' => [ + 'p2' => 'http://localhost:8000/v1', + ]], ['foo' => Rule::anyOf($this->ruleSet1)]); + $this->assertFalse($validator->passes()); + } + + protected function setUp(): void + { + parent::setUp(); + + $container = Container::getInstance(); + $container->bind('translator', function () { + return new Translator( + new ArrayLoader, + 'en' + ); + }); + + Facade::setFacadeApplication($container); + (new ValidationServiceProvider($container))->register(); + + $this->ruleSet1 = [ + [ + 'p1' => ['required', Rule::in([ArrayKeysBacked::key_1])], + 'p2' => ['required'], + 'p3' => ['required', 'url:http,https'], + 'p4' => ['sometimes', 'required'], + ], + [ + 'p1' => ['required', Rule::in([ArrayKeysBacked::key_2])], + 'p2' => ['required', 'url:http,https'], + ], + [ + 'p1' => ['required', Rule::in([ArrayKeysBacked::key_3])], + 'p2' => ['required'], + ], + [ + 'p1' => ['required', Rule::in([StringStatus::pending])], + 'p2' => ['required', 'numeric'], + 'p3' => ['nullable', 'string'], + ], + [ + 'p1' => ['required', Rule::in([StringStatus::done])], + 'p2' => ['required', 'email'], + 'p3' => ['nullable', 'alpha'], + ], + ]; + + $this->nestedRules = [ + [ + 'p1' => ['required', Rule::anyOf([ + [ + 'p2' => ['required', 'string'], + 'p3' => ['required', Rule::anyOf([[ + 'p4' => ['nullable', 'string'], + ]])], + ], + ])], + ], + ]; + } + + protected function tearDown(): void + { + Container::setInstance(null); + Facade::clearResolvedInstances(); + Facade::setFacadeApplication(null); + } +} diff --git a/tests/Validation/ValidationOneOfRuleTest.php b/tests/Validation/ValidationOneOfRuleTest.php deleted file mode 100644 index 140ecb111848..000000000000 --- a/tests/Validation/ValidationOneOfRuleTest.php +++ /dev/null @@ -1,222 +0,0 @@ -translator = $this->getIlluminateArrayTranslator(); - - $this->rules = [ - [ - 'p1' => ['required', Rule::in([ArrayKeysBacked::key_1])], - 'p2' => ['required'], - 'p3' => ['required', 'url:http,https'], - 'p4' => ['sometimes', 'required'], - ], - [ - 'p1' => ['required', Rule::in([ArrayKeysBacked::key_2])], - 'p2' => ['required', 'url:http,https'], - ], - [ - 'p1' => ['required', Rule::in([ArrayKeysBacked::key_3])], - 'p2' => ['required'], - ], - [ - 'p1' => ['required', Rule::in([StringStatus::pending])], - 'p2' => ['required', 'numeric'], - 'p3' => ['nullable', 'string'], - ], - [ - 'p1' => ['required', Rule::in([StringStatus::done])], - 'p2' => ['required', 'email'], - 'p3' => ['nullable', 'alpha'], - ], - ]; - - $this->nestedRules = [ - [ - 'p1' => ['required', Rule::oneOf([ - [ - 'p2' => ['required', 'string'], - 'p3' => ['required', Rule::oneOf([[ - 'p4' => ['nullable', 'string'], - ]])], - ], - ])], - ], - ]; - } - - public function testThrowsTypeErrorForInvalidInput() - { - $this->expectException(TypeError::class); - $v = new Validator($this->translator, ['foo' => 'not an array'], ['foo' => Rule::oneOf($this->rules)]); - $v->validate(); - } - - public function testValidatesPossibleNesting() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => [ - 'p2' => 'a_string', - 'p3' => [ - 'p4' => 'a_string', - ], - ], - ]], ['foo' => Rule::oneOf($this->nestedRules)]); - $this->assertTrue($validator->passes()); - } - - public function testValidatesSuccessfullyWithKey2AndValidP2() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => ArrayKeysBacked::key_2->value, - 'p2' => 'http://localhost:8000/v1', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertTrue($validator->passes()); - } - - public function testFailsOnMissingP1() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p2' => 'http://localhost:8000/v1', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertFalse($validator->passes()); - } - - public function testFailsWhenRequiredP2IsMissingForKey3() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => ArrayKeysBacked::key_3->value, - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertFalse($validator->passes()); - } - - public function testValidatesSuccessfullyWithKey3AndP2AsString() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => ArrayKeysBacked::key_3->value, - 'p2' => 'is a string', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertTrue($validator->passes()); - } - - public function testFailsWithInvalidP1Value() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => 'invalid_key', - 'p2' => 'valid value', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertFalse($validator->passes()); - } - - public function testFailsWithInvalidURLForP2() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => ArrayKeysBacked::key_2->value, - 'p2' => 'not_a_valid_url', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertFalse($validator->passes()); - } - - public function testFailsWhenP3IsRequiredButInvalid() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => ArrayKeysBacked::key_1->value, - 'p2' => 'required_value', - 'p3' => 'invalid_url', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertFalse($validator->passes()); - } - - public function testPassesWhenP3IsValidHttpURL() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => ArrayKeysBacked::key_1->value, - 'p2' => 'required_value', - 'p3' => 'http://example.com', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertTrue($validator->passes()); - } - - public function testPassesWithOptionalP4Field() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => ArrayKeysBacked::key_1->value, - 'p2' => 'required_value', - 'p3' => 'http://example.com', - 'p4' => 'optional_value', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertTrue($validator->passes()); - } - - public function testPassesWithoutOptionalP4Field() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => ArrayKeysBacked::key_1->value, - 'p2' => 'required_value', - 'p3' => 'https://example.com', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertTrue($validator->passes()); - } - - public function testFailsWithNonNumericP2ForKey4() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => StringStatus::pending->value, - 'p2' => 'not_a_number', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertFalse($validator->passes()); - } - - public function testPassesWithValidNumericP2ForKey4() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => StringStatus::pending->value, - 'p2' => 12345, - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertTrue($validator->passes()); - } - - public function testFailsWithInvalidEmailForKey5() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => StringStatus::done->value, - 'p2' => 'not_an_email', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertFalse($validator->passes()); - } - - public function testPassesWithValidEmailForKey5() - { - $validator = new Validator($this->translator, ['foo' => [ - 'p1' => StringStatus::done->value, - 'p2' => 'test@example.com', - ]], ['foo' => Rule::oneOf($this->rules)]); - $this->assertTrue($validator->passes()); - } - - private function getIlluminateArrayTranslator(): Translator - { - return new Translator(new ArrayLoader, 'en'); - } -} From ed65fa2ead8b7f99c0b451217738c3bad9095113 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Thu, 13 Mar 2025 16:05:26 +0100 Subject: [PATCH 06/27] fix: wrong failure message --- src/Illuminate/Validation/Rules/AnyOf.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/Rules/AnyOf.php b/src/Illuminate/Validation/Rules/AnyOf.php index d10b03130d72..413a323524c0 100644 --- a/src/Illuminate/Validation/Rules/AnyOf.php +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -69,7 +69,7 @@ public function passes($attribute, $value) } } - $this->validator->addFailure($attribute, 'oneof'); + $this->validator->addFailure($attribute, 'anyof'); return false; } From 735a8f677ef721434c21d7c74265fb083712fc25 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Thu, 13 Mar 2025 17:28:55 +0100 Subject: [PATCH 07/27] feat: update base tests --- src/Illuminate/Validation/Rule.php | 1 + tests/Validation/ValidationAnyOfRuleTest.php | 160 +++++++++++++------ 2 files changed, 114 insertions(+), 47 deletions(-) diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index 1f4c880f97dc..d65873ad9d28 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -252,6 +252,7 @@ public static function numeric() * * @param \Illuminate\Contracts\Validation\ValidationRule[] $values * @return \Illuminate\Validation\Rules\AnyOf + * @throws \InvalidArgumentException */ public static function anyOf($rules) { diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index 81b7a62b74b7..2ca901c50a56 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -14,88 +14,136 @@ include_once 'Enums.php'; +enum Validators: string +{ + case EMAIL = 'email'; + case URL = 'url'; + case IN = 'in'; +} + class ValidationAnyOfRuleTest extends TestCase { - private array $ruleSet1; + private array $ruleSets; private array $nestedRules; public function testThrowsTypeErrorForInvalidInput() { $this->expectException(TypeError::class); - $v = new Validator(resolve('translator'), ['foo' => 'not an array'], ['foo' => Rule::anyOf($this->ruleSet1)]); - $v->validate(); + $validator = new Validator(resolve('translator'), [ + 'foo' => 'not an array' + ], ['foo' => Rule::anyOf([[]])]); + + $validator->validate(); } - public function testValidatesPossibleNesting() + public function testValidEmailValidation() { $validator = new Validator(resolve('translator'), ['foo' => [ - 'p1' => [ - 'p2' => 'a_string', - 'p3' => [ - 'p4' => 'a_string', - ], - ], - ]], ['foo' => Rule::anyOf($this->nestedRules)]); + 'type' => 'email', + 'email' => 'test@example.com', + ]], ['foo' => Rule::anyOf($this->ruleSets)]); + $this->assertTrue($validator->passes()); } - public function testValidatesSuccessfullyWithKey2AndValidP2() + public function testInvalidEmailValidation() + { + $validator = new Validator(resolve('translator'), ['foo' => [ + 'type' => 'email', + 'email' => 'invalid-email', + ]], ['foo' => Rule::anyOf($this->ruleSets)]); + + $this->assertFalse($validator->passes()); + } + + public function testValidUrlValidation() { $validator = new Validator(resolve('translator'), ['foo' => [ - 'p1' => ArrayKeysBacked::key_2->value, - 'p2' => 'http://localhost:8000/v1', - ]], ['foo' => Rule::anyOf($this->ruleSet1)]); + 'type' => 'url', + 'url' => 'https://example.com', + ]], ['foo' => Rule::anyOf($this->ruleSets)]); + $this->assertTrue($validator->passes()); } - public function testFailsOnMissingP1() + public function testInvalidUrlValidation() { $validator = new Validator(resolve('translator'), ['foo' => [ - 'p2' => 'http://localhost:8000/v1', - ]], ['foo' => Rule::anyOf($this->ruleSet1)]); + 'type' => 'url', + 'url' => 'not-a-url', + ]], ['foo' => Rule::anyOf($this->ruleSets)]); + $this->assertFalse($validator->passes()); } - protected function setUp(): void + public function testValidInValidation() { - parent::setUp(); + $validator = new Validator(resolve('translator'), ['foo' => [ + 'type' => 'in', + 'in' => 'key_1', + ]], ['foo' => Rule::anyOf($this->ruleSets)]); - $container = Container::getInstance(); - $container->bind('translator', function () { - return new Translator( - new ArrayLoader, - 'en' - ); - }); + $this->assertTrue($validator->passes()); + } - Facade::setFacadeApplication($container); - (new ValidationServiceProvider($container))->register(); + public function testInvalidInValidation() + { + $validator = new Validator(resolve('translator'), ['foo' => [ + 'type' => 'in', + 'in' => 'unexpected_value', + ]], ['foo' => Rule::anyOf($this->ruleSets)]); - $this->ruleSet1 = [ - [ - 'p1' => ['required', Rule::in([ArrayKeysBacked::key_1])], - 'p2' => ['required'], - 'p3' => ['required', 'url:http,https'], - 'p4' => ['sometimes', 'required'], + $this->assertFalse($validator->passes()); + } + + public function testValidNestedValidation() + { + $validator = new Validator(resolve('translator'), [ + 'foo' => [ + 'p1' => [ + 'p2' => 'a_valid_string', + 'p3' => [ + 'p4' => 'another_valid_string', + ], + ], ], - [ - 'p1' => ['required', Rule::in([ArrayKeysBacked::key_2])], - 'p2' => ['required', 'url:http,https'], + ], ['foo' => Rule::anyOf($this->nestedRules)]); + + $this->assertTrue($validator->passes()); + } + + public function testInvalidNestedValidation() + { + $validator = new Validator(resolve('translator'), [ + 'foo' => [ + 'p1' => [ + 'p2' => '', // required field left empty + 'p3' => [ + 'p4' => 'valid_string', + ], + ], ], + ], ['foo' => Rule::anyOf($this->nestedRules)]); + + $this->assertFalse($validator->passes()); + } + + + protected function setUpRuleSets() + { + $this->ruleSets = [ [ - 'p1' => ['required', Rule::in([ArrayKeysBacked::key_3])], - 'p2' => ['required'], + 'type' => ['required', Rule::in([Validators::EMAIL])], + 'email' => ['required', 'email:rfc'], ], [ - 'p1' => ['required', Rule::in([StringStatus::pending])], - 'p2' => ['required', 'numeric'], - 'p3' => ['nullable', 'string'], + 'type' => ['required', Rule::in([Validators::URL])], + 'url' => ['required', 'url:http,https'], ], [ - 'p1' => ['required', Rule::in([StringStatus::done])], - 'p2' => ['required', 'email'], - 'p3' => ['nullable', 'alpha'], - ], + 'type' => ['required', Rule::in([Validators::IN])], + 'in' => ['required', Rule::enum(ArrayKeysBacked::class)], + ] ]; $this->nestedRules = [ @@ -112,6 +160,24 @@ protected function setUp(): void ]; } + protected function setUp(): void + { + parent::setUp(); + + $container = Container::getInstance(); + $container->bind('translator', function () { + return new Translator( + new ArrayLoader, + 'en' + ); + }); + + Facade::setFacadeApplication($container); + (new ValidationServiceProvider($container))->register(); + + $this->setUpRuleSets(); + } + protected function tearDown(): void { Container::setInstance(null); From 556698490987474bee931f0b95e1ae642963b6e1 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Thu, 13 Mar 2025 17:39:30 +0100 Subject: [PATCH 08/27] feat: add test case --- tests/Validation/ValidationAnyOfRuleTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index 2ca901c50a56..ae17c4d4c427 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -76,6 +76,16 @@ public function testInvalidUrlValidation() $this->assertFalse($validator->passes()); } + public function testErroneousEmailValidationOnUrlRule() + { + $validator = new Validator(resolve('translator'), ['foo' => [ + 'type' => 'url', + 'email' => 'test@example.com', + ]], ['foo' => Rule::anyOf($this->ruleSets)]); + + $this->assertFalse($validator->passes()); + } + public function testValidInValidation() { $validator = new Validator(resolve('translator'), ['foo' => [ From 2df37b9a11a29d94d7fb5be0fc604a31217c1e10 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Thu, 13 Mar 2025 17:40:56 +0100 Subject: [PATCH 09/27] chore: apply styleCI --- src/Illuminate/Validation/Rule.php | 3 ++- tests/Validation/ValidationAnyOfRuleTest.php | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index d65873ad9d28..a428549736d1 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Support\Arr; use Illuminate\Support\Traits\Macroable; +use Illuminate\Validation\Rules\AnyOf; use Illuminate\Validation\Rules\ArrayRule; use Illuminate\Validation\Rules\Can; use Illuminate\Validation\Rules\Date; @@ -18,7 +19,6 @@ use Illuminate\Validation\Rules\In; use Illuminate\Validation\Rules\NotIn; use Illuminate\Validation\Rules\Numeric; -use Illuminate\Validation\Rules\AnyOf; use Illuminate\Validation\Rules\ProhibitedIf; use Illuminate\Validation\Rules\RequiredIf; use Illuminate\Validation\Rules\Unique; @@ -252,6 +252,7 @@ public static function numeric() * * @param \Illuminate\Contracts\Validation\ValidationRule[] $values * @return \Illuminate\Validation\Rules\AnyOf + * * @throws \InvalidArgumentException */ public static function anyOf($rules) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index ae17c4d4c427..e90adbb4bcf7 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -3,9 +3,9 @@ namespace Illuminate\Tests\Validation; use Illuminate\Container\Container; +use Illuminate\Support\Facades\Facade; use Illuminate\Translation\ArrayLoader; use Illuminate\Translation\Translator; -use Illuminate\Support\Facades\Facade; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationServiceProvider; use Illuminate\Validation\Validator; @@ -30,7 +30,7 @@ public function testThrowsTypeErrorForInvalidInput() { $this->expectException(TypeError::class); $validator = new Validator(resolve('translator'), [ - 'foo' => 'not an array' + 'foo' => 'not an array', ], ['foo' => Rule::anyOf([[]])]); $validator->validate(); @@ -138,7 +138,6 @@ public function testInvalidNestedValidation() $this->assertFalse($validator->passes()); } - protected function setUpRuleSets() { $this->ruleSets = [ @@ -153,7 +152,7 @@ protected function setUpRuleSets() [ 'type' => ['required', Rule::in([Validators::IN])], 'in' => ['required', Rule::enum(ArrayKeysBacked::class)], - ] + ], ]; $this->nestedRules = [ From a19e327d290259ce1828722af87cebdbb5d9f8ce Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 17 Mar 2025 16:07:40 -0500 Subject: [PATCH 10/27] formatting --- .../Translation/lang/en/validation.php | 2 +- src/Illuminate/Validation/Rule.php | 4 ++-- src/Illuminate/Validation/Rules/AnyOf.php | 17 +++++++++-------- tests/Validation/ValidationAnyOfRuleTest.php | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/Translation/lang/en/validation.php b/src/Illuminate/Translation/lang/en/validation.php index 55f75d1e8f9c..a57a95ed9858 100644 --- a/src/Illuminate/Translation/lang/en/validation.php +++ b/src/Illuminate/Translation/lang/en/validation.php @@ -21,6 +21,7 @@ 'alpha' => 'The :attribute field must only contain letters.', 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.', 'alpha_num' => 'The :attribute field must only contain letters and numbers.', + 'any_of' => 'The :attribute field is invalid.', 'array' => 'The :attribute field must be an array.', 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.', 'before' => 'The :attribute field must be a date before :date.', @@ -117,7 +118,6 @@ 'not_in' => 'The selected :attribute is invalid.', 'not_regex' => 'The :attribute field format is invalid.', 'numeric' => 'The :attribute field must be a number.', - 'anyof' => 'The :attribute field does not match any of the allowed rule sets.', 'password' => [ 'letters' => 'The :attribute field must contain at least one letter.', 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.', diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index a428549736d1..170f4d04a1ea 100644 --- a/src/Illuminate/Validation/Rule.php +++ b/src/Illuminate/Validation/Rule.php @@ -248,9 +248,9 @@ public static function numeric() } /** - * Get a anyof rule builder instance. + * Get an "any of" rule builder instance. * - * @param \Illuminate\Contracts\Validation\ValidationRule[] $values + * @param array * @return \Illuminate\Validation\Rules\AnyOf * * @throws \InvalidArgumentException diff --git a/src/Illuminate/Validation/Rules/AnyOf.php b/src/Illuminate/Validation/Rules/AnyOf.php index 413a323524c0..7086a30c4f00 100644 --- a/src/Illuminate/Validation/Rules/AnyOf.php +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -10,11 +10,11 @@ class AnyOf implements Rule, ValidatorAwareRule { /** - * The validator performing the validation. + * The rules to match against. * - * @var \Illuminate\Validation\Validator + * @var array */ - protected $validator; + protected array $ruleSets = []; /** * The error message after validation, if any. @@ -24,11 +24,11 @@ class AnyOf implements Rule, ValidatorAwareRule protected $messages = []; /** - * The rules to match against. + * The validator performing the validation. * - * @var Illuminate\Contracts\Validation\ValidationRule[][] + * @var \Illuminate\Validation\Validator */ - private array $ruleSets = []; + protected $validator; /** * Sets the validation rules to match against. @@ -42,6 +42,7 @@ public function __construct($ruleSets) if (! is_array($ruleSets)) { throw new InvalidArgumentException('The provided value must be an array of validation rules.'); } + $this->ruleSets = $ruleSets; } @@ -69,13 +70,13 @@ public function passes($attribute, $value) } } - $this->validator->addFailure($attribute, 'anyof'); + $this->validator->addFailure($attribute, 'any_of'); return false; } /** - * Get the validation error message. + * Get the validation error messages. * * @return array */ diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index e90adbb4bcf7..fcb9c1cb4c24 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -46,6 +46,24 @@ public function testValidEmailValidation() $this->assertTrue($validator->passes()); } + public function testBasicValidation() + { + $validator = new Validator( + resolve('translator'), + [ + 'email' => 'test@example.com' + ], + [ + 'email' => Rule::anyOf([ + ['required', 'min:20'], + ['required', 'email'], + ]) + ], + ); + + $this->assertTrue($validator->passes()); + } + public function testInvalidEmailValidation() { $validator = new Validator(resolve('translator'), ['foo' => [ From f81e81c110ca13d6304f45d8039bd938fe70b358 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Tue, 18 Mar 2025 17:52:35 +0100 Subject: [PATCH 11/27] feat: allow string fields --- src/Illuminate/Validation/Rules/AnyOf.php | 5 +-- tests/Validation/ValidationAnyOfRuleTest.php | 33 ++++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/Illuminate/Validation/Rules/AnyOf.php b/src/Illuminate/Validation/Rules/AnyOf.php index 7086a30c4f00..04a20854cb90 100644 --- a/src/Illuminate/Validation/Rules/AnyOf.php +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Validation\Rule; use Illuminate\Contracts\Validation\ValidatorAwareRule; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Validator; use InvalidArgumentException; @@ -59,8 +60,8 @@ public function passes($attribute, $value) foreach ($this->ruleSets as $ruleSet) { $validator = Validator::make( - $value, - $ruleSet, + Arr::wrap($value), + Arr::wrap($ruleSet), $this->validator->customMessages, $this->validator->customAttributes ); diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index fcb9c1cb4c24..71733b73bac2 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -10,7 +10,6 @@ use Illuminate\Validation\ValidationServiceProvider; use Illuminate\Validation\Validator; use PHPUnit\Framework\TestCase; -use TypeError; include_once 'Enums.php'; @@ -26,16 +25,6 @@ class ValidationAnyOfRuleTest extends TestCase private array $ruleSets; private array $nestedRules; - public function testThrowsTypeErrorForInvalidInput() - { - $this->expectException(TypeError::class); - $validator = new Validator(resolve('translator'), [ - 'foo' => 'not an array', - ], ['foo' => Rule::anyOf([[]])]); - - $validator->validate(); - } - public function testValidEmailValidation() { $validator = new Validator(resolve('translator'), ['foo' => [ @@ -51,13 +40,31 @@ public function testBasicValidation() $validator = new Validator( resolve('translator'), [ - 'email' => 'test@example.com' + 'email' => 'test@example.com', ], [ 'email' => Rule::anyOf([ ['required', 'min:20'], ['required', 'email'], - ]) + ]), + ], + ); + + $this->assertTrue($validator->passes()); + } + + public function testBasicStringRuleValidation() + { + $validator = new Validator( + resolve('translator'), + [ + 'email' => '20charstringtestvalidation', + ], + [ + 'email' => Rule::anyOf([ + 'required|min:20', + 'required|email', + ]), ], ); From b5044575356b25db15a617f7413d011e6050c906 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Wed, 19 Mar 2025 10:43:00 +0100 Subject: [PATCH 12/27] feat: add test and clean nested rules --- tests/Validation/ValidationAnyOfRuleTest.php | 63 +++++++++++++------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index 71733b73bac2..c8970f1329df 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -37,38 +37,59 @@ public function testValidEmailValidation() public function testBasicValidation() { - $validator = new Validator( + $rule = [ + 'email' => Rule::anyOf([ + ['required', 'min:20'], + ['required', 'email'], + ]), + ]; + + $validatorEmail = new Validator( resolve('translator'), [ 'email' => 'test@example.com', ], + $rule + ); + $this->assertTrue($validatorEmail->passes()); + + $validatorString = new Validator( + resolve('translator'), [ - 'email' => Rule::anyOf([ - ['required', 'min:20'], - ['required', 'email'], - ]), + 'email' => '20charstringtestvalidation', ], + $rule ); - - $this->assertTrue($validator->passes()); + $this->assertTrue($validatorString->passes()); } - public function testBasicStringRuleValidation() + public function testBareBasicStringRuleValidation() { - $validator = new Validator( + $rule = [ + 'p1' => Rule::anyOf([ + ['p2' => 'required|min:20'], + 'required|min:20' + ]), + ]; + + $validatorNested = new Validator( resolve('translator'), [ - 'email' => '20charstringtestvalidation', + 'p1' => ['p2' => '20charstringtestvalidation'], ], + $rule + ); + + $validatorFlat = new Validator( + resolve('translator'), [ - 'email' => Rule::anyOf([ - 'required|min:20', - 'required|email', - ]), + 'p1' => '20charstringtestvalidation', ], + $rule ); - $this->assertTrue($validator->passes()); + $this->assertTrue($validatorNested->passes()); + $this->assertTrue($validatorFlat->passes()); } public function testInvalidEmailValidation() @@ -182,14 +203,14 @@ protected function setUpRuleSets() $this->nestedRules = [ [ - 'p1' => ['required', Rule::anyOf([ + 'p1' => Rule::anyOf([ [ - 'p2' => ['required', 'string'], - 'p3' => ['required', Rule::anyOf([[ - 'p4' => ['nullable', 'string'], - ]])], + 'p2' => 'required', + 'p3' => Rule::anyOf([[ + 'p4' => ['nullable'], + ]]), ], - ])], + ]), ], ]; } From c8c2147b7df228ec9f912fa730cce8d3f8480181 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Wed, 19 Mar 2025 15:36:27 +0100 Subject: [PATCH 13/27] chore: apply styleCI --- tests/Validation/ValidationAnyOfRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index c8970f1329df..6f3cbf7afd6f 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -68,7 +68,7 @@ public function testBareBasicStringRuleValidation() $rule = [ 'p1' => Rule::anyOf([ ['p2' => 'required|min:20'], - 'required|min:20' + 'required|min:20', ]), ]; From 61ad096ed088472947046b0ef1b35958070088c2 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sat, 22 Mar 2025 12:21:50 -0500 Subject: [PATCH 14/27] failing test --- tests/Validation/ValidationAnyOfRuleTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index 6f3cbf7afd6f..f674a3493577 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -61,6 +61,15 @@ public function testBasicValidation() $rule ); $this->assertTrue($validatorString->passes()); + + $validatorString = new Validator( + resolve('translator'), + [ + 'email' => 'abc', + ], + $rule + ); + $this->assertFalse($validatorString->passes()); } public function testBareBasicStringRuleValidation() From 5c532bf629ac023259787c85c3c7adfa01b3e830 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Mon, 24 Mar 2025 00:38:29 +0100 Subject: [PATCH 15/27] Validation tests (#1) * feat: add more validation tests * wip: add failing test * wip: add basic string rule validations * chore: rename object fields for better debugging * refactor: rename ruleSets to rules * fix: respect array rule validation --------- Co-authored-by: Christian Ascone * fix: this should be passing because AnyOf has no type relevance and 'required' only checks to see if the field has something in it --------- Co-authored-by: Christian Ascone --------- Co-authored-by: Christian Ascone --- src/Illuminate/Validation/Rules/AnyOf.php | 14 +- tests/Validation/ValidationAnyOfRuleTest.php | 304 ++++++++++++------- 2 files changed, 196 insertions(+), 122 deletions(-) diff --git a/src/Illuminate/Validation/Rules/AnyOf.php b/src/Illuminate/Validation/Rules/AnyOf.php index 04a20854cb90..2f9f6166a426 100644 --- a/src/Illuminate/Validation/Rules/AnyOf.php +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -15,7 +15,7 @@ class AnyOf implements Rule, ValidatorAwareRule * * @var array */ - protected array $ruleSets = []; + protected array $rules = []; /** * The error message after validation, if any. @@ -34,17 +34,17 @@ class AnyOf implements Rule, ValidatorAwareRule /** * Sets the validation rules to match against. * - * @param Illuminate\Contracts\Validation\ValidationRule[][] $ruleSets + * @param Illuminate\Contracts\Validation\ValidationRule[][] $rules * * @throws \InvalidArgumentException */ - public function __construct($ruleSets) + public function __construct($rules) { - if (! is_array($ruleSets)) { + if (! is_array($rules)) { throw new InvalidArgumentException('The provided value must be an array of validation rules.'); } - $this->ruleSets = $ruleSets; + $this->rules = $rules; } /** @@ -58,10 +58,10 @@ public function passes($attribute, $value) { $this->messages = []; - foreach ($this->ruleSets as $ruleSet) { + foreach ($this->rules as $rule) { $validator = Validator::make( Arr::wrap($value), - Arr::wrap($ruleSet), + Arr::isAssoc(Arr::wrap($rule)) ? $rule : [$rule], $this->validator->customMessages, $this->validator->customAttributes ); diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index f674a3493577..55e23e0b7921 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -24,173 +24,249 @@ class ValidationAnyOfRuleTest extends TestCase { private array $ruleSets; private array $nestedRules; + private array $nestedRules2; - public function testValidEmailValidation() + public function testBasicValidation() { - $validator = new Validator(resolve('translator'), ['foo' => [ - 'type' => 'email', + $rule = ['email' => Rule::anyOf([ + ['required', 'min:20'], + ['required', 'email'], + ])]; + + $validator = new Validator(resolve('translator'), [ 'email' => 'test@example.com', - ]], ['foo' => Rule::anyOf($this->ruleSets)]); + ], $rule); + $this->assertTrue($validator->passes()); + $validator = new Validator(resolve('translator'), [ + 'email' => '20charstringtestvalidation', + ], $rule); $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'email' => null, + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'email' => 'abc', + ], $rule); + $this->assertFalse($validator->passes()); } - public function testBasicValidation() + public function testBasicStringValidation() { - $rule = [ - 'email' => Rule::anyOf([ - ['required', 'min:20'], - ['required', 'email'], - ]), - ]; + $rule = ['email' => Rule::anyOf([ + 'required|min:20', + 'required|email', + ])]; - $validatorEmail = new Validator( - resolve('translator'), - [ - 'email' => 'test@example.com', - ], - $rule - ); - $this->assertTrue($validatorEmail->passes()); + $validator = new Validator(resolve('translator'), [ + 'email' => 'test@example.com', + ], $rule); + $this->assertTrue($validator->passes()); - $validatorString = new Validator( - resolve('translator'), - [ - 'email' => '20charstringtestvalidation', - ], - $rule - ); - $this->assertTrue($validatorString->passes()); + $validator = new Validator(resolve('translator'), [ + 'email' => '20charstringtestvalidation', + ], $rule); + $this->assertTrue($validator->passes()); - $validatorString = new Validator( - resolve('translator'), - [ - 'email' => 'abc', - ], - $rule - ); - $this->assertFalse($validatorString->passes()); + $validator = new Validator(resolve('translator'), [ + 'email' => null, + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'email' => 'abc', + ], $rule); + $this->assertFalse($validator->passes()); } public function testBareBasicStringRuleValidation() { - $rule = [ - 'p1' => Rule::anyOf([ - ['p2' => 'required|min:20'], - 'required|min:20', - ]), - ]; + $rule = ['p1' => Rule::anyOf([ + ['p2' => ['required', 'min:20']], + 'required|min:20', + ])]; - $validatorNested = new Validator( - resolve('translator'), - [ - 'p1' => ['p2' => '20charstringtestvalidation'], - ], - $rule - ); + $validator = new Validator(resolve('translator'), [ + 'p1' => ['p2' => '20charstringtestvalidation'], + ], $rule); + $this->assertTrue($validator->passes()); - $validatorFlat = new Validator( - resolve('translator'), - [ - 'p1' => '20charstringtestvalidation', - ], - $rule - ); + $validator = new Validator(resolve('translator'), [ + 'p1' => ['p2' => 'abc'], + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'p1' => ['p2' => null], + ], $rule); + $this->assertFalse($validator->passes()); - $this->assertTrue($validatorNested->passes()); - $this->assertTrue($validatorFlat->passes()); + $validator = new Validator(resolve('translator'), [ + 'p1' => null, + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'p1' => '20charstringtestvalidation', + ], $rule); + $this->assertTrue($validator->passes()); } - public function testInvalidEmailValidation() + public function testEmailValidation() { - $validator = new Validator(resolve('translator'), ['foo' => [ + $validator = new Validator(resolve('translator'), ['type_email_matches' => [ + 'type' => 'email', + 'email' => 'test@example.com', + ]], ['type_email_matches' => Rule::anyOf($this->ruleSets)]); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), ['email_is_just_a_string' => [ 'type' => 'email', 'email' => 'invalid-email', - ]], ['foo' => Rule::anyOf($this->ruleSets)]); + ]], ['email_is_just_a_string' => Rule::anyOf($this->ruleSets)]); + $this->assertFalse($validator->passes()); + $validator = new Validator(resolve('translator'), ['url_instead_of_email' => [ + 'type' => 'email', + 'url' => 'https://example.com', + ]], ['url_instead_of_email' => Rule::anyOf($this->ruleSets)]); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), ['missing_email' => [ + 'type' => 'email', + ]], ['missing_email' => Rule::anyOf($this->ruleSets)]); $this->assertFalse($validator->passes()); } - public function testValidUrlValidation() + public function testUrlValidation() { - $validator = new Validator(resolve('translator'), ['foo' => [ + $validator = new Validator(resolve('translator'), ['type_url_matches' => [ 'type' => 'url', 'url' => 'https://example.com', - ]], ['foo' => Rule::anyOf($this->ruleSets)]); - + ]], ['type_url_matches' => Rule::anyOf($this->ruleSets)]); $this->assertTrue($validator->passes()); - } - public function testInvalidUrlValidation() - { - $validator = new Validator(resolve('translator'), ['foo' => [ + $validator = new Validator(resolve('translator'), ['url_is_just_a_string' => [ 'type' => 'url', 'url' => 'not-a-url', - ]], ['foo' => Rule::anyOf($this->ruleSets)]); - + ]], ['url_is_just_a_string' => Rule::anyOf($this->ruleSets)]); $this->assertFalse($validator->passes()); - } - public function testErroneousEmailValidationOnUrlRule() - { - $validator = new Validator(resolve('translator'), ['foo' => [ + $validator = new Validator(resolve('translator'), ['email_instead_of_url' => [ 'type' => 'url', 'email' => 'test@example.com', - ]], ['foo' => Rule::anyOf($this->ruleSets)]); + ]], ['email_instead_of_url' => Rule::anyOf($this->ruleSets)]); + $this->assertFalse($validator->passes()); + $validator = new Validator(resolve('translator'), ['missing_url' => [ + 'type' => 'url', + ]], ['missing_url' => Rule::anyOf($this->ruleSets)]); $this->assertFalse($validator->passes()); } - public function testValidInValidation() + public function testInValidation() { - $validator = new Validator(resolve('translator'), ['foo' => [ + $validator = new Validator(resolve('translator'), ['type_in_matches_1' => [ 'type' => 'in', 'in' => 'key_1', - ]], ['foo' => Rule::anyOf($this->ruleSets)]); + ]], ['type_in_matches_1' => Rule::anyOf($this->ruleSets)]); + $this->assertTrue($validator->passes()); + $validator = new Validator(resolve('translator'), ['type_in_matches_2' => [ + 'type' => 'in', + 'in' => 'key_2', + ]], ['type_in_matches_2' => Rule::anyOf($this->ruleSets)]); $this->assertTrue($validator->passes()); - } - public function testInvalidInValidation() - { - $validator = new Validator(resolve('translator'), ['foo' => [ + $validator = new Validator(resolve('translator'), ['unexpected_in_value' => [ 'type' => 'in', 'in' => 'unexpected_value', - ]], ['foo' => Rule::anyOf($this->ruleSets)]); + ]], ['unexpected_in_value' => Rule::anyOf($this->ruleSets)]); + $this->assertFalse($validator->passes()); + $validator = new Validator(resolve('translator'), ['url_instead_of_in' => [ + 'type' => 'in', + 'url' => 'https://example.com', + ]], ['url_instead_of_in' => Rule::anyOf($this->ruleSets)]); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), ['missing_in' => [ + 'type' => 'in', + ]], ['missing_in' => Rule::anyOf($this->ruleSets)]); $this->assertFalse($validator->passes()); } - public function testValidNestedValidation() + public function testMissingTagValidation() { - $validator = new Validator(resolve('translator'), [ - 'foo' => [ - 'p1' => [ - 'p2' => 'a_valid_string', - 'p3' => [ - 'p4' => 'another_valid_string', - ], - ], - ], - ], ['foo' => Rule::anyOf($this->nestedRules)]); + $validator = new Validator(resolve('translator'), ['invalid_tag_with_url' => [ + 'type' => 'doesnt_exist', + 'url' => 'https://example.com', + ]], ['invalid_tag_with_url' => Rule::anyOf($this->ruleSets)]); + $this->assertFalse($validator->passes()); - $this->assertTrue($validator->passes()); + $validator = new Validator(resolve('translator'), ['invalid_tag_with_email' => [ + 'type' => 'doesnt_exist', + 'email' => 'test@example.com', + ]], ['invalid_tag_with_email' => Rule::anyOf($this->ruleSets)]); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), ['invalid_tag_with_in' => [ + 'type' => 'doesnt_exist', + 'in' => 'key_1', + ]], ['invalid_tag_with_in' => Rule::anyOf($this->ruleSets)]); + $this->assertFalse($validator->passes()); } - public function testInvalidNestedValidation() + public function testNestedValidation() { $validator = new Validator(resolve('translator'), [ - 'foo' => [ - 'p1' => [ - 'p2' => '', // required field left empty - 'p3' => [ - 'p4' => 'valid_string', - ], - ], - ], - ], ['foo' => Rule::anyOf($this->nestedRules)]); + 'complete' => ['p1' => [ + 'p2' => 'a_valid_string', + 'p3' => ['p4' => 'another_valid_string'], + ]], + ], ['complete' => Rule::anyOf($this->nestedRules)]); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'p2_is_missing' => ['p1' => [ + 'p3' => ['p4' => 'valid_string'], + ]], + ], ['p2_is_missing' => Rule::anyOf($this->nestedRules)]); + $this->assertFalse($validator->passes()); + $validator = new Validator(resolve('translator'), [ + 'p3_is_missing' => ['p1' => [ + 'p2' => 'a_valid_string', + ]], + ], ['p3_is_missing' => Rule::anyOf($this->nestedRules)]); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'p3_is_null' => ['p1' => [ + 'p2' => 'a_valid_string', + 'p3' => null, + ]], + ], ['p3_is_null' => Rule::anyOf($this->nestedRules)]); $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'p3_shouldnt_be_a_string' => ['p1' => [ + 'p2' => 'a_valid_string', + 'p3' => 'shouldnt_be_a_string', + ]], + ], ['p3_shouldnt_be_a_string' => Rule::anyOf($this->nestedRules)]); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'p4_is_nullable' => ['p1' => [ + 'p2' => 'a_valid_string', + 'p3' => ['p4' => null], + ]], + ], ['p4_is_nullable' => Rule::anyOf($this->nestedRules)]); + $this->assertTrue($validator->passes()); } protected function setUpRuleSets() @@ -211,16 +287,14 @@ protected function setUpRuleSets() ]; $this->nestedRules = [ - [ - 'p1' => Rule::anyOf([ - [ - 'p2' => 'required', - 'p3' => Rule::anyOf([[ - 'p4' => ['nullable'], - ]]), - ], - ]), - ], + ['p1' => Rule::anyOf([ + [ + 'p2' => 'required', + 'p3' => ['required', Rule::anyOf([[ + 'p4' => ['nullable'], + ]])], + ], + ])], ]; } From 25ef030ce8dee307990818f492c20b79881e2c2f Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Mon, 24 Mar 2025 01:42:48 +0100 Subject: [PATCH 16/27] chore: correspond with recent changes https://github.com/brianferri/framework/pull/1/commits/de3b902a950b8f5ba8edaafa273f91d7c6ade295 --- tests/Validation/ValidationAnyOfRuleTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index 55e23e0b7921..941b72e0c07c 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -253,11 +253,11 @@ public function testNestedValidation() $this->assertFalse($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'p3_shouldnt_be_a_string' => ['p1' => [ + 'p3_is_required_whatever_it_may_be' => ['p1' => [ 'p2' => 'a_valid_string', - 'p3' => 'shouldnt_be_a_string', + 'p3' => 'is_required_whatever_it_may_be', ]], - ], ['p3_shouldnt_be_a_string' => Rule::anyOf($this->nestedRules)]); + ], ['p3_is_required_whatever_it_may_be' => Rule::anyOf($this->nestedRules)]); $this->assertTrue($validator->passes()); $validator = new Validator(resolve('translator'), [ From d1c8daff98d04ceaafd30ddce846318c8ca40a2a Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Mon, 24 Mar 2025 01:43:35 +0100 Subject: [PATCH 17/27] chore: remove unused private property --- tests/Validation/ValidationAnyOfRuleTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index 941b72e0c07c..05580d89c41c 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -24,7 +24,6 @@ class ValidationAnyOfRuleTest extends TestCase { private array $ruleSets; private array $nestedRules; - private array $nestedRules2; public function testBasicValidation() { From 14598f62bf8305bbe2a94ee4e2848d6ec2e05587 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Wed, 26 Mar 2025 12:07:51 +0100 Subject: [PATCH 18/27] feat: attribute mapping in favor of potentially indexed mapping --- src/Illuminate/Validation/Rules/AnyOf.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Validation/Rules/AnyOf.php b/src/Illuminate/Validation/Rules/AnyOf.php index 2f9f6166a426..3be01d2a22e8 100644 --- a/src/Illuminate/Validation/Rules/AnyOf.php +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -60,8 +60,8 @@ public function passes($attribute, $value) foreach ($this->rules as $rule) { $validator = Validator::make( - Arr::wrap($value), - Arr::isAssoc(Arr::wrap($rule)) ? $rule : [$rule], + Arr::isAssoc(Arr::wrap($value)) ? $value : [$attribute => $value], + Arr::isAssoc(Arr::wrap($rule)) ? $rule : [$attribute => $rule], $this->validator->customMessages, $this->validator->customAttributes ); From e15e385df9ac161c21714e00d26b019fd36df070 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Thu, 27 Mar 2025 14:52:49 +0100 Subject: [PATCH 19/27] feat: add more tests --- tests/Validation/ValidationAnyOfRuleTest.php | 134 +++++++++++++++++-- 1 file changed, 126 insertions(+), 8 deletions(-) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index 05580d89c41c..d2f3d4c81b71 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -24,6 +24,7 @@ class ValidationAnyOfRuleTest extends TestCase { private array $ruleSets; private array $nestedRules; + private array $nestedRulesRequired; public function testBasicValidation() { @@ -229,6 +230,16 @@ public function testNestedValidation() ], ['complete' => Rule::anyOf($this->nestedRules)]); $this->assertTrue($validator->passes()); + $validator = new Validator(resolve('translator'), [ + 'p1_is_empty' => [], + ], ['p1_is_empty' => Rule::anyOf($this->nestedRules)]); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'p1_is_empty' => [], + ], ['p1_is_empty' => Rule::anyOf($this->nestedRulesRequired)]); + $this->assertFalse($validator->passes()); + $validator = new Validator(resolve('translator'), [ 'p2_is_missing' => ['p1' => [ 'p3' => ['p4' => 'valid_string'], @@ -266,6 +277,107 @@ public function testNestedValidation() ]], ], ['p4_is_nullable' => Rule::anyOf($this->nestedRules)]); $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'extra_key_is_present' => ['p1' => [ + 'p2' => 'a_valid_string', + 'p3' => [ + 'p4' => 'another_valid_string', + 'extra_key' => 'unexpected_value', + ], + ]], + ], ['extra_key_is_present' => Rule::anyOf($this->nestedRules)]); + $this->assertTrue($validator->passes()); + } + + public function testEmptyInputs() + { + $rule = ['email' => Rule::anyOf([ + 'email', + ])]; + + $validator = new Validator(resolve('translator'), [], $rule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), ['email' => ''], $rule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), ['email' => 'not-an-email'], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), ['email' => 'test@example.com'], $rule); + $this->assertTrue($validator->passes()); + + $requiredRule = ['email' => ['required', Rule::anyOf([ + 'email', + ])]]; + + $validator = new Validator(resolve('translator'), [], $requiredRule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), ['email' => ''], $requiredRule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), ['email' => 'not-an-email'], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), ['email' => 'test@example.com'], $requiredRule); + $this->assertTrue($validator->passes()); + } + + public function testUnexpectedInputType() + { + $rule = ['email' => ['required', Rule::anyOf([ + 'email:rfc', + ])]]; + + $validator = new Validator(resolve('translator'), [ + 'email' => ['not', 'an', 'email'], + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'email' => [], + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'email' => 123, + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'email' => '', + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'email' => null, + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'email' => 'test@example.com', + ], $rule); + $this->assertTrue($validator->passes()); + } + + public function testConflictingRules() + { + $rule = ['field' => Rule::anyOf([ + ['required', 'min:10'], + ['required', 'max:5'], + ])]; + + $validator = new Validator(resolve('translator'), [ + 'field' => 'short', + ], $rule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'field' => 'toolongfieldstring', + ], $rule); + $this->assertTrue($validator->passes()); } protected function setUpRuleSets() @@ -285,15 +397,21 @@ protected function setUpRuleSets() ], ]; + $oneOfNestedRule = Rule::anyOf([ + [ + 'p2' => 'required', + 'p3' => ['required', Rule::anyOf([[ + 'p4' => ['nullable'], + ]])], + ], + ]); + $this->nestedRules = [ - ['p1' => Rule::anyOf([ - [ - 'p2' => 'required', - 'p3' => ['required', Rule::anyOf([[ - 'p4' => ['nullable'], - ]])], - ], - ])], + ['p1' => $oneOfNestedRule], + ]; + + $this->nestedRulesRequired = [ + ['p1' => ['required', $oneOfNestedRule]], ]; } From 396e2a2340a5374a095d13a38bb8da324a3f1ec8 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Sat, 29 Mar 2025 15:59:22 +0100 Subject: [PATCH 20/27] refactor(tests): remove unnecessary amount of tests, rename parameter properties to be more descriptive/analogous to use cases --- tests/Validation/ValidationAnyOfRuleTest.php | 440 ++++++------------- 1 file changed, 135 insertions(+), 305 deletions(-) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index d2f3d4c81b71..f4a610c8008c 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -11,407 +11,237 @@ use Illuminate\Validation\Validator; use PHPUnit\Framework\TestCase; -include_once 'Enums.php'; - -enum Validators: string +enum TaggedUnionDiscriminatorType: string { case EMAIL = 'email'; case URL = 'url'; - case IN = 'in'; } class ValidationAnyOfRuleTest extends TestCase { - private array $ruleSets; + private array $taggedUnionRules; private array $nestedRules; - private array $nestedRulesRequired; public function testBasicValidation() { - $rule = ['email' => Rule::anyOf([ - ['required', 'min:20'], + $rule = Rule::anyOf([ + ['required', 'uuid:4'], ['required', 'email'], - ])]; + ]); + $idRule = ['id' => $rule]; + $requiredIdRule = ['id' => ['required', $rule]]; $validator = new Validator(resolve('translator'), [ - 'email' => 'test@example.com', - ], $rule); + 'id' => 'taylor@laravel.com', + ], $idRule); $this->assertTrue($validator->passes()); - $validator = new Validator(resolve('translator'), [ - 'email' => '20charstringtestvalidation', - ], $rule); + $validator = new Validator(resolve('translator'), [], $idRule); $this->assertTrue($validator->passes()); - $validator = new Validator(resolve('translator'), [ - 'email' => null, - ], $rule); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), [ - 'email' => 'abc', - ], $rule); + $validator = new Validator(resolve('translator'), [], $requiredIdRule); $this->assertFalse($validator->passes()); - } - - public function testBasicStringValidation() - { - $rule = ['email' => Rule::anyOf([ - 'required|min:20', - 'required|email', - ])]; - - $validator = new Validator(resolve('translator'), [ - 'email' => 'test@example.com', - ], $rule); - $this->assertTrue($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'email' => '20charstringtestvalidation', - ], $rule); + 'id' => '3c8ff5cb-4bc1-457b-a477-1833c477b254', + ], $idRule); $this->assertTrue($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'email' => null, - ], $rule); + 'id' => null, + ], $idRule); $this->assertFalse($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'email' => 'abc', - ], $rule); - $this->assertFalse($validator->passes()); - } - - public function testBareBasicStringRuleValidation() - { - $rule = ['p1' => Rule::anyOf([ - ['p2' => ['required', 'min:20']], - 'required|min:20', - ])]; - - $validator = new Validator(resolve('translator'), [ - 'p1' => ['p2' => '20charstringtestvalidation'], - ], $rule); + 'id' => '', + ], $idRule); $this->assertTrue($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'p1' => ['p2' => 'abc'], - ], $rule); + 'id' => '', + ], $requiredIdRule); $this->assertFalse($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'p1' => ['p2' => null], - ], $rule); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), [ - 'p1' => null, - ], $rule); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), [ - 'p1' => '20charstringtestvalidation', - ], $rule); - $this->assertTrue($validator->passes()); - } - - public function testEmailValidation() - { - $validator = new Validator(resolve('translator'), ['type_email_matches' => [ - 'type' => 'email', - 'email' => 'test@example.com', - ]], ['type_email_matches' => Rule::anyOf($this->ruleSets)]); - $this->assertTrue($validator->passes()); - - $validator = new Validator(resolve('translator'), ['email_is_just_a_string' => [ - 'type' => 'email', - 'email' => 'invalid-email', - ]], ['email_is_just_a_string' => Rule::anyOf($this->ruleSets)]); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['url_instead_of_email' => [ - 'type' => 'email', - 'url' => 'https://example.com', - ]], ['url_instead_of_email' => Rule::anyOf($this->ruleSets)]); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['missing_email' => [ - 'type' => 'email', - ]], ['missing_email' => Rule::anyOf($this->ruleSets)]); + 'id' => 'abc', + ], $idRule); $this->assertFalse($validator->passes()); } - public function testUrlValidation() + public function testBasicStringValidation() { - $validator = new Validator(resolve('translator'), ['type_url_matches' => [ - 'type' => 'url', - 'url' => 'https://example.com', - ]], ['type_url_matches' => Rule::anyOf($this->ruleSets)]); - $this->assertTrue($validator->passes()); - - $validator = new Validator(resolve('translator'), ['url_is_just_a_string' => [ - 'type' => 'url', - 'url' => 'not-a-url', - ]], ['url_is_just_a_string' => Rule::anyOf($this->ruleSets)]); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['email_instead_of_url' => [ - 'type' => 'url', - 'email' => 'test@example.com', - ]], ['email_instead_of_url' => Rule::anyOf($this->ruleSets)]); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['missing_url' => [ - 'type' => 'url', - ]], ['missing_url' => Rule::anyOf($this->ruleSets)]); - $this->assertFalse($validator->passes()); - } + $rule = Rule::anyOf([ + 'required|uuid:4', + 'required|email', + ]); + $idRule = ['id' => $rule]; + $requiredIdRule = ['id' => ['required', $rule]]; - public function testInValidation() - { - $validator = new Validator(resolve('translator'), ['type_in_matches_1' => [ - 'type' => 'in', - 'in' => 'key_1', - ]], ['type_in_matches_1' => Rule::anyOf($this->ruleSets)]); + $validator = new Validator(resolve('translator'), [ + 'id' => 'test@example.com', + ], $idRule); $this->assertTrue($validator->passes()); - $validator = new Validator(resolve('translator'), ['type_in_matches_2' => [ - 'type' => 'in', - 'in' => 'key_2', - ]], ['type_in_matches_2' => Rule::anyOf($this->ruleSets)]); + $validator = new Validator(resolve('translator'), [], $idRule); $this->assertTrue($validator->passes()); - $validator = new Validator(resolve('translator'), ['unexpected_in_value' => [ - 'type' => 'in', - 'in' => 'unexpected_value', - ]], ['unexpected_in_value' => Rule::anyOf($this->ruleSets)]); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['url_instead_of_in' => [ - 'type' => 'in', - 'url' => 'https://example.com', - ]], ['url_instead_of_in' => Rule::anyOf($this->ruleSets)]); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['missing_in' => [ - 'type' => 'in', - ]], ['missing_in' => Rule::anyOf($this->ruleSets)]); - $this->assertFalse($validator->passes()); - } - - public function testMissingTagValidation() - { - $validator = new Validator(resolve('translator'), ['invalid_tag_with_url' => [ - 'type' => 'doesnt_exist', - 'url' => 'https://example.com', - ]], ['invalid_tag_with_url' => Rule::anyOf($this->ruleSets)]); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['invalid_tag_with_email' => [ - 'type' => 'doesnt_exist', - 'email' => 'test@example.com', - ]], ['invalid_tag_with_email' => Rule::anyOf($this->ruleSets)]); + $validator = new Validator(resolve('translator'), [], $requiredIdRule); $this->assertFalse($validator->passes()); - $validator = new Validator(resolve('translator'), ['invalid_tag_with_in' => [ - 'type' => 'doesnt_exist', - 'in' => 'key_1', - ]], ['invalid_tag_with_in' => Rule::anyOf($this->ruleSets)]); - $this->assertFalse($validator->passes()); - } - - public function testNestedValidation() - { - $validator = new Validator(resolve('translator'), [ - 'complete' => ['p1' => [ - 'p2' => 'a_valid_string', - 'p3' => ['p4' => 'another_valid_string'], - ]], - ], ['complete' => Rule::anyOf($this->nestedRules)]); - $this->assertTrue($validator->passes()); - $validator = new Validator(resolve('translator'), [ - 'p1_is_empty' => [], - ], ['p1_is_empty' => Rule::anyOf($this->nestedRules)]); + 'id' => '3c8ff5cb-4bc1-457b-a477-1833c477b254', + ], $idRule); $this->assertTrue($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'p1_is_empty' => [], - ], ['p1_is_empty' => Rule::anyOf($this->nestedRulesRequired)]); + 'id' => null, + ], $idRule); $this->assertFalse($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'p2_is_missing' => ['p1' => [ - 'p3' => ['p4' => 'valid_string'], - ]], - ], ['p2_is_missing' => Rule::anyOf($this->nestedRules)]); - $this->assertFalse($validator->passes()); + 'id' => '', + ], $idRule); + $this->assertTrue($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'p3_is_missing' => ['p1' => [ - 'p2' => 'a_valid_string', - ]], - ], ['p3_is_missing' => Rule::anyOf($this->nestedRules)]); + 'id' => '', + ], $requiredIdRule); $this->assertFalse($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'p3_is_null' => ['p1' => [ - 'p2' => 'a_valid_string', - 'p3' => null, - ]], - ], ['p3_is_null' => Rule::anyOf($this->nestedRules)]); + 'id' => 'abc', + ], $idRule); $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), [ - 'p3_is_required_whatever_it_may_be' => ['p1' => [ - 'p2' => 'a_valid_string', - 'p3' => 'is_required_whatever_it_may_be', - ]], - ], ['p3_is_required_whatever_it_may_be' => Rule::anyOf($this->nestedRules)]); - $this->assertTrue($validator->passes()); - - $validator = new Validator(resolve('translator'), [ - 'p4_is_nullable' => ['p1' => [ - 'p2' => 'a_valid_string', - 'p3' => ['p4' => null], - ]], - ], ['p4_is_nullable' => Rule::anyOf($this->nestedRules)]); - $this->assertTrue($validator->passes()); - - $validator = new Validator(resolve('translator'), [ - 'extra_key_is_present' => ['p1' => [ - 'p2' => 'a_valid_string', - 'p3' => [ - 'p4' => 'another_valid_string', - 'extra_key' => 'unexpected_value', - ], - ]], - ], ['extra_key_is_present' => Rule::anyOf($this->nestedRules)]); - $this->assertTrue($validator->passes()); } - public function testEmptyInputs() + public function testTaggedUnionObjects() { - $rule = ['email' => Rule::anyOf([ - 'email', - ])]; - - $validator = new Validator(resolve('translator'), [], $rule); - $this->assertTrue($validator->passes()); - - $validator = new Validator(resolve('translator'), ['email' => ''], $rule); - $this->assertTrue($validator->passes()); - - $validator = new Validator(resolve('translator'), ['email' => 'not-an-email'], $rule); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['email' => 'test@example.com'], $rule); - $this->assertTrue($validator->passes()); - - $requiredRule = ['email' => ['required', Rule::anyOf([ - 'email', - ])]]; - - $validator = new Validator(resolve('translator'), [], $requiredRule); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['email' => ''], $requiredRule); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['email' => 'not-an-email'], $rule); - $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), ['email' => 'test@example.com'], $requiredRule); + $validator = new Validator(resolve('translator'), [ + 'data' => [ + 'type' => TaggedUnionDiscriminatorType::EMAIL->value, + 'email' => 'taylor@laravel.com', + ] + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertTrue($validator->passes()); - } - - public function testUnexpectedInputType() - { - $rule = ['email' => ['required', Rule::anyOf([ - 'email:rfc', - ])]]; $validator = new Validator(resolve('translator'), [ - 'email' => ['not', 'an', 'email'], - ], $rule); + 'data' => [ + 'type' => TaggedUnionDiscriminatorType::EMAIL->value, + 'email' => 'invalid-email', + ] + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertFalse($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'email' => [], - ], $rule); - $this->assertFalse($validator->passes()); + 'data' => [ + 'type' => TaggedUnionDiscriminatorType::URL->value, + 'url' => 'http://laravel.com', + ] + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); + $this->assertTrue($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'email' => 123, - ], $rule); + 'data' => [ + 'type' => TaggedUnionDiscriminatorType::URL->value, + 'url' => 'not-a-url', + ] + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertFalse($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'email' => '', - ], $rule); + 'data' => [ + 'type' => TaggedUnionDiscriminatorType::EMAIL->value, + 'url' => 'url-should-not-be-present-with-email-discriminator', + ] + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertFalse($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'email' => null, - ], $rule); + 'data' => [ + 'type' => 'doesnt-exist', + 'email' => 'taylor@laravel.com', + ] + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertFalse($validator->passes()); - - $validator = new Validator(resolve('translator'), [ - 'email' => 'test@example.com', - ], $rule); - $this->assertTrue($validator->passes()); } - public function testConflictingRules() + public function testNestedValidation() { - $rule = ['field' => Rule::anyOf([ - ['required', 'min:10'], - ['required', 'max:5'], - ])]; - $validator = new Validator(resolve('translator'), [ - 'field' => 'short', - ], $rule); + 'user' => [ + 'identifier' => 1, + 'properties' => [ + 'name' => 'Taylor', + 'surname' => 'Otwell', + ] + ] + ], $this->nestedRules); $this->assertTrue($validator->passes()); $validator = new Validator(resolve('translator'), [ - 'field' => 'toolongfieldstring', - ], $rule); + 'user' => [ + 'identifier' => 'taylor@laravel.com', + 'properties' => [ + 'bio' => 'biography', + 'name' => 'Taylor', + 'surname' => 'Otwell', + ] + ] + ], $this->nestedRules); $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'user' => [ + 'identifier' => 'taylor@laravel.com', + 'properties' => [ + 'name' => null, + 'surname' => 'Otwell', + ] + ] + ], $this->nestedRules); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'user' => [ + 'properties' => [ + 'name' => 'Taylor', + 'surname' => 'Otwell', + ] + ] + ], $this->nestedRules); + $this->assertFalse($validator->passes()); } protected function setUpRuleSets() { - $this->ruleSets = [ + $this->taggedUnionRules = [ [ - 'type' => ['required', Rule::in([Validators::EMAIL])], + 'type' => ['required', Rule::in([TaggedUnionDiscriminatorType::EMAIL])], 'email' => ['required', 'email:rfc'], ], [ - 'type' => ['required', Rule::in([Validators::URL])], + 'type' => ['required', Rule::in([TaggedUnionDiscriminatorType::URL])], 'url' => ['required', 'url:http,https'], ], - [ - 'type' => ['required', Rule::in([Validators::IN])], - 'in' => ['required', Rule::enum(ArrayKeysBacked::class)], - ], ]; - $oneOfNestedRule = Rule::anyOf([ - [ - 'p2' => 'required', - 'p3' => ['required', Rule::anyOf([[ - 'p4' => ['nullable'], - ]])], - ], - ]); - + // Using AnyOf as nesting feature $this->nestedRules = [ - ['p1' => $oneOfNestedRule], - ]; - - $this->nestedRulesRequired = [ - ['p1' => ['required', $oneOfNestedRule]], + 'user' => Rule::anyOf([ + [ + 'identifier' => ['required', Rule::anyOf([ + 'email:rfc', + 'integer', + ])], + 'properties' => ['required', Rule::anyOf([ + [ + 'bio' => 'nullable', + 'name' => 'required', + 'surname' => 'required', + ] + ])], + ] + ]) ]; } From 49869ed17da47895dbccc468a6c2d9bc49e3ab26 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Sat, 29 Mar 2025 16:01:50 +0100 Subject: [PATCH 21/27] chore: apply styleCI --- tests/Validation/ValidationAnyOfRuleTest.php | 34 ++++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index f4a610c8008c..c8c6397f5a86 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -120,7 +120,7 @@ public function testTaggedUnionObjects() 'data' => [ 'type' => TaggedUnionDiscriminatorType::EMAIL->value, 'email' => 'taylor@laravel.com', - ] + ], ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertTrue($validator->passes()); @@ -128,7 +128,7 @@ public function testTaggedUnionObjects() 'data' => [ 'type' => TaggedUnionDiscriminatorType::EMAIL->value, 'email' => 'invalid-email', - ] + ], ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertFalse($validator->passes()); @@ -136,7 +136,7 @@ public function testTaggedUnionObjects() 'data' => [ 'type' => TaggedUnionDiscriminatorType::URL->value, 'url' => 'http://laravel.com', - ] + ], ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertTrue($validator->passes()); @@ -144,7 +144,7 @@ public function testTaggedUnionObjects() 'data' => [ 'type' => TaggedUnionDiscriminatorType::URL->value, 'url' => 'not-a-url', - ] + ], ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertFalse($validator->passes()); @@ -152,7 +152,7 @@ public function testTaggedUnionObjects() 'data' => [ 'type' => TaggedUnionDiscriminatorType::EMAIL->value, 'url' => 'url-should-not-be-present-with-email-discriminator', - ] + ], ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertFalse($validator->passes()); @@ -160,7 +160,7 @@ public function testTaggedUnionObjects() 'data' => [ 'type' => 'doesnt-exist', 'email' => 'taylor@laravel.com', - ] + ], ], ['data' => Rule::anyOf($this->taggedUnionRules)]); $this->assertFalse($validator->passes()); } @@ -173,8 +173,8 @@ public function testNestedValidation() 'properties' => [ 'name' => 'Taylor', 'surname' => 'Otwell', - ] - ] + ], + ], ], $this->nestedRules); $this->assertTrue($validator->passes()); @@ -185,8 +185,8 @@ public function testNestedValidation() 'bio' => 'biography', 'name' => 'Taylor', 'surname' => 'Otwell', - ] - ] + ], + ], ], $this->nestedRules); $this->assertTrue($validator->passes()); @@ -196,8 +196,8 @@ public function testNestedValidation() 'properties' => [ 'name' => null, 'surname' => 'Otwell', - ] - ] + ], + ], ], $this->nestedRules); $this->assertFalse($validator->passes()); @@ -206,8 +206,8 @@ public function testNestedValidation() 'properties' => [ 'name' => 'Taylor', 'surname' => 'Otwell', - ] - ] + ], + ], ], $this->nestedRules); $this->assertFalse($validator->passes()); } @@ -238,10 +238,10 @@ protected function setUpRuleSets() 'bio' => 'nullable', 'name' => 'required', 'surname' => 'required', - ] + ], ])], - ] - ]) + ], + ]), ]; } From 81817edc6cc24bbe9996444b45cdc16dbcd4017a Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Sat, 29 Mar 2025 19:34:19 +0100 Subject: [PATCH 22/27] feat: add tests to verify compliance with dot notation nesting validator --- tests/Validation/ValidationAnyOfRuleTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index c8c6397f5a86..311820e28644 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -20,6 +20,7 @@ enum TaggedUnionDiscriminatorType: string class ValidationAnyOfRuleTest extends TestCase { private array $taggedUnionRules; + private array $dotNotationNestedRules; private array $nestedRules; public function testBasicValidation() @@ -177,6 +178,8 @@ public function testNestedValidation() ], ], $this->nestedRules); $this->assertTrue($validator->passes()); + $validator->setRules($this->dotNotationNestedRules); + $this->assertTrue($validator->passes()); $validator = new Validator(resolve('translator'), [ 'user' => [ @@ -189,6 +192,8 @@ public function testNestedValidation() ], ], $this->nestedRules); $this->assertTrue($validator->passes()); + $validator->setRules($this->dotNotationNestedRules); + $this->assertTrue($validator->passes()); $validator = new Validator(resolve('translator'), [ 'user' => [ @@ -200,6 +205,8 @@ public function testNestedValidation() ], ], $this->nestedRules); $this->assertFalse($validator->passes()); + $validator->setRules($this->dotNotationNestedRules); + $this->assertFalse($validator->passes()); $validator = new Validator(resolve('translator'), [ 'user' => [ @@ -210,6 +217,8 @@ public function testNestedValidation() ], ], $this->nestedRules); $this->assertFalse($validator->passes()); + $validator->setRules($this->dotNotationNestedRules); + $this->assertFalse($validator->passes()); } protected function setUpRuleSets() @@ -243,6 +252,16 @@ protected function setUpRuleSets() ], ]), ]; + + $this->dotNotationNestedRules = [ + 'user.identifier' => ['required', Rule::anyOf([ + 'email:rfc', + 'integer', + ])], + 'user.properties.bio' => 'nullable', + 'user.properties.name' => 'required', + 'user.properties.surname' => 'required', + ]; } protected function setUp(): void From 5630b32b1924f183d60981e0499078b13c9ba3c8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 1 Apr 2025 13:46:14 -0500 Subject: [PATCH 23/27] formatting --- src/Illuminate/Validation/Rules/AnyOf.php | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Illuminate/Validation/Rules/AnyOf.php b/src/Illuminate/Validation/Rules/AnyOf.php index 3be01d2a22e8..3eb7e893e845 100644 --- a/src/Illuminate/Validation/Rules/AnyOf.php +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -17,13 +17,6 @@ class AnyOf implements Rule, ValidatorAwareRule */ protected array $rules = []; - /** - * The error message after validation, if any. - * - * @var array - */ - protected $messages = []; - /** * The validator performing the validation. * @@ -71,8 +64,6 @@ public function passes($attribute, $value) } } - $this->validator->addFailure($attribute, 'any_of'); - return false; } @@ -83,7 +74,11 @@ public function passes($attribute, $value) */ public function message() { - return $this->messages; + $message = $this->validator->getTranslator()->get('validation.any_of'); + + return $message === 'validation.any_of' + ? ['The :attribute field is invalid.'] + : $message; } /** From a40b9d31933a9c376815efea22a87ca59672e483 Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Wed, 2 Apr 2025 00:00:42 +0200 Subject: [PATCH 24/27] fix: remove messages --- src/Illuminate/Validation/Rules/AnyOf.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Illuminate/Validation/Rules/AnyOf.php b/src/Illuminate/Validation/Rules/AnyOf.php index 3eb7e893e845..5edbf7776aa4 100644 --- a/src/Illuminate/Validation/Rules/AnyOf.php +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -49,8 +49,6 @@ public function __construct($rules) */ public function passes($attribute, $value) { - $this->messages = []; - foreach ($this->rules as $rule) { $validator = Validator::make( Arr::isAssoc(Arr::wrap($value)) ? $value : [$attribute => $value], From 9366e99da9b06430825cec7645a1bcc113e275db Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Wed, 2 Apr 2025 00:35:21 +0200 Subject: [PATCH 25/27] fix(wip): regression introduced in 14598f62bf8305bbe2a94ee4e2848d6ec2e05587 https://github.com/laravel/framework/pull/55191#issuecomment-2770383023 --- src/Illuminate/Validation/Rules/AnyOf.php | 4 +-- tests/Validation/ValidationAnyOfRuleTest.php | 28 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Validation/Rules/AnyOf.php b/src/Illuminate/Validation/Rules/AnyOf.php index 5edbf7776aa4..9d653bdaadbe 100644 --- a/src/Illuminate/Validation/Rules/AnyOf.php +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -51,8 +51,8 @@ public function passes($attribute, $value) { foreach ($this->rules as $rule) { $validator = Validator::make( - Arr::isAssoc(Arr::wrap($value)) ? $value : [$attribute => $value], - Arr::isAssoc(Arr::wrap($rule)) ? $rule : [$attribute => $rule], + Arr::isAssoc(Arr::wrap($value)) ? $value : [$value], + Arr::isAssoc(Arr::wrap($rule)) ? $rule : [$rule], $this->validator->customMessages, $this->validator->customAttributes ); diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index 311820e28644..d813e569ce53 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -221,6 +221,34 @@ public function testNestedValidation() $this->assertFalse($validator->passes()); } + public function testStarRuleSimple() + { + $rule = [ + 'persons.*.age' => ['required', Rule::anyOf([ + ['min:10'], + ['integer'], + ])], + ]; + + $validator = new Validator(resolve('translator'), [ + 'persons' => [ + ['age' => 12], + ['age' => 'foobar'], + ], + ], $rule); + + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'persons' => [ + ['age' => 12], + ['age' => 'foobarbazqux'], + ], + ], $rule); + + $this->assertTrue($validator->passes()); + } + protected function setUpRuleSets() { $this->taggedUnionRules = [ From d0edafbf7b1e81ae66bde62bbb0d1d3701e0d73e Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Mon, 7 Apr 2025 11:37:37 +0200 Subject: [PATCH 26/27] feat: implement star rule counter tests for simple and nested rules Co-authored-by: Christian Ascone --- tests/Validation/ValidationAnyOfRuleTest.php | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index d813e569ce53..ca626ec0f37f 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -236,7 +236,14 @@ public function testStarRuleSimple() ['age' => 'foobar'], ], ], $rule); + $this->assertFalse($validator->passes()); + $validator = new Validator(resolve('translator'), [ + 'persons' => [ + ['age' => 'foobarbazqux'], + ['month' => 12], + ], + ], $rule); $this->assertFalse($validator->passes()); $validator = new Validator(resolve('translator'), [ @@ -245,8 +252,58 @@ public function testStarRuleSimple() ['age' => 'foobarbazqux'], ], ], $rule); + $this->assertTrue($validator->passes()); + } + public function testStarRuleNested() + { + $rule = [ + 'persons.*.birth' => ['required', Rule::anyOf([ + ['year' => 'required|integer'], + 'required|min:10', + ])], + ]; + + $validator = new Validator(resolve('translator'), [ + 'persons' => [ + ['age' => ['year' => 12]], + ], + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'persons' => [ + ['birth' => ['month' => 12]], + ], + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'persons' => [ + ['birth' => ['year' => 12]] + ], + ], $rule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'persons' => [ + ['birth' => 'foobarbazqux'], + ['birth' => [ + 'year' => 12, + ]], + ], + ], $rule); $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'persons' => [ + ['birth' => 'foobar'], + ['birth' => [ + 'year' => 12, + ]], + ], + ], $rule); + $this->assertFalse($validator->passes()); } protected function setUpRuleSets() From 94b46ab7922e45c590dcbaecf5e427930c54b19f Mon Sep 17 00:00:00 2001 From: Brian Ferri Date: Mon, 7 Apr 2025 11:42:03 +0200 Subject: [PATCH 27/27] chore: apply styleCI --- tests/Validation/ValidationAnyOfRuleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Validation/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php index ca626ec0f37f..f0806182c693 100644 --- a/tests/Validation/ValidationAnyOfRuleTest.php +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -280,7 +280,7 @@ public function testStarRuleNested() $validator = new Validator(resolve('translator'), [ 'persons' => [ - ['birth' => ['year' => 12]] + ['birth' => ['year' => 12]], ], ], $rule); $this->assertTrue($validator->passes());