Skip to content

Commit dc75c06

Browse files
authored
feat: support {field}, {param}, {value} placeholders in rule $error messages (#10024)
* feat: support {field}, {param}, {value} placeholders in rule $error messages * add changelog * add user guide note * update docs
1 parent e08562d commit dc75c06

File tree

8 files changed

+192
-17
lines changed

8 files changed

+192
-17
lines changed

system/Validation/Validation.php

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -369,14 +369,16 @@ protected function processRules(
369369
$fieldForErrors = ($rule === 'field_exists') ? $originalField : $field;
370370

371371
// @phpstan-ignore-next-line $error may be set by rule methods.
372-
$this->errors[$fieldForErrors] = $error ?? $this->getErrorMessage(
373-
($this->isClosure($rule) || $arrayCallable) ? (string) $i : $rule,
374-
$field,
375-
$label,
376-
$param,
377-
(string) $value,
378-
$originalField,
379-
);
372+
$this->errors[$fieldForErrors] = $error !== null
373+
? $this->parseErrorMessage($error, $field, $label, $param, (string) $value)
374+
: $this->getErrorMessage(
375+
($this->isClosure($rule) || $arrayCallable) ? (string) $i : $rule,
376+
$field,
377+
$label,
378+
$param,
379+
(string) $value,
380+
$originalField,
381+
);
380382

381383
return false;
382384
}
@@ -933,13 +935,7 @@ protected function getErrorMessage(
933935
?string $value = null,
934936
?string $originalField = null,
935937
): string {
936-
$param ??= '';
937-
938-
$args = [
939-
'field' => ($label === null || $label === '') ? $field : lang($label),
940-
'param' => isset($this->rules[$param]['label']) ? lang($this->rules[$param]['label']) : $param,
941-
'value' => $value ?? '',
942-
];
938+
$args = $this->buildErrorArgs($field, $label, $param, $value);
943939

944940
// Check if custom message has been defined by user
945941
if (isset($this->customErrors[$field][$rule])) {
@@ -955,6 +951,49 @@ protected function getErrorMessage(
955951
return lang('Validation.' . $rule, $args);
956952
}
957953

954+
/**
955+
* Substitutes {field}, {param}, and {value} placeholders in an error message
956+
* set directly by a rule method via the $error reference parameter.
957+
*
958+
* Uses simple string replacement rather than lang() to avoid ICU MessageFormatter
959+
* warnings on unrecognised patterns and to leave any other {xyz} content untouched.
960+
*/
961+
private function parseErrorMessage(
962+
string $message,
963+
string $field,
964+
?string $label = null,
965+
?string $param = null,
966+
?string $value = null,
967+
): string {
968+
$args = $this->buildErrorArgs($field, $label, $param, $value);
969+
970+
return str_replace(
971+
['{field}', '{param}', '{value}'],
972+
[$args['field'], $args['param'], $args['value']],
973+
$message,
974+
);
975+
}
976+
977+
/**
978+
* Builds the placeholder arguments array used for error message substitution.
979+
*
980+
* @return array{field: string, param: string, value: string}
981+
*/
982+
private function buildErrorArgs(
983+
string $field,
984+
?string $label = null,
985+
?string $param = null,
986+
?string $value = null,
987+
): array {
988+
$param ??= '';
989+
990+
return [
991+
'field' => ($label === null || $label === '') ? $field : lang($label),
992+
'param' => isset($this->rules[$param]['label']) ? lang($this->rules[$param]['label']) : $param,
993+
'value' => $value ?? '',
994+
];
995+
}
996+
958997
/**
959998
* Split rules string by pipe operator.
960999
*/

tests/_support/Validation/TestRules.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,16 @@ public function customError(string $str, ?string &$error = null)
2525
return false;
2626
}
2727

28+
/**
29+
* @param-out string $error
30+
*/
31+
public function custom_error_with_param(mixed $str, string $param, array $data, ?string &$error = null, string $field = ''): bool
32+
{
33+
$error = 'The {field} must be one of: {param}. Got: {value}';
34+
35+
return false;
36+
}
37+
2838
public function check_object_rule(object $value, ?string $fields, array $data = [])
2939
{
3040
$find = false;

tests/system/Validation/ValidationTest.php

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,35 @@ static function ($value, $data, &$error, $field): bool {
342342
$this->assertSame([], $this->validation->getValidated());
343343
}
344344

345+
public function testClosureRuleWithParamErrorPlaceholders(): void
346+
{
347+
$this->validation->setRules([
348+
'status' => [
349+
'label' => 'Status',
350+
'rules' => [
351+
static function ($value, $data, &$error, $field): bool {
352+
if ($value !== 'active') {
353+
$error = 'The field {field} must be one of: {param}. Received: {value}';
354+
355+
return false;
356+
}
357+
358+
return true;
359+
},
360+
],
361+
],
362+
]);
363+
364+
$data = ['status' => 'invalid'];
365+
$result = $this->validation->run($data);
366+
367+
$this->assertFalse($result);
368+
$this->assertSame(
369+
['status' => 'The field Status must be one of: . Received: invalid'],
370+
$this->validation->getErrors(),
371+
);
372+
}
373+
345374
public function testClosureRuleWithLabel(): void
346375
{
347376
$this->validation->setRules([
@@ -415,6 +444,22 @@ public function rule2(mixed $value, array $data, ?string &$error, string $field)
415444
return true;
416445
}
417446

447+
/**
448+
* Validation rule3
449+
*
450+
* @param array<string, mixed> $data
451+
*/
452+
public function rule3(mixed $value, array $data, ?string &$error, string $field): bool
453+
{
454+
if ($value !== 'active') {
455+
$error = 'The field {field} must be one of: {param}. Received: {value}';
456+
457+
return false;
458+
}
459+
460+
return true;
461+
}
462+
418463
public function testCallableRuleWithParamError(): void
419464
{
420465
$this->validation->setRules([
@@ -435,6 +480,68 @@ public function testCallableRuleWithParamError(): void
435480
$this->assertSame([], $this->validation->getValidated());
436481
}
437482

483+
public function testCallableRuleWithParamErrorPlaceholders(): void
484+
{
485+
$this->validation->setRules([
486+
'status' => [
487+
'label' => 'Status',
488+
'rules' => [$this->rule3(...)],
489+
],
490+
]);
491+
492+
$data = ['status' => 'invalid'];
493+
$result = $this->validation->run($data);
494+
495+
$this->assertFalse($result);
496+
$this->assertSame(
497+
['status' => 'The field Status must be one of: . Received: invalid'],
498+
$this->validation->getErrors(),
499+
);
500+
}
501+
502+
public function testRuleSetRuleWithParamErrorPlaceholders(): void
503+
{
504+
$this->validation->setRules([
505+
'status' => [
506+
'label' => 'Status',
507+
'rules' => 'custom_error_with_param[active,inactive]',
508+
],
509+
]);
510+
511+
$data = ['status' => 'invalid'];
512+
$result = $this->validation->run($data);
513+
514+
$this->assertFalse($result);
515+
$this->assertSame(
516+
['status' => 'The Status must be one of: active,inactive. Got: invalid'],
517+
$this->validation->getErrors(),
518+
);
519+
}
520+
521+
public function testClosureRuleErrorWithUnknownPlaceholderPreserved(): void
522+
{
523+
$this->validation->setRules([
524+
'status' => [
525+
'rules' => [
526+
static function ($value, $data, &$error, $field): bool {
527+
$error = 'Value {value} is invalid. See {link} for details.';
528+
529+
return false;
530+
},
531+
],
532+
],
533+
]);
534+
535+
$data = ['status' => 'bad'];
536+
$result = $this->validation->run($data);
537+
538+
$this->assertFalse($result);
539+
$this->assertSame(
540+
['status' => 'Value bad is invalid. See {link} for details.'],
541+
$this->validation->getErrors(),
542+
);
543+
}
544+
438545
public function testCallableRuleWithLabel(): void
439546
{
440547
$this->validation->setRules([

user_guide_src/source/changelogs/v4.8.0.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,11 @@ HTTP
192192
- ``Response`` and its child classes no longer require ``Config\App`` passed to their constructors.
193193
Consequently, ``CURLRequest``'s ``$config`` parameter is unused and will be removed in a future release.
194194

195+
Validation
196+
==========
197+
198+
- Custom rule methods that set an error via the ``&$error`` reference parameter now support the ``{field}``, ``{param}``, and ``{value}`` placeholders, consistent with language-file and ``setRule()``/``setRules()`` error messages.
199+
195200
Others
196201
======
197202

user_guide_src/source/libraries/validation.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,10 @@ fourth) parameter:
802802

803803
.. literalinclude:: validation/035.php
804804

805+
.. note:: Since v4.8.0, the ``{field}``, ``{param}``, and ``{value}`` placeholders are supported in ``$error``
806+
messages and will be replaced with the field's human-readable label (or field name if no label is set),
807+
the rule parameter, and the submitted value respectively.
808+
805809
Using a Custom Rule
806810
-------------------
807811

@@ -854,6 +858,10 @@ Or you can use the following parameters:
854858
.. literalinclude:: validation/041.php
855859
:lines: 2-
856860

861+
.. note:: Since v4.8.0, the ``{field}``, ``{param}``, and ``{value}`` placeholders are supported in ``$error``
862+
messages and will be replaced with the field's human-readable label (or field name if no label is set),
863+
the rule parameter, and the submitted value respectively.
864+
857865
.. _validation-using-callable-rule:
858866

859867
Using Callable Rule
@@ -877,6 +885,10 @@ Or you can use the following parameters:
877885
.. literalinclude:: validation/047.php
878886
:lines: 2-
879887

888+
.. note:: Since v4.8.0, the ``{field}``, ``{param}``, and ``{value}`` placeholders are supported in ``$error``
889+
messages and will be replaced with the field's human-readable label (or field name if no label is set),
890+
the rule parameter, and the submitted value respectively.
891+
880892
.. _validation-available-rules:
881893

882894
***************

user_guide_src/source/libraries/validation/035.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ public function even($value, ?string &$error = null): bool
66
{
77
if ((int) $value % 2 !== 0) {
88
$error = lang('myerrors.evenError');
9+
// You can also use {field}, {param}, and {value} placeholders:
10+
// $error = 'The value of {field} is not even.';
911

1012
return false;
1113
}

user_guide_src/source/libraries/validation/041.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ static function ($value, $data, &$error, $field) {
88
return true;
99
}
1010

11-
$error = 'The value is not even.';
11+
$error = 'The value of {field} is not even.';
1212

1313
return false;
1414
},

user_guide_src/source/libraries/validation/047.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public function _ruleEven($value, $data, &$error, $field): bool
1313
return true;
1414
}
1515

16-
$error = 'The value is not even.';
16+
$error = 'The value of {field} is not even.';
1717

1818
return false;
1919
}

0 commit comments

Comments
 (0)