diff --git a/src/Illuminate/Validation/Concerns/FormatsMessages.php b/src/Illuminate/Validation/Concerns/FormatsMessages.php index c9c1596fda1e..67a7c4df63a4 100644 --- a/src/Illuminate/Validation/Concerns/FormatsMessages.php +++ b/src/Illuminate/Validation/Concerns/FormatsMessages.php @@ -229,6 +229,8 @@ public function makeReplacements($message, $attribute, $rule, $parameters) ); $message = $this->replaceInputPlaceholder($message, $attribute); + $message = $this->replaceIndexPlaceholder($message, $attribute); + $message = $this->replacePositionPlaceholder($message, $attribute); if (isset($this->replacers[Str::snake($rule)])) { return $this->callReplacer($message, $attribute, Str::snake($rule), $parameters, $this); @@ -307,6 +309,92 @@ protected function replaceAttributePlaceholder($message, $value) ); } + /** + * Replace the :index placeholder in the given message. + * + * @param string $message + * @param string $attribute + * @return string + */ + protected function replaceIndexPlaceholder($message, $attribute) + { + return $this->replaceIndexOrPositionPlaceholder( + $message, $attribute, 'index' + ); + } + + /** + * Replace the :position placeholder in the given message. + * + * @param string $message + * @param string $attribute + * @return string + */ + protected function replacePositionPlaceholder($message, $attribute) + { + return $this->replaceIndexOrPositionPlaceholder( + $message, $attribute, 'position', fn ($segment) => $segment + 1 + ); + } + + /** + * Replace the :index or :position placeholder in the given message. + * + * @param string $message + * @param string $attribute + * @param string $placeholder + * @param \Closure $modifier + * @return string + */ + protected function replaceIndexOrPositionPlaceholder($message, $attribute, $placeholder, Closure $modifier = null) + { + $segments = explode('.', $attribute); + + $modifier ??= fn ($value) => $value; + + $numericIndex = 1; + + foreach ($segments as $segment) { + if (is_numeric($segment)) { + if ($numericIndex === 1) { + $message = str_ireplace(':'.$placeholder, $modifier((int) $segment), $message); + } + + $message = str_ireplace( + ':'.$this->numberToIndexOrPositionWord($numericIndex).'-'.$placeholder, + $modifier((int) $segment), + $message + ); + + $numericIndex++; + } + } + + return $message; + } + + /** + * Get the word for a index or position segment. + * + * @param int $value + * @return string + */ + protected function numberToIndexOrPositionWord(int $value) + { + return [ + 1 => 'first', + 2 => 'second', + 3 => 'third', + 4 => 'fourth', + 5 => 'fifth', + 6 => 'sixth', + 7 => 'seventh', + 8 => 'eighth', + 9 => 'ninth', + 10 => 'tenth', + ][(int) $value] ?? 'other'; + } + /** * Replace the :input placeholder in the given message. * diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index a0bf005441fa..6799b3da6692 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -28,6 +28,7 @@ use InvalidArgumentException; use Mockery as m; use PHPUnit\Framework\TestCase; +use RuntimeException; use stdClass; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\UploadedFile; @@ -584,6 +585,84 @@ public function testDisplayableAttributesAreReplacedInCustomReplacers() new Validator($trans, ['firstname' => 'Bob', 'lastname' => 'Smith'], ['lastname' => 'alliteration:firstname']); } + public function testIndexValuesAreReplaced() + { + $trans = $this->getIlluminateArrayTranslator(); + + // $v = new Validator($trans, ['name' => ''], ['name' => 'required'], ['name.required' => 'Name :index is required.']); + // $this->assertFalse($v->passes()); + // $this->assertSame('Name 0 is required.', $v->messages()->first('name')); + + $v = new Validator($trans, ['input' => [['name' => '']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :index is required.']); + $this->assertFalse($v->passes()); + $this->assertSame('Name 0 is required.', $v->messages()->first('input.*.name')); + $v = new Validator($trans, ['input' => [['name' => '']]], ['input.*.name' => 'required'], ['input.*.name.required' => ':Attribute :index is required.']); + $v->setAttributeNames([ + 'input.*.name' => 'name', + ]); + $this->assertFalse($v->passes()); + $this->assertSame('Name 0 is required.', $v->messages()->first('input.*.name')); + + $v = new Validator($trans, [ + 'input' => [ + [ + 'name' => '', + 'attributes' => [ + 'foo', + 1, + ], + ] + ] + ], ['input.*.attributes.*' => 'string'], ['input.*.attributes.*.string' => 'Attribute (:first-index, :first-position) (:second-index, :second-position) must be a string.']); + $this->assertFalse($v->passes()); + $this->assertSame('Attribute (0, 1) (1, 2) must be a string.', $v->messages()->first('input.*.attributes.*')); + + $v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => ''], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :index is required.']); + $this->assertFalse($v->passes()); + $this->assertSame('Name 1 is required.', $v->messages()->first('input.*.name')); + $v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => ''], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => ':Attribute :index is required.']); + $v->setAttributeNames([ + 'input.*.name' => 'name', + ]); + $this->assertFalse($v->passes()); + $this->assertSame('Name 1 is required.', $v->messages()->first('input.*.name')); + + $v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :index is required.']); + $this->assertTrue($v->passes()); + } + + public function testPositionValuesAreReplaced() + { + $trans = $this->getIlluminateArrayTranslator(); + + // $v = new Validator($trans, ['name' => ''], ['name' => 'required'], ['name.required' => 'Name :position is required.']); + // $this->assertFalse($v->passes()); + // $this->assertSame('Name 1 is required.', $v->messages()->first('name')); + + $v = new Validator($trans, ['input' => [['name' => '']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :position is required.']); + $this->assertFalse($v->passes()); + $this->assertSame('Name 1 is required.', $v->messages()->first('input.*.name')); + $v = new Validator($trans, ['input' => [['name' => '']]], ['input.*.name' => 'required'], ['input.*.name.required' => ':Attribute :position is required.']); + $v->setAttributeNames([ + 'input.*.name' => 'name', + ]); + $this->assertFalse($v->passes()); + $this->assertSame('Name 1 is required.', $v->messages()->first('input.*.name')); + + $v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => ''], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :position is required.']); + $this->assertFalse($v->passes()); + $this->assertSame('Name 2 is required.', $v->messages()->first('input.*.name')); + $v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => ''], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => ':Attribute :position is required.']); + $v->setAttributeNames([ + 'input.*.name' => 'name', + ]); + $this->assertFalse($v->passes()); + $this->assertSame('Name 2 is required.', $v->messages()->first('input.*.name')); + + $v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :position is required.']); + $this->assertTrue($v->passes()); + } + public function testCustomValidationLinesAreRespected() { $trans = $this->getIlluminateArrayTranslator(); @@ -678,7 +757,7 @@ public function testCustomExceptionMustExtendValidationException() $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Exception [RuntimeException] is invalid. It must extend [Illuminate\Validation\ValidationException].'); - $v->setException(\RuntimeException::class); + $v->setException(RuntimeException::class); } public function testValidationDotCustomDotAnythingCanBeTranslated() @@ -732,6 +811,47 @@ public function testInlineValidationMessagesAreRespectedWithAsterisks() $this->assertSame('all must be required!', $v->messages()->first('name.1')); } + public function testInlineValidationMessagesForRuleObjectsAreRespected() + { + $rule = new class implements Rule + { + public function passes($attribute, $value) + { + return false; + } + + public function message() + { + return 'this is my message'; + } + }; + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['name' => 'Taylor'], ['name' => $rule], [$rule::class => 'my custom message']); + $this->assertFalse($v->passes()); + $v->messages()->setFormat(':message'); + $this->assertSame('my custom message', $v->messages()->first('name')); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['name' => 'Ryan'], ['name' => $rule], ['name.'.$rule::class => 'my custom message']); + $this->assertFalse($v->passes()); + $v->messages()->setFormat(':message'); + $this->assertSame('my custom message', $v->messages()->first('name')); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['name' => ['foo', 'bar']], ['name.*' => $rule], ['name.*.'.$rule::class => 'my custom message']); + $this->assertFalse($v->passes()); + $v->messages()->setFormat(':message'); + $this->assertSame('my custom message', $v->messages()->first('name.0')); + $this->assertSame('my custom message', $v->messages()->first('name.1')); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['name' => 'Ryan'], ['name' => $rule], [$rule::class => 'my attribute is :attribute']); + $this->assertFalse($v->passes()); + $v->messages()->setFormat(':message'); + $this->assertSame('my attribute is name', $v->messages()->first('name')); + } + public function testIfRulesAreSuccessfullyAdded() { $trans = $this->getIlluminateArrayTranslator();