From aca4e732ed8cfe7f356c9301f14db7c1704d02c0 Mon Sep 17 00:00:00 2001 From: kj Date: Thu, 3 Sep 2026 15:58:49 -0300 Subject: [PATCH] feat(validation): Add human-readable validation error messages --- src/Libs/Request.php | 23 ++++++-- src/Libs/Validator.php | 117 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/src/Libs/Request.php b/src/Libs/Request.php index a25e9d0..99bff58 100644 --- a/src/Libs/Request.php +++ b/src/Libs/Request.php @@ -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. * diff --git a/src/Libs/Validator.php b/src/Libs/Validator.php index 4321c1e..d5a66d1 100644 --- a/src/Libs/Validator.php +++ b/src/Libs/Validator.php @@ -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 + */ + 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. *