Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[9.x] Added support for index and position placeholders in array validation messages #41123

Merged
merged 10 commits into from
Feb 24, 2022
88 changes: 88 additions & 0 deletions src/Illuminate/Validation/Concerns/FormatsMessages.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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.
*
Expand Down
122 changes: 121 additions & 1 deletion tests/Validation/ValidationValidatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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();
Expand Down