feat(validation): Add human-readable validation error messages

This commit is contained in:
kj
2026-09-03 15:58:49 -03:00
parent f475c9125f
commit aca4e732ed
2 changed files with 135 additions and 5 deletions

View File

@@ -84,11 +84,11 @@ class Request extends Neuron
return true;
}
if (isset(static::messages()[Validator::$lastFailed])) {
$error = static::messages()[Validator::$lastFailed];
} else {
$error = 'Error: validation failed of ' . preg_replace('/\./', ' as ', Validator::$lastFailed, 1);
}
$error = Validator::message(
Validator::$lastFailed,
static::messages(),
static::attributes()
);
static::onInvalid($error);
return false;
@@ -134,6 +134,19 @@ class Request extends Neuron
return [];
}
/**
* Human-readable names for the fields.
*
* Forwarded to Validator::message() so the :attribute marker in the error
* text can be replaced with a friendlier name (e.g. "edad" => "la edad").
*
* @return array
*/
public function attributes(): array
{
return [];
}
/**
* Function to execute when an invalid value has been detected.
*

View File

@@ -52,6 +52,123 @@ class Validator
*/
private const DATA_AWARE_RULES = ['confirmed', 'required_with', 'required_if'];
/**
* Default error message template for each rule, used by message() to build
* the text for the first failing rule.
*
* Supported placeholders:
* |------------+--------------------------------------------------|
* | Marker | Replaced with |
* |------------+--------------------------------------------------|
* | :attribute | attributes[campo] if given, else the field name |
* | :min | the min/between lower bound |
* | :max | the max/between upper bound |
* | :size | the size rule value |
* | :value | the required_if expected value |
* | :other | the required_if companion field / confirmation |
* | :values | a comma list (enum, mimes, required_with) |
* |----------+----------------------------------------------------|
*
* @var array<string,string>
*/
public static array $messageTemplates = [
'not' => 'The :attribute is not valid.',
'exists' => 'The :attribute field is missing.',
'required' => 'The :attribute field is required.',
'number' => 'The :attribute must be a number.',
'int' => 'The :attribute must be an integer.',
'float' => 'The :attribute must be a float.',
'bool' => 'The :attribute must be a boolean.',
'string' => 'The :attribute must be a string.',
'array' => 'The :attribute must be an array.',
'email' => 'The :attribute must be a valid email address.',
'url' => 'The :attribute must be a valid URL.',
'enum' => 'The selected :attribute is invalid. Allowed: :values.',
'min' => 'The :attribute must be at least :min.',
'max' => 'The :attribute must not be greater than :max.',
'between' => 'The :attribute must be between :min and :max.',
'size' => 'The :attribute must be exactly :size.',
'regex' => 'The :attribute format is invalid.',
'date' => 'The :attribute is not a valid date.',
'confirmed' => 'The :attribute confirmation does not match.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_if' => 'The :attribute field is required when :other is :value.',
'nullable' => 'The :attribute field may be null.',
'file' => 'The :attribute must be a file.',
'image' => 'The :attribute must be an image.',
'mimes' => 'The :attribute must be a file of type: :values.',
];
/**
* Builds the human-readable message for a single failed validation.
*
* Pure helper: it only reads its arguments and never reads nor writes
* $lastFailed or any other state, and it is independent of Request. A
* controller may adopt it (opt-in) to render a message with :attribute in
* place of Request's default text.
*
* Resolution order for the message text:
* 1. $messages[$lastFailed] override by full "field.rule:args"
* 2. $messages["field.rule"] override by "field.rule" (no args)
* 3. $messageTemplates[rule] default for the rule
* Placeholders are then substituted in whichever text was chosen.
*
* @param string $lastFailed The "field.rule[:args]" produced by validateList().
* @param array $messages Optional per-field overrides.
* @param array $attributes Optional human field names ("field" => "Field").
*
* @return string
*/
public static function message(string $lastFailed, array $messages = [], array $attributes = []): string
{
$separator = strpos($lastFailed, '.');
if ($separator === false) {
return 'The :attribute is invalid.';
}
$target = substr($lastFailed, 0, $separator);
$ruleStr = substr($lastFailed, $separator + 1);
[$rule, $rawArguments] = static::parseRule($ruleStr);
$text = $messages[$lastFailed]
?? $messages[$target . '.' . $rule]
?? (static::$messageTemplates[$rule] ?? 'The :attribute is invalid.');
$replace = [
':attribute' => $attributes[$target] ?? $target,
];
$args = static::splitArguments($rule, $rawArguments);
switch ($rule) {
case 'min':
$replace[':min'] = $args[0] ?? '';
break;
case 'max':
$replace[':max'] = $args[0] ?? '';
break;
case 'between':
$replace[':min'] = $args[0] ?? '';
$replace[':max'] = $args[1] ?? '';
break;
case 'size':
$replace[':size'] = $args[0] ?? '';
break;
case 'enum':
case 'mimes':
case 'required_with':
$replace[':values'] = implode(', ', $args);
break;
case 'required_if':
$replace[':other'] = $args[0] ?? '';
$replace[':value'] = $args[1] ?? '';
break;
}
return strtr($text, $replace);
}
/**
* Validates a list of rules against the properties of an object.
*