diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 557ab19f57a2..efe08f28cf26 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -307,11 +307,11 @@ class Validator implements ValidatorContract protected $defaultNumericRules = ['Numeric', 'Integer', 'Decimal']; /** - * The current placeholder for dots in rule keys. + * The current random hash for the validator. * * @var string */ - protected $dotPlaceholder; + protected static $placeholderHash; /** * The exception to throw upon failure. @@ -344,7 +344,9 @@ public function __construct( array $messages = [], array $attributes = [], ) { - $this->dotPlaceholder = Str::random(); + if (! isset(static::$placeholderHash)) { + static::$placeholderHash = Str::random(); + } $this->initialRules = $rules; $this->translator = $translator; @@ -372,7 +374,7 @@ public function parseData(array $data) $key = str_replace( ['.', '*'], - [$this->dotPlaceholder, '__asterisk__'], + ['__dot__'.static::$placeholderHash, '__asterisk__'.static::$placeholderHash], $key ); @@ -410,7 +412,7 @@ protected function replacePlaceholders($data) protected function replacePlaceholderInString(string $value) { return str_replace( - [$this->dotPlaceholder, '__asterisk__'], + ['__dot__'.static::$placeholderHash, '__asterisk__'.static::$placeholderHash], ['.', '*'], $value ); @@ -425,7 +427,7 @@ protected function replacePlaceholderInString(string $value) protected function replaceDotPlaceholderInParameters(array $parameters) { return array_map(function ($field) { - return str_replace($this->dotPlaceholder, '.', $field); + return str_replace('__dot__'.static::$placeholderHash, '.', $field); }, $parameters); } @@ -746,7 +748,7 @@ protected function getPrimaryAttribute($attribute) protected function replaceDotInParameters(array $parameters) { return array_map(function ($field) { - return str_replace('\.', $this->dotPlaceholder, $field); + return str_replace('\.', '__dot__'.static::$placeholderHash, $field); }, $parameters); } @@ -872,11 +874,24 @@ protected function hasNotFailedPreviousRuleIfPresenceRule($rule, $attribute) */ protected function validateUsingCustomRule($attribute, $value, $rule) { - $attribute = $this->replacePlaceholderInString($attribute); + $originalAttribute = $this->replacePlaceholderInString($attribute); + + $attribute = match (true) { + $rule instanceof Rules\Email => $attribute, + $rule instanceof Rules\File => $attribute, + $rule instanceof Rules\Password => $attribute, + default => $originalAttribute, + }; $value = is_array($value) ? $this->replacePlaceholders($value) : $value; if ($rule instanceof ValidatorAwareRule) { + if ($attribute !== $originalAttribute) { + $this->addCustomAttributes([ + $attribute => $this->customAttributes[$originalAttribute] ?? $originalAttribute, + ]); + } + $rule->setValidator($this); } @@ -889,14 +904,14 @@ protected function validateUsingCustomRule($attribute, $value, $rule) get_class($rule->invokable()) : get_class($rule); - $this->failedRules[$attribute][$ruleClass] = []; + $this->failedRules[$originalAttribute][$ruleClass] = []; - $messages = $this->getFromLocalArray($attribute, $ruleClass) ?? $rule->message(); + $messages = $this->getFromLocalArray($originalAttribute, $ruleClass) ?? $rule->message(); $messages = $messages ? (array) $messages : [$ruleClass]; foreach ($messages as $key => $message) { - $key = is_string($key) ? $key : $attribute; + $key = is_string($key) ? $key : $originalAttribute; $this->messages->add($key, $this->makeReplacements( $message, $key, $ruleClass, [] @@ -1189,7 +1204,7 @@ public function getRulesWithoutPlaceholders() { return (new Collection($this->rules)) ->mapWithKeys(fn ($value, $key) => [ - str_replace($this->dotPlaceholder, '\\.', $key) => $value, + str_replace('__dot__'.static::$placeholderHash, '\\.', $key) => $value, ]) ->all(); } @@ -1203,7 +1218,7 @@ public function getRulesWithoutPlaceholders() public function setRules(array $rules) { $rules = (new Collection($rules))->mapWithKeys(function ($value, $key) { - return [str_replace('\.', $this->dotPlaceholder, $key) => $value]; + return [str_replace('\.', '__dot__'.static::$placeholderHash, $key) => $value]; })->toArray(); $this->initialRules = $rules; diff --git a/tests/Integration/Validation/Rules/EmailValidationTest.php b/tests/Integration/Validation/Rules/EmailValidationTest.php new file mode 100644 index 000000000000..558bef77a972 --- /dev/null +++ b/tests/Integration/Validation/Rules/EmailValidationTest.php @@ -0,0 +1,49 @@ + [ + $attribute => 'taylor@laravel.com', + ], + ], [ + 'emails.*' => ['required', Email::default()->rfcCompliant()], + ]); + + $this->assertTrue($validator->passes()); + } + + #[TestWith(['0'])] + #[TestWith(['.'])] + #[TestWith(['*'])] + #[TestWith(['__asterisk__'])] + public function test_it_can_validate_attribute_as_array_when_validation_should_fails(string $attribute) + { + $validator = Validator::make([ + 'emails' => [ + $attribute => 'taylor[at]laravel.com', + ], + ], [ + 'emails.*' => ['required', Email::default()->rfcCompliant()], + ]); + + $this->assertFalse($validator->passes()); + + $this->assertSame([ + 0 => __('validation.email', ['attribute' => sprintf('emails.%s', str_replace('_', ' ', $attribute))]), + ], $validator->messages()->all()); + } +} diff --git a/tests/Integration/Validation/Rules/FileValidationTest.php b/tests/Integration/Validation/Rules/FileValidationTest.php new file mode 100644 index 000000000000..ba0d54920e46 --- /dev/null +++ b/tests/Integration/Validation/Rules/FileValidationTest.php @@ -0,0 +1,54 @@ +create('laravel.png', 1, 'image/png'); + + $validator = Validator::make([ + 'files' => [ + $attribute => $file, + ], + ], [ + 'files.*' => ['required', File::types(['image/png', 'image/jpeg'])], + ]); + + $this->assertTrue($validator->passes()); + } + + #[TestWith(['0'])] + #[TestWith(['.'])] + #[TestWith(['*'])] + #[TestWith(['__asterisk__'])] + public function test_it_can_validate_attribute_as_array_when_validation_should_fails(string $attribute) + { + $file = UploadedFile::fake()->create('laravel.php', 1, 'image/php'); + + $validator = Validator::make([ + 'files' => [ + $attribute => $file, + ], + ], [ + 'files.*' => ['required', File::types($mimes = ['image/png', 'image/jpeg'])], + ]); + + $this->assertFalse($validator->passes()); + + $this->assertSame([ + 0 => __('validation.mimetypes', ['attribute' => sprintf('files.%s', str_replace('_', ' ', $attribute)), 'values' => implode(', ', $mimes)]), + ], $validator->messages()->all()); + } +} diff --git a/tests/Integration/Validation/Rules/PasswordValidationTest.php b/tests/Integration/Validation/Rules/PasswordValidationTest.php new file mode 100644 index 000000000000..e1f7672ac89b --- /dev/null +++ b/tests/Integration/Validation/Rules/PasswordValidationTest.php @@ -0,0 +1,49 @@ + [ + $attribute => 'secret', + ], + ], [ + 'passwords.*' => ['required', Password::default()->min(6)], + ]); + + $this->assertTrue($validator->passes()); + } + + #[TestWith(['0'])] + #[TestWith(['.'])] + #[TestWith(['*'])] + #[TestWith(['__asterisk__'])] + public function test_it_can_validate_attribute_as_array_when_validation_should_fails(string $attribute) + { + $validator = Validator::make([ + 'passwords' => [ + $attribute => 'secret', + ], + ], [ + 'passwords.*' => ['required', Password::default()->min(8)], + ]); + + $this->assertFalse($validator->passes()); + + $this->assertSame([ + 0 => sprintf('The passwords.%s field must be at least 8 characters.', str_replace('_', ' ', $attribute)), + ], $validator->messages()->all()); + } +}