Skip to content

Commit 745687e

Browse files
Albert Schermantaylorotwell
andauthored
[9.x] Added support for index and position placeholders in array validation messages (#41123)
* Added :index and :pos placeholders for array validation messages * Added check for non array inputs * Code analysis changes * Code analysis changes * Review changes * CS Changes * Update ValidationValidatorTest.php * Update FormatsMessages.php * Update FormatsMessages.php * Update ValidationValidatorTest.php Co-authored-by: Taylor Otwell <[email protected]>
1 parent 0f8624f commit 745687e

File tree

2 files changed

+166
-0
lines changed

2 files changed

+166
-0
lines changed

src/Illuminate/Validation/Concerns/FormatsMessages.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ public function makeReplacements($message, $attribute, $rule, $parameters)
229229
);
230230

231231
$message = $this->replaceInputPlaceholder($message, $attribute);
232+
$message = $this->replaceIndexPlaceholder($message, $attribute);
233+
$message = $this->replacePositionPlaceholder($message, $attribute);
232234

233235
if (isset($this->replacers[Str::snake($rule)])) {
234236
return $this->callReplacer($message, $attribute, Str::snake($rule), $parameters, $this);
@@ -307,6 +309,92 @@ protected function replaceAttributePlaceholder($message, $value)
307309
);
308310
}
309311

312+
/**
313+
* Replace the :index placeholder in the given message.
314+
*
315+
* @param string $message
316+
* @param string $attribute
317+
* @return string
318+
*/
319+
protected function replaceIndexPlaceholder($message, $attribute)
320+
{
321+
return $this->replaceIndexOrPositionPlaceholder(
322+
$message, $attribute, 'index'
323+
);
324+
}
325+
326+
/**
327+
* Replace the :position placeholder in the given message.
328+
*
329+
* @param string $message
330+
* @param string $attribute
331+
* @return string
332+
*/
333+
protected function replacePositionPlaceholder($message, $attribute)
334+
{
335+
return $this->replaceIndexOrPositionPlaceholder(
336+
$message, $attribute, 'position', fn ($segment) => $segment + 1
337+
);
338+
}
339+
340+
/**
341+
* Replace the :index or :position placeholder in the given message.
342+
*
343+
* @param string $message
344+
* @param string $attribute
345+
* @param string $placeholder
346+
* @param \Closure $modifier
347+
* @return string
348+
*/
349+
protected function replaceIndexOrPositionPlaceholder($message, $attribute, $placeholder, Closure $modifier = null)
350+
{
351+
$segments = explode('.', $attribute);
352+
353+
$modifier ??= fn ($value) => $value;
354+
355+
$numericIndex = 1;
356+
357+
foreach ($segments as $segment) {
358+
if (is_numeric($segment)) {
359+
if ($numericIndex === 1) {
360+
$message = str_ireplace(':'.$placeholder, $modifier((int) $segment), $message);
361+
}
362+
363+
$message = str_ireplace(
364+
':'.$this->numberToIndexOrPositionWord($numericIndex).'-'.$placeholder,
365+
$modifier((int) $segment),
366+
$message
367+
);
368+
369+
$numericIndex++;
370+
}
371+
}
372+
373+
return $message;
374+
}
375+
376+
/**
377+
* Get the word for a index or position segment.
378+
*
379+
* @param int $value
380+
* @return string
381+
*/
382+
protected function numberToIndexOrPositionWord(int $value)
383+
{
384+
return [
385+
1 => 'first',
386+
2 => 'second',
387+
3 => 'third',
388+
4 => 'fourth',
389+
5 => 'fifth',
390+
6 => 'sixth',
391+
7 => 'seventh',
392+
8 => 'eighth',
393+
9 => 'ninth',
394+
10 => 'tenth',
395+
][(int) $value] ?? 'other';
396+
}
397+
310398
/**
311399
* Replace the :input placeholder in the given message.
312400
*

tests/Validation/ValidationValidatorTest.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,84 @@ public function testDisplayableAttributesAreReplacedInCustomReplacers()
585585
new Validator($trans, ['firstname' => 'Bob', 'lastname' => 'Smith'], ['lastname' => 'alliteration:firstname']);
586586
}
587587

588+
public function testIndexValuesAreReplaced()
589+
{
590+
$trans = $this->getIlluminateArrayTranslator();
591+
592+
// $v = new Validator($trans, ['name' => ''], ['name' => 'required'], ['name.required' => 'Name :index is required.']);
593+
// $this->assertFalse($v->passes());
594+
// $this->assertSame('Name 0 is required.', $v->messages()->first('name'));
595+
596+
$v = new Validator($trans, ['input' => [['name' => '']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :index is required.']);
597+
$this->assertFalse($v->passes());
598+
$this->assertSame('Name 0 is required.', $v->messages()->first('input.*.name'));
599+
$v = new Validator($trans, ['input' => [['name' => '']]], ['input.*.name' => 'required'], ['input.*.name.required' => ':Attribute :index is required.']);
600+
$v->setAttributeNames([
601+
'input.*.name' => 'name',
602+
]);
603+
$this->assertFalse($v->passes());
604+
$this->assertSame('Name 0 is required.', $v->messages()->first('input.*.name'));
605+
606+
$v = new Validator($trans, [
607+
'input' => [
608+
[
609+
'name' => '',
610+
'attributes' => [
611+
'foo',
612+
1,
613+
],
614+
]
615+
]
616+
], ['input.*.attributes.*' => 'string'], ['input.*.attributes.*.string' => 'Attribute (:first-index, :first-position) (:second-index, :second-position) must be a string.']);
617+
$this->assertFalse($v->passes());
618+
$this->assertSame('Attribute (0, 1) (1, 2) must be a string.', $v->messages()->first('input.*.attributes.*'));
619+
620+
$v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => ''], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :index is required.']);
621+
$this->assertFalse($v->passes());
622+
$this->assertSame('Name 1 is required.', $v->messages()->first('input.*.name'));
623+
$v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => ''], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => ':Attribute :index is required.']);
624+
$v->setAttributeNames([
625+
'input.*.name' => 'name',
626+
]);
627+
$this->assertFalse($v->passes());
628+
$this->assertSame('Name 1 is required.', $v->messages()->first('input.*.name'));
629+
630+
$v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :index is required.']);
631+
$this->assertTrue($v->passes());
632+
}
633+
634+
public function testPositionValuesAreReplaced()
635+
{
636+
$trans = $this->getIlluminateArrayTranslator();
637+
638+
// $v = new Validator($trans, ['name' => ''], ['name' => 'required'], ['name.required' => 'Name :position is required.']);
639+
// $this->assertFalse($v->passes());
640+
// $this->assertSame('Name 1 is required.', $v->messages()->first('name'));
641+
642+
$v = new Validator($trans, ['input' => [['name' => '']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :position is required.']);
643+
$this->assertFalse($v->passes());
644+
$this->assertSame('Name 1 is required.', $v->messages()->first('input.*.name'));
645+
$v = new Validator($trans, ['input' => [['name' => '']]], ['input.*.name' => 'required'], ['input.*.name.required' => ':Attribute :position is required.']);
646+
$v->setAttributeNames([
647+
'input.*.name' => 'name',
648+
]);
649+
$this->assertFalse($v->passes());
650+
$this->assertSame('Name 1 is required.', $v->messages()->first('input.*.name'));
651+
652+
$v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => ''], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :position is required.']);
653+
$this->assertFalse($v->passes());
654+
$this->assertSame('Name 2 is required.', $v->messages()->first('input.*.name'));
655+
$v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => ''], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => ':Attribute :position is required.']);
656+
$v->setAttributeNames([
657+
'input.*.name' => 'name',
658+
]);
659+
$this->assertFalse($v->passes());
660+
$this->assertSame('Name 2 is required.', $v->messages()->first('input.*.name'));
661+
662+
$v = new Validator($trans, ['input' => [['name' => 'Bob'], ['name' => 'Jane']]], ['input.*.name' => 'required'], ['input.*.name.required' => 'Name :position is required.']);
663+
$this->assertTrue($v->passes());
664+
}
665+
588666
public function testCustomValidationLinesAreRespected()
589667
{
590668
$trans = $this->getIlluminateArrayTranslator();

0 commit comments

Comments
 (0)