Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 54 additions & 15 deletions system/Validation/Validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -369,14 +369,16 @@ protected function processRules(
$fieldForErrors = ($rule === 'field_exists') ? $originalField : $field;

// @phpstan-ignore-next-line $error may be set by rule methods.
$this->errors[$fieldForErrors] = $error ?? $this->getErrorMessage(
($this->isClosure($rule) || $arrayCallable) ? (string) $i : $rule,
$field,
$label,
$param,
(string) $value,
$originalField,
);
$this->errors[$fieldForErrors] = $error !== null
? $this->parseErrorMessage($error, $field, $label, $param, (string) $value)
: $this->getErrorMessage(
($this->isClosure($rule) || $arrayCallable) ? (string) $i : $rule,
$field,
$label,
$param,
(string) $value,
$originalField,
);

return false;
}
Expand Down Expand Up @@ -933,13 +935,7 @@ protected function getErrorMessage(
?string $value = null,
?string $originalField = null,
): string {
$param ??= '';

$args = [
'field' => ($label === null || $label === '') ? $field : lang($label),
'param' => isset($this->rules[$param]['label']) ? lang($this->rules[$param]['label']) : $param,
'value' => $value ?? '',
];
$args = $this->buildErrorArgs($field, $label, $param, $value);

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

/**
* Substitutes {field}, {param}, and {value} placeholders in an error message
* set directly by a rule method via the $error reference parameter.
*
* Uses simple string replacement rather than lang() to avoid ICU MessageFormatter
* warnings on unrecognised patterns and to leave any other {xyz} content untouched.
*/
private function parseErrorMessage(
string $message,
string $field,
?string $label = null,
?string $param = null,
?string $value = null,
): string {
$args = $this->buildErrorArgs($field, $label, $param, $value);

return str_replace(
['{field}', '{param}', '{value}'],
[$args['field'], $args['param'], $args['value']],
$message,
);
}

/**
* Builds the placeholder arguments array used for error message substitution.
*
* @return array{field: string, param: string, value: string}
*/
private function buildErrorArgs(
string $field,
?string $label = null,
?string $param = null,
?string $value = null,
): array {
$param ??= '';

return [
'field' => ($label === null || $label === '') ? $field : lang($label),
'param' => isset($this->rules[$param]['label']) ? lang($this->rules[$param]['label']) : $param,
'value' => $value ?? '',
];
}

/**
* Split rules string by pipe operator.
*/
Expand Down
10 changes: 10 additions & 0 deletions tests/_support/Validation/TestRules.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ public function customError(string $str, ?string &$error = null)
return false;
}

/**
* @param-out string $error
*/
public function custom_error_with_param(mixed $str, string $param, array $data, ?string &$error = null, string $field = ''): bool
{
$error = 'The {field} must be one of: {param}. Got: {value}';

return false;
}

public function check_object_rule(object $value, ?string $fields, array $data = [])
{
$find = false;
Expand Down
107 changes: 107 additions & 0 deletions tests/system/Validation/ValidationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,35 @@ static function ($value, $data, &$error, $field): bool {
$this->assertSame([], $this->validation->getValidated());
}

public function testClosureRuleWithParamErrorPlaceholders(): void
{
$this->validation->setRules([
'status' => [
'label' => 'Status',
'rules' => [
static function ($value, $data, &$error, $field): bool {
if ($value !== 'active') {
$error = 'The field {field} must be one of: {param}. Received: {value}';

return false;
}

return true;
},
],
],
]);

$data = ['status' => 'invalid'];
$result = $this->validation->run($data);

$this->assertFalse($result);
$this->assertSame(
['status' => 'The field Status must be one of: . Received: invalid'],
$this->validation->getErrors(),
);
}

public function testClosureRuleWithLabel(): void
{
$this->validation->setRules([
Expand Down Expand Up @@ -415,6 +444,22 @@ public function rule2(mixed $value, array $data, ?string &$error, string $field)
return true;
}

/**
* Validation rule3
*
* @param array<string, mixed> $data
*/
public function rule3(mixed $value, array $data, ?string &$error, string $field): bool
{
if ($value !== 'active') {
$error = 'The field {field} must be one of: {param}. Received: {value}';

return false;
}

return true;
}

public function testCallableRuleWithParamError(): void
{
$this->validation->setRules([
Expand All @@ -435,6 +480,68 @@ public function testCallableRuleWithParamError(): void
$this->assertSame([], $this->validation->getValidated());
}

public function testCallableRuleWithParamErrorPlaceholders(): void
{
$this->validation->setRules([
'status' => [
'label' => 'Status',
'rules' => [$this->rule3(...)],
],
]);

$data = ['status' => 'invalid'];
$result = $this->validation->run($data);

$this->assertFalse($result);
$this->assertSame(
['status' => 'The field Status must be one of: . Received: invalid'],
$this->validation->getErrors(),
);
}

public function testRuleSetRuleWithParamErrorPlaceholders(): void
{
$this->validation->setRules([
'status' => [
'label' => 'Status',
'rules' => 'custom_error_with_param[active,inactive]',
],
]);

$data = ['status' => 'invalid'];
$result = $this->validation->run($data);

$this->assertFalse($result);
$this->assertSame(
['status' => 'The Status must be one of: active,inactive. Got: invalid'],
$this->validation->getErrors(),
);
}

public function testClosureRuleErrorWithUnknownPlaceholderPreserved(): void
{
$this->validation->setRules([
'status' => [
'rules' => [
static function ($value, $data, &$error, $field): bool {
$error = 'Value {value} is invalid. See {link} for details.';

return false;
},
],
],
]);

$data = ['status' => 'bad'];
$result = $this->validation->run($data);

$this->assertFalse($result);
$this->assertSame(
['status' => 'Value bad is invalid. See {link} for details.'],
$this->validation->getErrors(),
);
}

public function testCallableRuleWithLabel(): void
{
$this->validation->setRules([
Expand Down
5 changes: 5 additions & 0 deletions user_guide_src/source/changelogs/v4.8.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ HTTP
- ``Response`` and its child classes no longer require ``Config\App`` passed to their constructors.
Consequently, ``CURLRequest``'s ``$config`` parameter is unused and will be removed in a future release.

Validation
==========

- 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.

Others
======

Expand Down
12 changes: 12 additions & 0 deletions user_guide_src/source/libraries/validation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -802,6 +802,10 @@ fourth) parameter:

.. literalinclude:: validation/035.php

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

Using a Custom Rule
-------------------

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

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

.. _validation-using-callable-rule:

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

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

.. _validation-available-rules:

***************
Expand Down
2 changes: 2 additions & 0 deletions user_guide_src/source/libraries/validation/035.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public function even($value, ?string &$error = null): bool
{
if ((int) $value % 2 !== 0) {
$error = lang('myerrors.evenError');
// You can also use {field}, {param}, and {value} placeholders:
// $error = 'The value of {field} is not even.';

return false;
}
Expand Down
2 changes: 1 addition & 1 deletion user_guide_src/source/libraries/validation/041.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ static function ($value, $data, &$error, $field) {
return true;
}

$error = 'The value is not even.';
$error = 'The value of {field} is not even.';

return false;
},
Expand Down
2 changes: 1 addition & 1 deletion user_guide_src/source/libraries/validation/047.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function _ruleEven($value, $data, &$error, $field): bool
return true;
}

$error = 'The value is not even.';
$error = 'The value of {field} is not even.';

return false;
}
Expand Down
Loading