diff --git a/src/Illuminate/Translation/lang/en/validation.php b/src/Illuminate/Translation/lang/en/validation.php index f19bd64ed6c9..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.', diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php index 44bb2b4347b5..170f4d04a1ea 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; @@ -246,6 +247,19 @@ public static function numeric() return new Numeric; } + /** + * Get an "any of" rule builder instance. + * + * @param array + * @return \Illuminate\Validation\Rules\AnyOf + * + * @throws \InvalidArgumentException + */ + public static function anyOf($rules) + { + return new AnyOf($rules); + } + /** * Compile a set of rules for an attribute. * diff --git a/src/Illuminate/Validation/Rules/AnyOf.php b/src/Illuminate/Validation/Rules/AnyOf.php new file mode 100644 index 000000000000..9d653bdaadbe --- /dev/null +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -0,0 +1,94 @@ +rules = $rules; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + foreach ($this->rules as $rule) { + $validator = Validator::make( + Arr::isAssoc(Arr::wrap($value)) ? $value : [$value], + Arr::isAssoc(Arr::wrap($rule)) ? $rule : [$rule], + $this->validator->customMessages, + $this->validator->customAttributes + ); + + if ($validator->passes()) { + return true; + } + } + + return false; + } + + /** + * Get the validation error messages. + * + * @return array + */ + public function message() + { + $message = $this->validator->getTranslator()->get('validation.any_of'); + + return $message === 'validation.any_of' + ? ['The :attribute field is invalid.'] + : $message; + } + + /** + * 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/ValidationAnyOfRuleTest.php b/tests/Validation/ValidationAnyOfRuleTest.php new file mode 100644 index 000000000000..f0806182c693 --- /dev/null +++ b/tests/Validation/ValidationAnyOfRuleTest.php @@ -0,0 +1,376 @@ + $rule]; + $requiredIdRule = ['id' => ['required', $rule]]; + + $validator = new Validator(resolve('translator'), [ + 'id' => 'taylor@laravel.com', + ], $idRule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [], $idRule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [], $requiredIdRule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'id' => '3c8ff5cb-4bc1-457b-a477-1833c477b254', + ], $idRule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'id' => null, + ], $idRule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'id' => '', + ], $idRule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'id' => '', + ], $requiredIdRule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'id' => 'abc', + ], $idRule); + $this->assertFalse($validator->passes()); + } + + public function testBasicStringValidation() + { + $rule = Rule::anyOf([ + 'required|uuid:4', + 'required|email', + ]); + $idRule = ['id' => $rule]; + $requiredIdRule = ['id' => ['required', $rule]]; + + $validator = new Validator(resolve('translator'), [ + 'id' => 'test@example.com', + ], $idRule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [], $idRule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [], $requiredIdRule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'id' => '3c8ff5cb-4bc1-457b-a477-1833c477b254', + ], $idRule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'id' => null, + ], $idRule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'id' => '', + ], $idRule); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'id' => '', + ], $requiredIdRule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'id' => 'abc', + ], $idRule); + $this->assertFalse($validator->passes()); + } + + public function testTaggedUnionObjects() + { + $validator = new Validator(resolve('translator'), [ + 'data' => [ + 'type' => TaggedUnionDiscriminatorType::EMAIL->value, + 'email' => 'taylor@laravel.com', + ], + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'data' => [ + 'type' => TaggedUnionDiscriminatorType::EMAIL->value, + 'email' => 'invalid-email', + ], + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'data' => [ + 'type' => TaggedUnionDiscriminatorType::URL->value, + 'url' => 'http://laravel.com', + ], + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'data' => [ + 'type' => TaggedUnionDiscriminatorType::URL->value, + 'url' => 'not-a-url', + ], + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + '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'), [ + 'data' => [ + 'type' => 'doesnt-exist', + 'email' => 'taylor@laravel.com', + ], + ], ['data' => Rule::anyOf($this->taggedUnionRules)]); + $this->assertFalse($validator->passes()); + } + + public function testNestedValidation() + { + $validator = new Validator(resolve('translator'), [ + 'user' => [ + 'identifier' => 1, + 'properties' => [ + 'name' => 'Taylor', + 'surname' => 'Otwell', + ], + ], + ], $this->nestedRules); + $this->assertTrue($validator->passes()); + $validator->setRules($this->dotNotationNestedRules); + $this->assertTrue($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'user' => [ + 'identifier' => 'taylor@laravel.com', + 'properties' => [ + 'bio' => 'biography', + 'name' => 'Taylor', + 'surname' => 'Otwell', + ], + ], + ], $this->nestedRules); + $this->assertTrue($validator->passes()); + $validator->setRules($this->dotNotationNestedRules); + $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->setRules($this->dotNotationNestedRules); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'user' => [ + 'properties' => [ + 'name' => 'Taylor', + 'surname' => 'Otwell', + ], + ], + ], $this->nestedRules); + $this->assertFalse($validator->passes()); + $validator->setRules($this->dotNotationNestedRules); + $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' => 'foobarbazqux'], + ['month' => 12], + ], + ], $rule); + $this->assertFalse($validator->passes()); + + $validator = new Validator(resolve('translator'), [ + 'persons' => [ + ['age' => 12], + ['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() + { + $this->taggedUnionRules = [ + [ + 'type' => ['required', Rule::in([TaggedUnionDiscriminatorType::EMAIL])], + 'email' => ['required', 'email:rfc'], + ], + [ + 'type' => ['required', Rule::in([TaggedUnionDiscriminatorType::URL])], + 'url' => ['required', 'url:http,https'], + ], + ]; + + // Using AnyOf as nesting feature + $this->nestedRules = [ + 'user' => Rule::anyOf([ + [ + 'identifier' => ['required', Rule::anyOf([ + 'email:rfc', + 'integer', + ])], + 'properties' => ['required', Rule::anyOf([ + [ + 'bio' => 'nullable', + 'name' => 'required', + 'surname' => 'required', + ], + ])], + ], + ]), + ]; + + $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 + { + 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); + Facade::clearResolvedInstances(); + Facade::setFacadeApplication(null); + } +}