diff --git a/src/Validation/Contracts/Rule.php b/src/Validation/Contracts/Rule.php index ef90281e..f52a7761 100644 --- a/src/Validation/Contracts/Rule.php +++ b/src/Validation/Contracts/Rule.php @@ -13,4 +13,6 @@ public function setField(string $field): self; public function setData(Dot|array $data): self; public function passes(): bool; + + public function message(): string|null; } diff --git a/src/Validation/Rules/Between.php b/src/Validation/Rules/Between.php index 3cbfbc5a..567141ee 100644 --- a/src/Validation/Rules/Between.php +++ b/src/Validation/Rules/Between.php @@ -21,4 +21,23 @@ public function passes(): bool return $value >= $this->min && $value <= $this->max; } + + public function message(): string|null + { + $value = $this->data->get($this->field) ?? null; + $type = gettype($value); + + $key = match ($type) { + 'string' => 'validation.between.string', + 'array' => 'validation.between.array', + 'object' => 'validation.between.file', + default => 'validation.between.numeric', + }; + + return trans($key, [ + 'field' => $this->getFieldForHumans(), + 'min' => $this->min, + 'max' => $this->max, + ]); + } } diff --git a/src/Validation/Rules/Confirmed.php b/src/Validation/Rules/Confirmed.php index 99455a9d..a1000b07 100644 --- a/src/Validation/Rules/Confirmed.php +++ b/src/Validation/Rules/Confirmed.php @@ -20,4 +20,12 @@ public function passes(): bool && $confirmation !== null && $original === $confirmation; } + + public function message(): string|null + { + return trans('validation.confirmed', [ + 'field' => $this->getFieldForHumans(), + 'other' => $this->confirmationField, + ]); + } } diff --git a/src/Validation/Rules/Dates/After.php b/src/Validation/Rules/Dates/After.php index 24270a43..7297a35a 100644 --- a/src/Validation/Rules/Dates/After.php +++ b/src/Validation/Rules/Dates/After.php @@ -12,4 +12,9 @@ public function passes(): bool { return Date::parse($this->getValue())->greaterThan($this->date); } + + public function message(): string|null + { + return trans('validation.date.after', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Dates/AfterOrEqual.php b/src/Validation/Rules/Dates/AfterOrEqual.php index 5fb598e2..6ba3b263 100644 --- a/src/Validation/Rules/Dates/AfterOrEqual.php +++ b/src/Validation/Rules/Dates/AfterOrEqual.php @@ -12,4 +12,9 @@ public function passes(): bool { return Date::parse($this->getValue())->greaterThanOrEqualTo($this->date); } + + public function message(): string|null + { + return trans('validation.date.after_or_equal', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Dates/AfterOrEqualTo.php b/src/Validation/Rules/Dates/AfterOrEqualTo.php index d8b4f182..7b047d37 100644 --- a/src/Validation/Rules/Dates/AfterOrEqualTo.php +++ b/src/Validation/Rules/Dates/AfterOrEqualTo.php @@ -15,4 +15,12 @@ public function passes(): bool return Date::parse($date)->greaterThanOrEqualTo($relatedDate); } + + public function message(): string|null + { + return trans('validation.date.after_or_equal_to', [ + 'field' => $this->getFieldForHumans(), + 'other' => $this->relatedField, + ]); + } } diff --git a/src/Validation/Rules/Dates/AfterTo.php b/src/Validation/Rules/Dates/AfterTo.php index 927bedf6..a6643dbf 100644 --- a/src/Validation/Rules/Dates/AfterTo.php +++ b/src/Validation/Rules/Dates/AfterTo.php @@ -15,4 +15,12 @@ public function passes(): bool return Date::parse($date)->greaterThan($relatedDate); } + + public function message(): string|null + { + return trans('validation.date.after_to', [ + 'field' => $this->getFieldForHumans(), + 'other' => $this->relatedField, + ]); + } } diff --git a/src/Validation/Rules/Dates/Before.php b/src/Validation/Rules/Dates/Before.php index 9449ab85..7069450f 100644 --- a/src/Validation/Rules/Dates/Before.php +++ b/src/Validation/Rules/Dates/Before.php @@ -12,4 +12,9 @@ public function passes(): bool { return Date::parse($this->getValue())->lessThan($this->date); } + + public function message(): string|null + { + return trans('validation.date.before', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Dates/BeforeOrEqual.php b/src/Validation/Rules/Dates/BeforeOrEqual.php index 489fb328..31586bb9 100644 --- a/src/Validation/Rules/Dates/BeforeOrEqual.php +++ b/src/Validation/Rules/Dates/BeforeOrEqual.php @@ -12,4 +12,9 @@ public function passes(): bool { return Date::parse($this->getValue())->lessThanOrEqualTo($this->date); } + + public function message(): string|null + { + return trans('validation.date.before_or_equal', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Dates/BeforeOrEqualTo.php b/src/Validation/Rules/Dates/BeforeOrEqualTo.php index 4267155d..9ec91206 100644 --- a/src/Validation/Rules/Dates/BeforeOrEqualTo.php +++ b/src/Validation/Rules/Dates/BeforeOrEqualTo.php @@ -15,4 +15,12 @@ public function passes(): bool return Date::parse($date)->lessThanOrEqualTo($relatedDate); } + + public function message(): string|null + { + return trans('validation.date.before_or_equal_to', [ + 'field' => $this->getFieldForHumans(), + 'other' => $this->relatedField, + ]); + } } diff --git a/src/Validation/Rules/Dates/BeforeTo.php b/src/Validation/Rules/Dates/BeforeTo.php index b00d2710..aea0b0f7 100644 --- a/src/Validation/Rules/Dates/BeforeTo.php +++ b/src/Validation/Rules/Dates/BeforeTo.php @@ -15,4 +15,12 @@ public function passes(): bool return Date::parse($date)->lessThan($relatedDate); } + + public function message(): string|null + { + return trans('validation.date.before_to', [ + 'field' => $this->getFieldForHumans(), + 'other' => $this->relatedField, + ]); + } } diff --git a/src/Validation/Rules/Dates/Equal.php b/src/Validation/Rules/Dates/Equal.php index 3ee43b0f..20625d7a 100644 --- a/src/Validation/Rules/Dates/Equal.php +++ b/src/Validation/Rules/Dates/Equal.php @@ -21,4 +21,9 @@ public function passes(): bool { return Date::parse($this->getValue())->equalTo($this->date); } + + public function message(): string|null + { + return trans('validation.date.equal', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Dates/EqualTo.php b/src/Validation/Rules/Dates/EqualTo.php index ca496c16..b5792bdf 100644 --- a/src/Validation/Rules/Dates/EqualTo.php +++ b/src/Validation/Rules/Dates/EqualTo.php @@ -15,4 +15,12 @@ public function passes(): bool return $date->equalTo($relatedDate); } + + public function message(): string|null + { + return trans('validation.date.equal_to', [ + 'field' => $this->getFieldForHumans(), + 'other' => $this->relatedField, + ]); + } } diff --git a/src/Validation/Rules/Dates/Format.php b/src/Validation/Rules/Dates/Format.php index 45bd3593..e639996c 100644 --- a/src/Validation/Rules/Dates/Format.php +++ b/src/Validation/Rules/Dates/Format.php @@ -20,4 +20,9 @@ public function passes(): bool return $dateTime instanceof DateTime; } + + public function message(): string|null + { + return trans('validation.date.format', ['field' => $this->getFieldForHumans(), 'format' => $this->format]); + } } diff --git a/src/Validation/Rules/Dates/IsDate.php b/src/Validation/Rules/Dates/IsDate.php index 54590677..69528103 100644 --- a/src/Validation/Rules/Dates/IsDate.php +++ b/src/Validation/Rules/Dates/IsDate.php @@ -27,4 +27,9 @@ public function passes(): bool } } + + public function message(): string|null + { + return trans('validation.date.is_date', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/DoesNotEndWith.php b/src/Validation/Rules/DoesNotEndWith.php index cbe194a4..cc6d1ed6 100644 --- a/src/Validation/Rules/DoesNotEndWith.php +++ b/src/Validation/Rules/DoesNotEndWith.php @@ -10,4 +10,12 @@ public function passes(): bool { return ! parent::passes(); } + + public function message(): string|null + { + return trans('validation.does_not_end_with', [ + 'field' => $this->getFieldForHumans(), + 'values' => $this->needle, + ]); + } } diff --git a/src/Validation/Rules/DoesNotStartWith.php b/src/Validation/Rules/DoesNotStartWith.php index fd605ebc..7328c1c4 100644 --- a/src/Validation/Rules/DoesNotStartWith.php +++ b/src/Validation/Rules/DoesNotStartWith.php @@ -10,4 +10,12 @@ public function passes(): bool { return ! parent::passes(); } + + public function message(): string|null + { + return trans('validation.does_not_start_with', [ + 'field' => $this->getFieldForHumans(), + 'values' => $this->needle, + ]); + } } diff --git a/src/Validation/Rules/EndsWith.php b/src/Validation/Rules/EndsWith.php index f94a2ad3..cc0c851d 100644 --- a/src/Validation/Rules/EndsWith.php +++ b/src/Validation/Rules/EndsWith.php @@ -10,4 +10,12 @@ public function passes(): bool { return str_ends_with($this->getValue(), $this->needle); } + + public function message(): string|null + { + return trans('validation.ends_with', [ + 'field' => $this->getFieldForHumans(), + 'values' => $this->needle, + ]); + } } diff --git a/src/Validation/Rules/Exists.php b/src/Validation/Rules/Exists.php index 2f081467..a06147de 100644 --- a/src/Validation/Rules/Exists.php +++ b/src/Validation/Rules/Exists.php @@ -20,4 +20,11 @@ public function passes(): bool ->whereEqual($this->column ?? $this->field, $this->getValue()) ->exists(); } + + public function message(): string|null + { + return trans('validation.exists', [ + 'field' => $this->getFieldForHumans(), + ]); + } } diff --git a/src/Validation/Rules/In.php b/src/Validation/Rules/In.php index a268a854..b222795a 100644 --- a/src/Validation/Rules/In.php +++ b/src/Validation/Rules/In.php @@ -17,4 +17,12 @@ public function passes(): bool { return in_array($this->getValue(), $this->haystack, true); } + + public function message(): string|null + { + return trans('validation.in', [ + 'field' => $this->getFieldForHumans(), + 'values' => implode(', ', $this->haystack), + ]); + } } diff --git a/src/Validation/Rules/IsArray.php b/src/Validation/Rules/IsArray.php index be7353e7..016e634e 100644 --- a/src/Validation/Rules/IsArray.php +++ b/src/Validation/Rules/IsArray.php @@ -12,4 +12,9 @@ public function passes(): bool { return is_array($this->getValue()); } + + public function message(): string|null + { + return trans('validation.array', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/IsBool.php b/src/Validation/Rules/IsBool.php index 71752a74..2e5b601d 100644 --- a/src/Validation/Rules/IsBool.php +++ b/src/Validation/Rules/IsBool.php @@ -12,4 +12,9 @@ public function passes(): bool { return in_array($this->getValue(), [true, false, 'true', 'false', 1, 0, '1', '0'], true); } + + public function message(): string|null + { + return trans('validation.boolean', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/IsCollection.php b/src/Validation/Rules/IsCollection.php index 7e3363c7..f1e6bdab 100644 --- a/src/Validation/Rules/IsCollection.php +++ b/src/Validation/Rules/IsCollection.php @@ -17,4 +17,9 @@ public function passes(): bool && array_is_list($value) && ! $this->isScalar($value); } + + public function message(): string|null + { + return trans('validation.collection', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/IsDictionary.php b/src/Validation/Rules/IsDictionary.php index f7b26395..2b7edab9 100644 --- a/src/Validation/Rules/IsDictionary.php +++ b/src/Validation/Rules/IsDictionary.php @@ -17,4 +17,9 @@ public function passes(): bool && ! array_is_list($value) && $this->isScalar($value); } + + public function message(): string|null + { + return trans('validation.dictionary', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/IsEmail.php b/src/Validation/Rules/IsEmail.php index 3f8a1d65..0f35aaf3 100644 --- a/src/Validation/Rules/IsEmail.php +++ b/src/Validation/Rules/IsEmail.php @@ -35,4 +35,9 @@ public function pusValidation(EmailValidation $emailValidation): self return $this; } + + public function message(): string|null + { + return trans('validation.email', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/IsFile.php b/src/Validation/Rules/IsFile.php index 301c576e..06466f6b 100644 --- a/src/Validation/Rules/IsFile.php +++ b/src/Validation/Rules/IsFile.php @@ -14,4 +14,9 @@ public function passes(): bool return $value instanceof BufferedFile; } + + public function message(): string|null + { + return trans('validation.file', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/IsList.php b/src/Validation/Rules/IsList.php index 25b5ecc7..5b21294e 100644 --- a/src/Validation/Rules/IsList.php +++ b/src/Validation/Rules/IsList.php @@ -28,4 +28,9 @@ protected function isScalar(array $data): bool return true; } + + public function message(): string|null + { + return trans('validation.list', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/IsString.php b/src/Validation/Rules/IsString.php index e462505b..7e960169 100644 --- a/src/Validation/Rules/IsString.php +++ b/src/Validation/Rules/IsString.php @@ -12,4 +12,9 @@ public function passes(): bool { return is_string($this->getValue()); } + + public function message(): string|null + { + return trans('validation.string', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/IsUrl.php b/src/Validation/Rules/IsUrl.php index 14948010..d45d4ec4 100644 --- a/src/Validation/Rules/IsUrl.php +++ b/src/Validation/Rules/IsUrl.php @@ -11,4 +11,9 @@ public function passes(): bool return parent::passes() && filter_var($this->getValue(), FILTER_VALIDATE_URL) !== false; } + + public function message(): string|null + { + return trans('validation.url', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Max.php b/src/Validation/Rules/Max.php index 63a4ab98..43ad759f 100644 --- a/src/Validation/Rules/Max.php +++ b/src/Validation/Rules/Max.php @@ -10,4 +10,22 @@ public function passes(): bool { return $this->getValue() <= $this->limit; } + + public function message(): string|null + { + $value = $this->data->get($this->field) ?? null; + $type = gettype($value); + + $key = match ($type) { + 'string' => 'validation.max.string', + 'array' => 'validation.max.array', + 'object' => 'validation.max.file', + default => 'validation.max.numeric', + }; + + return trans($key, [ + 'field' => $this->getFieldForHumans(), + 'max' => $this->limit, + ]); + } } diff --git a/src/Validation/Rules/Mimes.php b/src/Validation/Rules/Mimes.php index 311c7ede..51d2c4f9 100644 --- a/src/Validation/Rules/Mimes.php +++ b/src/Validation/Rules/Mimes.php @@ -17,4 +17,12 @@ public function passes(): bool { return in_array($this->getValue()->getMimeType(), $this->haystack, true); } + + public function message(): string|null + { + return trans('validation.mimes', [ + 'field' => $this->getFieldForHumans(), + 'values' => implode(', ', $this->haystack), + ]); + } } diff --git a/src/Validation/Rules/Min.php b/src/Validation/Rules/Min.php index 94b45e86..b55c6033 100644 --- a/src/Validation/Rules/Min.php +++ b/src/Validation/Rules/Min.php @@ -10,4 +10,22 @@ public function passes(): bool { return $this->getValue() >= $this->limit; } + + public function message(): string|null + { + $value = $this->data->get($this->field) ?? null; + $type = gettype($value); + + $key = match ($type) { + 'string' => 'validation.min.string', + 'array' => 'validation.min.array', + 'object' => 'validation.min.file', + default => 'validation.min.numeric', + }; + + return trans($key, [ + 'field' => $this->getFieldForHumans(), + 'min' => $this->limit, + ]); + } } diff --git a/src/Validation/Rules/NotIn.php b/src/Validation/Rules/NotIn.php index 14968f7d..9035cb2f 100644 --- a/src/Validation/Rules/NotIn.php +++ b/src/Validation/Rules/NotIn.php @@ -10,4 +10,12 @@ public function passes(): bool { return ! parent::passes(); } + + public function message(): string|null + { + return trans('validation.not_in', [ + 'field' => $this->getFieldForHumans(), + 'values' => implode(', ', $this->haystack), + ]); + } } diff --git a/src/Validation/Rules/Nullable.php b/src/Validation/Rules/Nullable.php index 1955939c..982f183b 100644 --- a/src/Validation/Rules/Nullable.php +++ b/src/Validation/Rules/Nullable.php @@ -25,4 +25,10 @@ public function skip(): bool { return is_null($this->getValue()); } + + public function message(): string|null + { + // Nullable itself doesn't produce an error message; defer to Required if fails + return null; + } } diff --git a/src/Validation/Rules/Numbers/Digits.php b/src/Validation/Rules/Numbers/Digits.php index f270c919..a36f9a10 100644 --- a/src/Validation/Rules/Numbers/Digits.php +++ b/src/Validation/Rules/Numbers/Digits.php @@ -21,4 +21,12 @@ public function passes(): bool return strlen($digits) === $this->digits; } + + public function message(): string|null + { + return trans('validation.digits', [ + 'field' => $this->getFieldForHumans(), + 'digits' => $this->digits, + ]); + } } diff --git a/src/Validation/Rules/Numbers/DigitsBetween.php b/src/Validation/Rules/Numbers/DigitsBetween.php index 71569189..a1b6bd65 100644 --- a/src/Validation/Rules/Numbers/DigitsBetween.php +++ b/src/Validation/Rules/Numbers/DigitsBetween.php @@ -19,4 +19,13 @@ public function passes(): bool return $digits >= $this->min && $digits <= $this->max; } + + public function message(): string|null + { + return trans('validation.digits_between', [ + 'field' => $this->getFieldForHumans(), + 'min' => $this->min, + 'max' => $this->max, + ]); + } } diff --git a/src/Validation/Rules/Numbers/IsFloat.php b/src/Validation/Rules/Numbers/IsFloat.php index bf64491e..a0ea4033 100644 --- a/src/Validation/Rules/Numbers/IsFloat.php +++ b/src/Validation/Rules/Numbers/IsFloat.php @@ -14,4 +14,9 @@ public function passes(): bool { return is_float($this->getValue()); } + + public function message(): string|null + { + return trans('validation.float', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Numbers/IsInteger.php b/src/Validation/Rules/Numbers/IsInteger.php index ace93227..a5ad9cb1 100644 --- a/src/Validation/Rules/Numbers/IsInteger.php +++ b/src/Validation/Rules/Numbers/IsInteger.php @@ -14,4 +14,9 @@ public function passes(): bool { return is_integer($this->getValue()); } + + public function message(): string|null + { + return trans('validation.integer', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Numbers/IsNumeric.php b/src/Validation/Rules/Numbers/IsNumeric.php index 6950e9cd..718e7a25 100644 --- a/src/Validation/Rules/Numbers/IsNumeric.php +++ b/src/Validation/Rules/Numbers/IsNumeric.php @@ -14,4 +14,9 @@ public function passes(): bool { return is_numeric($this->getValue()); } + + public function message(): string|null + { + return trans('validation.numeric', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Optional.php b/src/Validation/Rules/Optional.php index 1cfb7383..572bac12 100644 --- a/src/Validation/Rules/Optional.php +++ b/src/Validation/Rules/Optional.php @@ -19,4 +19,9 @@ public function skip(): bool { return ! $this->data->has($this->field); } + + public function message(): string|null + { + return null; // Optional never triggers its own message + } } diff --git a/src/Validation/Rules/RegEx.php b/src/Validation/Rules/RegEx.php index 9b2c3a90..c1dbb59d 100644 --- a/src/Validation/Rules/RegEx.php +++ b/src/Validation/Rules/RegEx.php @@ -15,4 +15,11 @@ public function passes(): bool { return preg_match($this->regEx, $this->getValue()) > 0; } + + public function message(): string|null + { + return trans('validation.regex', [ + 'field' => $this->getFieldForHumans(), + ]); + } } diff --git a/src/Validation/Rules/Required.php b/src/Validation/Rules/Required.php index baa1cdaf..3e32bd9c 100644 --- a/src/Validation/Rules/Required.php +++ b/src/Validation/Rules/Required.php @@ -32,4 +32,9 @@ public function skip(): bool { return false; } + + public function message(): string|null + { + return trans('validation.required', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Rule.php b/src/Validation/Rules/Rule.php index 179c63f8..2898b397 100644 --- a/src/Validation/Rules/Rule.php +++ b/src/Validation/Rules/Rule.php @@ -6,6 +6,7 @@ use Adbar\Dot; use Amp\Http\Server\FormParser\BufferedFile; +use Phenix\Facades\Translator; use Phenix\Validation\Contracts\Rule as RuleContract; use function is_array; @@ -14,6 +15,7 @@ abstract class Rule implements RuleContract { protected string $field; + protected Dot $data; public function __construct(array|null $data = null) @@ -51,4 +53,13 @@ protected function getValueType(): string { return gettype($this->data->get($this->field) ?? null); } + + protected function getFieldForHumans(): string + { + if (Translator::has("validation.fields.{$this->field}")) { + return Translator::get("validation.fields.{$this->field}"); + } + + return $this->field; + } } diff --git a/src/Validation/Rules/Size.php b/src/Validation/Rules/Size.php index 4420ab5f..5d48ba62 100644 --- a/src/Validation/Rules/Size.php +++ b/src/Validation/Rules/Size.php @@ -48,4 +48,22 @@ private function resolveCountableObject(object $value): float|int return $count; } + + public function message(): string|null + { + $value = $this->data->get($this->field) ?? null; + $type = gettype($value); + + $key = match ($type) { + 'string' => 'validation.size.string', + 'array' => 'validation.size.array', + 'object' => 'validation.size.file', // treat countable / file objects as file + default => 'validation.size.numeric', + }; + + return trans($key, [ + 'field' => $this->getFieldForHumans(), + 'size' => $this->limit, + ]); + } } diff --git a/src/Validation/Rules/StartsWith.php b/src/Validation/Rules/StartsWith.php index d3e9d5b4..8864e118 100644 --- a/src/Validation/Rules/StartsWith.php +++ b/src/Validation/Rules/StartsWith.php @@ -15,4 +15,12 @@ public function passes(): bool { return str_starts_with($this->getValue(), $this->needle); } + + public function message(): string|null + { + return trans('validation.starts_with', [ + 'field' => $this->getFieldForHumans(), + 'values' => $this->needle, + ]); + } } diff --git a/src/Validation/Rules/Ulid.php b/src/Validation/Rules/Ulid.php index f2cca00a..846338a2 100644 --- a/src/Validation/Rules/Ulid.php +++ b/src/Validation/Rules/Ulid.php @@ -13,4 +13,9 @@ public function passes(): bool return Str::isUlid($this->getValue()); } + + public function message(): string|null + { + return trans('validation.ulid', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Rules/Unique.php b/src/Validation/Rules/Unique.php index 344cc1f7..ae6de253 100644 --- a/src/Validation/Rules/Unique.php +++ b/src/Validation/Rules/Unique.php @@ -12,4 +12,11 @@ public function passes(): bool ->whereEqual($this->column ?? $this->field, $this->getValue()) ->count() === 0; } + + public function message(): string|null + { + return trans('validation.unique', [ + 'field' => $this->getFieldForHumans(), + ]); + } } diff --git a/src/Validation/Rules/Uuid.php b/src/Validation/Rules/Uuid.php index b8fe4733..67adc216 100644 --- a/src/Validation/Rules/Uuid.php +++ b/src/Validation/Rules/Uuid.php @@ -12,4 +12,9 @@ public function passes(): bool { return Str::isUuid($this->getValue()); } + + public function message(): string|null + { + return trans('validation.uuid', ['field' => $this->getFieldForHumans()]); + } } diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index 2173eb36..c0c16482 100755 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -177,7 +177,7 @@ protected function checkRule(string $field, Rule $rule, string|int|null $parent $passes = $rule->passes(); if (! $passes) { - $this->failing[$field][] = $rule::class; + $this->failing[$field][] = $rule->message(); } $this->validated[] = $field; diff --git a/tests/Unit/Validation/Rules/BetweenTest.php b/tests/Unit/Validation/Rules/BetweenTest.php new file mode 100644 index 00000000..e2ed15f5 --- /dev/null +++ b/tests/Unit/Validation/Rules/BetweenTest.php @@ -0,0 +1,20 @@ +setField('items')->setData(['items' => ['a','b','c','d','e']]); + + assertFalse($rule->passes()); + assertStringContainsString('between 2 and 4 items', (string) $rule->message()); +}); + +it('passes between for array inside range', function () { + $rule = new Between(2, 4); + $rule->setField('items')->setData(['items' => ['a','b','c']]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/ConfirmedTest.php b/tests/Unit/Validation/Rules/ConfirmedTest.php new file mode 100644 index 00000000..2adb8c6f --- /dev/null +++ b/tests/Unit/Validation/Rules/ConfirmedTest.php @@ -0,0 +1,16 @@ +setField('password')->setData([ + 'password' => 'secret1', + 'password_confirmation' => 'secret2', + ]); + + assertFalse($rule->passes()); + assertStringContainsString('does not match', (string) $rule->message()); +}); diff --git a/tests/Unit/Validation/Rules/DateAfterOrEqualTest.php b/tests/Unit/Validation/Rules/DateAfterOrEqualTest.php new file mode 100644 index 00000000..754c3dd6 --- /dev/null +++ b/tests/Unit/Validation/Rules/DateAfterOrEqualTest.php @@ -0,0 +1,27 @@ +setField('date')->setData(['date' => '2023-12-31']); + + assertFalse($rule->passes()); + assertStringContainsString('The date must be a date after or equal to the specified date.', (string) $rule->message()); +}); + +it('passes when date is equal', function () { + $rule = new AfterOrEqual('2024-01-01'); + $rule->setField('date')->setData(['date' => '2024-01-01']); + + assertTrue($rule->passes()); +}); + +it('passes when date is after', function () { + $rule = new AfterOrEqual('2024-01-01'); + $rule->setField('date')->setData(['date' => '2024-01-02']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DateAfterOrEqualToTest.php b/tests/Unit/Validation/Rules/DateAfterOrEqualToTest.php new file mode 100644 index 00000000..ffcafbea --- /dev/null +++ b/tests/Unit/Validation/Rules/DateAfterOrEqualToTest.php @@ -0,0 +1,36 @@ +setField('start_date')->setData([ + 'start_date' => '2024-01-01', + 'end_date' => '2024-01-02', + ]); + + assertFalse($rule->passes()); + assertStringContainsString('must be a date after or equal to end_date', (string) $rule->message()); +}); + +it('passes when date is equal to related date', function () { + $rule = new AfterOrEqualTo('end_date'); + $rule->setField('start_date')->setData([ + 'start_date' => '2024-01-02', + 'end_date' => '2024-01-02', + ]); + + assertTrue($rule->passes()); +}); + +it('passes when date is after related date', function () { + $rule = new AfterOrEqualTo('end_date'); + $rule->setField('start_date')->setData([ + 'start_date' => '2024-01-03', + 'end_date' => '2024-01-02', + ]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DateAfterTest.php b/tests/Unit/Validation/Rules/DateAfterTest.php new file mode 100644 index 00000000..91db8043 --- /dev/null +++ b/tests/Unit/Validation/Rules/DateAfterTest.php @@ -0,0 +1,13 @@ +setField('date')->setData(['date' => '2023-12-31']); + + assertFalse($rule->passes()); + assertStringContainsString('must be a date after', (string) $rule->message()); +}); diff --git a/tests/Unit/Validation/Rules/DateAfterToTest.php b/tests/Unit/Validation/Rules/DateAfterToTest.php new file mode 100644 index 00000000..f8aaff5f --- /dev/null +++ b/tests/Unit/Validation/Rules/DateAfterToTest.php @@ -0,0 +1,26 @@ +setField('start_date')->setData([ + 'start_date' => '2024-01-02', + 'end_date' => '2024-01-02', + ]); + + assertFalse($rule->passes()); + assertStringContainsString('must be a date after end_date', (string) $rule->message()); +}); + +it('passes when date is after related date', function () { + $rule = new AfterTo('end_date'); + $rule->setField('start_date')->setData([ + 'start_date' => '2024-01-03', + 'end_date' => '2024-01-02', + ]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DateBeforeOrEqualTest.php b/tests/Unit/Validation/Rules/DateBeforeOrEqualTest.php new file mode 100644 index 00000000..ea4310cc --- /dev/null +++ b/tests/Unit/Validation/Rules/DateBeforeOrEqualTest.php @@ -0,0 +1,27 @@ +setField('date')->setData(['date' => '2024-01-02']); + + assertFalse($rule->passes()); + assertStringContainsString('The date must be a date before or equal to the specified date.', (string) $rule->message()); +}); + +it('passes when date is equal to given date', function () { + $rule = new BeforeOrEqual('2024-01-01'); + $rule->setField('date')->setData(['date' => '2024-01-01']); + + assertTrue($rule->passes()); +}); + +it('passes when date is before given date', function () { + $rule = new BeforeOrEqual('2024-01-01'); + $rule->setField('date')->setData(['date' => '2023-12-31']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DateBeforeOrEqualToTest.php b/tests/Unit/Validation/Rules/DateBeforeOrEqualToTest.php new file mode 100644 index 00000000..a63c20fa --- /dev/null +++ b/tests/Unit/Validation/Rules/DateBeforeOrEqualToTest.php @@ -0,0 +1,36 @@ +setField('start_date')->setData([ + 'start_date' => '2024-01-03', + 'end_date' => '2024-01-02', + ]); + + assertFalse($rule->passes()); + assertStringContainsString('must be a date before or equal to end_date', (string) $rule->message()); +}); + +it('passes when date is equal to related date', function () { + $rule = new BeforeOrEqualTo('end_date'); + $rule->setField('start_date')->setData([ + 'start_date' => '2024-01-02', + 'end_date' => '2024-01-02', + ]); + + assertTrue($rule->passes()); +}); + +it('passes when date is before related date', function () { + $rule = new BeforeOrEqualTo('end_date'); + $rule->setField('start_date')->setData([ + 'start_date' => '2024-01-01', + 'end_date' => '2024-01-02', + ]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DateBeforeTest.php b/tests/Unit/Validation/Rules/DateBeforeTest.php new file mode 100644 index 00000000..717ca276 --- /dev/null +++ b/tests/Unit/Validation/Rules/DateBeforeTest.php @@ -0,0 +1,20 @@ +setField('date')->setData(['date' => '2024-01-01']); + + assertFalse($rule->passes()); + assertStringContainsString('The date must be a date before the specified date.', (string) $rule->message()); +}); + +it('passes when date is before given date', function () { + $rule = new Before('2024-01-01'); + $rule->setField('date')->setData(['date' => '2023-12-31']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DateBeforeToTest.php b/tests/Unit/Validation/Rules/DateBeforeToTest.php new file mode 100644 index 00000000..fdbeb1af --- /dev/null +++ b/tests/Unit/Validation/Rules/DateBeforeToTest.php @@ -0,0 +1,26 @@ +setField('start_date')->setData([ + 'start_date' => '2024-01-02', + 'end_date' => '2024-01-01', + ]); + + assertFalse($rule->passes()); + assertStringContainsString('must be a date before end_date', (string) $rule->message()); +}); + +it('passes when date is before related date', function () { + $rule = new BeforeTo('end_date'); + $rule->setField('start_date')->setData([ + 'start_date' => '2024-01-01', + 'end_date' => '2024-01-02', + ]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DateEqualTest.php b/tests/Unit/Validation/Rules/DateEqualTest.php new file mode 100644 index 00000000..0c67eda8 --- /dev/null +++ b/tests/Unit/Validation/Rules/DateEqualTest.php @@ -0,0 +1,20 @@ +setField('date')->setData(['date' => '2024-01-02']); + + assertFalse($rule->passes()); + assertStringContainsString('The date must be a date equal to the specified date.', (string) $rule->message()); +}); + +it('passes when date is equal to given date', function () { + $rule = new Equal('2024-01-01'); + $rule->setField('date')->setData(['date' => '2024-01-01']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DateEqualToTest.php b/tests/Unit/Validation/Rules/DateEqualToTest.php new file mode 100644 index 00000000..26cbeabb --- /dev/null +++ b/tests/Unit/Validation/Rules/DateEqualToTest.php @@ -0,0 +1,26 @@ +setField('start_date')->setData([ + 'start_date' => '2024-01-01', + 'end_date' => '2024-01-02', + ]); + + assertFalse($rule->passes()); + assertStringContainsString('must be a date equal to end_date', (string) $rule->message()); +}); + +it('passes when date is equal to related date', function () { + $rule = new EqualTo('end_date'); + $rule->setField('start_date')->setData([ + 'start_date' => '2024-01-02', + 'end_date' => '2024-01-02', + ]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DigitsBetweenTest.php b/tests/Unit/Validation/Rules/DigitsBetweenTest.php new file mode 100644 index 00000000..e9c098fd --- /dev/null +++ b/tests/Unit/Validation/Rules/DigitsBetweenTest.php @@ -0,0 +1,27 @@ +setField('value')->setData(['value' => 12]); // 2 digits + + assertFalse($rule->passes()); + assertStringContainsString('must be between 3 and 5 digits', (string) $rule->message()); +}); + +it('fails when digits count is above maximum', function () { + $rule = new DigitsBetween(3, 5); + $rule->setField('value')->setData(['value' => 123456]); // 6 digits + + assertFalse($rule->passes()); +}); + +it('passes when digits count is within range', function () { + $rule = new DigitsBetween(3, 5); + $rule->setField('value')->setData(['value' => 1234]); // 4 digits + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DigitsTest.php b/tests/Unit/Validation/Rules/DigitsTest.php new file mode 100644 index 00000000..97633fbd --- /dev/null +++ b/tests/Unit/Validation/Rules/DigitsTest.php @@ -0,0 +1,20 @@ +setField('code')->setData(['code' => 12]); // length 2 + + assertFalse($rule->passes()); + assertStringContainsString('must be 3 digits', (string) $rule->message()); +}); + +it('passes when value digits length matches required', function () { + $rule = new Digits(3); + $rule->setField('code')->setData(['code' => 123]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DoesNotEndWithTest.php b/tests/Unit/Validation/Rules/DoesNotEndWithTest.php new file mode 100644 index 00000000..4d8dbc2f --- /dev/null +++ b/tests/Unit/Validation/Rules/DoesNotEndWithTest.php @@ -0,0 +1,20 @@ +setField('text')->setData(['text' => 'endsuf']); + + assertFalse($rule->passes()); + assertStringContainsString('must not end', (string) $rule->message()); +}); + +it('passes when string does not end with forbidden suffix', function () { + $rule = new DoesNotEndWith('suf'); + $rule->setField('text')->setData(['text' => 'suffixx']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/DoesNotStartWithTest.php b/tests/Unit/Validation/Rules/DoesNotStartWithTest.php new file mode 100644 index 00000000..b044cf95 --- /dev/null +++ b/tests/Unit/Validation/Rules/DoesNotStartWithTest.php @@ -0,0 +1,20 @@ +setField('text')->setData(['text' => 'prefix']); + + assertFalse($rule->passes()); + assertStringContainsString('must not start', (string) $rule->message()); +}); + +it('passes when string does not start with forbidden prefix', function () { + $rule = new DoesNotStartWith('pre'); + $rule->setField('text')->setData(['text' => 'xpre']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/EndsWithTest.php b/tests/Unit/Validation/Rules/EndsWithTest.php new file mode 100644 index 00000000..e55255f2 --- /dev/null +++ b/tests/Unit/Validation/Rules/EndsWithTest.php @@ -0,0 +1,20 @@ +setField('text')->setData(['text' => 'prefix']); + + assertFalse($rule->passes()); + assertStringContainsString('must end with', (string) $rule->message()); +}); + +it('passes when string ends with needle', function () { + $rule = new EndsWith('suf'); + $rule->setField('text')->setData(['text' => 'endsuf']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/ExistsTest.php b/tests/Unit/Validation/Rules/ExistsTest.php new file mode 100644 index 00000000..48521abf --- /dev/null +++ b/tests/Unit/Validation/Rules/ExistsTest.php @@ -0,0 +1,29 @@ +getMockBuilder(MysqlConnectionPool::class)->getMock(); + + $connection->expects($this->exactly(1)) + ->method('prepare') + ->willReturnOnConsecutiveCalls( + new Statement(new Result([['exists' => 0]])), + ); + + $this->app->swap(Connection::default(), $connection); + + $exists = new Exists(DB::from('users'), 'email'); + $exists->setData(['email' => 'Abc@ietf.org']); + $exists->setField('email'); + + expect($exists->passes())->toBeFalse(); + expect($exists->message())->toBe('The selected email is invalid.'); +}); diff --git a/tests/Unit/Validation/Rules/FormatDateTest.php b/tests/Unit/Validation/Rules/FormatDateTest.php new file mode 100644 index 00000000..d96e0c1c --- /dev/null +++ b/tests/Unit/Validation/Rules/FormatDateTest.php @@ -0,0 +1,20 @@ +setField('start')->setData(['start' => '2024/01/01']); + + assertFalse($rule->passes()); + assertStringContainsString('does not match the format', (string) $rule->message()); +}); + +it('passes when date matches expected format', function () { + $rule = new Format('Y-m-d'); + $rule->setField('start')->setData(['start' => '2024-01-01']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/InTest.php b/tests/Unit/Validation/Rules/InTest.php new file mode 100644 index 00000000..d03cdde0 --- /dev/null +++ b/tests/Unit/Validation/Rules/InTest.php @@ -0,0 +1,20 @@ +setField('val')->setData(['val' => 'c']); + + assertFalse($rule->passes()); + assertStringContainsString('Allowed', (string) $rule->message()); +}); + +it('passes when value is in allowed list', function () { + $rule = new In(['a','b']); + $rule->setField('val')->setData(['val' => 'a']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/IsArrayTest.php b/tests/Unit/Validation/Rules/IsArrayTest.php new file mode 100644 index 00000000..013f7489 --- /dev/null +++ b/tests/Unit/Validation/Rules/IsArrayTest.php @@ -0,0 +1,20 @@ +setField('data')->setData(['data' => 'string']); + + assertFalse($rule->passes()); + assertStringContainsString('must be an array', (string) $rule->message()); +}); + +it('passes is_array when value is array', function () { + $rule = new IsArray(); + $rule->setField('data')->setData(['data' => []]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/IsBoolTest.php b/tests/Unit/Validation/Rules/IsBoolTest.php new file mode 100644 index 00000000..c936aaae --- /dev/null +++ b/tests/Unit/Validation/Rules/IsBoolTest.php @@ -0,0 +1,20 @@ +setField('flag')->setData(['flag' => 'nope']); + + assertFalse($rule->passes()); + assertStringContainsString('must be true or false', (string) $rule->message()); +}); + +it('passes is_bool when value boolean', function () { + $rule = new IsBool(); + $rule->setField('flag')->setData(['flag' => true]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/IsCollectionTest.php b/tests/Unit/Validation/Rules/IsCollectionTest.php new file mode 100644 index 00000000..01c3b003 --- /dev/null +++ b/tests/Unit/Validation/Rules/IsCollectionTest.php @@ -0,0 +1,27 @@ +setField('items')->setData(['items' => ['a', ['nested' => 'value']]]); + + assertTrue($rule->passes()); +}); + +it('fails for scalar-only list (should be a list, not collection)', function () { + $rule = new IsCollection(); + $rule->setField('items')->setData(['items' => ['a', 'b', 'c']]); + + assertFalse($rule->passes()); + assertStringContainsString('must be a collection', (string) $rule->message()); +}); + +it('fails for associative array where not list', function () { + $rule = new IsCollection(); + $rule->setField('items')->setData(['items' => ['a' => 'v', 'b' => 'z']]); + + assertFalse($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/IsDateTest.php b/tests/Unit/Validation/Rules/IsDateTest.php new file mode 100644 index 00000000..29388b5b --- /dev/null +++ b/tests/Unit/Validation/Rules/IsDateTest.php @@ -0,0 +1,20 @@ +setField('start')->setData(['start' => 'not-date']); + + assertFalse($rule->passes()); + assertStringContainsString('not a valid date', (string) $rule->message()); +}); + +it('passes for valid date string', function () { + $rule = new IsDate(); + $rule->setField('start')->setData(['start' => '2024-12-01']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/IsEmailTest.php b/tests/Unit/Validation/Rules/IsEmailTest.php new file mode 100644 index 00000000..ab6d8c67 --- /dev/null +++ b/tests/Unit/Validation/Rules/IsEmailTest.php @@ -0,0 +1,20 @@ +setField('email')->setData(['email' => 'invalid']); + + assertFalse($rule->passes()); + assertStringContainsString('valid email', (string) $rule->message()); +}); + +it('passes is_email when valid', function () { + $rule = new IsEmail(); + $rule->setField('email')->setData(['email' => 'user@example.com']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/IsFileTest.php b/tests/Unit/Validation/Rules/IsFileTest.php new file mode 100644 index 00000000..b029a6aa --- /dev/null +++ b/tests/Unit/Validation/Rules/IsFileTest.php @@ -0,0 +1,13 @@ +setField('upload')->setData(['upload' => 'string']); + + assertFalse($rule->passes()); + assertStringContainsString('must be a file', (string) $rule->message()); +}); diff --git a/tests/Unit/Validation/Rules/IsFloatTest.php b/tests/Unit/Validation/Rules/IsFloatTest.php new file mode 100644 index 00000000..c39558ee --- /dev/null +++ b/tests/Unit/Validation/Rules/IsFloatTest.php @@ -0,0 +1,20 @@ +setField('ratio')->setData(['ratio' => 10]); + + assertFalse($rule->passes()); + assertStringContainsString('must be a float', (string) $rule->message()); +}); + +it('passes is_float when value float', function () { + $rule = new IsFloat(); + $rule->setField('ratio')->setData(['ratio' => 10.5]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/IsIntegerTest.php b/tests/Unit/Validation/Rules/IsIntegerTest.php new file mode 100644 index 00000000..eda322cb --- /dev/null +++ b/tests/Unit/Validation/Rules/IsIntegerTest.php @@ -0,0 +1,20 @@ +setField('age')->setData(['age' => '12']); + + assertFalse($rule->passes()); + assertStringContainsString('must be an integer', (string) $rule->message()); +}); + +it('passes is_integer when value integer', function () { + $rule = new IsInteger(); + $rule->setField('age')->setData(['age' => 12]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/IsListTest.php b/tests/Unit/Validation/Rules/IsListTest.php new file mode 100644 index 00000000..f87163b2 --- /dev/null +++ b/tests/Unit/Validation/Rules/IsListTest.php @@ -0,0 +1,27 @@ +setField('items')->setData(['items' => ['a', 'b', 'c']]); + + assertTrue($rule->passes()); +}); + +it('fails for non list array (associative)', function () { + $rule = new IsList(); + $rule->setField('items')->setData(['items' => ['a' => 'value', 'b' => 'v']]); + + assertFalse($rule->passes()); + assertStringContainsString('must be a list', (string) $rule->message()); +}); + +it('fails when list contains non scalar values', function () { + $rule = new IsList(); + $rule->setField('items')->setData(['items' => ['a', ['nested']]]); + + assertFalse($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/IsNumericTest.php b/tests/Unit/Validation/Rules/IsNumericTest.php new file mode 100644 index 00000000..194da4a8 --- /dev/null +++ b/tests/Unit/Validation/Rules/IsNumericTest.php @@ -0,0 +1,20 @@ +setField('code')->setData(['code' => 'abc']); + + assertFalse($rule->passes()); + assertStringContainsString('must be a number', (string) $rule->message()); +}); + +it('passes when value numeric', function () { + $rule = new IsNumeric(); + $rule->setField('code')->setData(['code' => '123']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/IsStringTest.php b/tests/Unit/Validation/Rules/IsStringTest.php new file mode 100644 index 00000000..8d483d64 --- /dev/null +++ b/tests/Unit/Validation/Rules/IsStringTest.php @@ -0,0 +1,28 @@ +setField('name')->setData(['name' => 123]); + + assertFalse($rule->passes()); + assertStringContainsString('must be a string', (string) $rule->message()); +}); + +it('passes when value is a string', function () { + $rule = new IsString(); + $rule->setField('name')->setData(['name' => 'John']); + + assertTrue($rule->passes()); +}); + +it('display field name for humans', function () { + $rule = new IsString(); + $rule->setField('last_name')->setData(['last_name' => 123]); + + assertFalse($rule->passes()); + assertStringContainsString('last name must be a string', (string) $rule->message()); +}); diff --git a/tests/Unit/Validation/Rules/IsUrlTest.php b/tests/Unit/Validation/Rules/IsUrlTest.php new file mode 100644 index 00000000..9c09bebe --- /dev/null +++ b/tests/Unit/Validation/Rules/IsUrlTest.php @@ -0,0 +1,20 @@ +setField('site')->setData(['site' => 'notaurl']); + + assertFalse($rule->passes()); + assertStringContainsString('valid URL', (string) $rule->message()); +}); + +it('passes when valid', function () { + $rule = new IsUrl(); + $rule->setField('site')->setData(['site' => 'https://example.com']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/MaxTest.php b/tests/Unit/Validation/Rules/MaxTest.php index 8e7b7df2..50fbc0f8 100644 --- a/tests/Unit/Validation/Rules/MaxTest.php +++ b/tests/Unit/Validation/Rules/MaxTest.php @@ -47,3 +47,19 @@ public function count(): int false, ], ]); + +it('builds proper max messages for each type', function (int|float $limit, string $field, array $data, string $expectedFragment): void { + $rule = new Max($limit); + $rule->setField($field)->setData($data); + + expect($rule->passes())->toBeFalse(); + + $message = $rule->message(); + + expect($message)->toBeString(); + expect($message)->toContain($expectedFragment); +})->with([ + 'numeric' => [1, 'value', ['value' => 2], 'greater than'], + 'string' => [3, 'name', ['name' => 'John'], 'greater than 3 characters'], + 'array' => [1, 'items', ['items' => ['a','b']], 'more than 1 items'], +]); diff --git a/tests/Unit/Validation/Rules/MimesTest.php b/tests/Unit/Validation/Rules/MimesTest.php new file mode 100644 index 00000000..389d9af4 --- /dev/null +++ b/tests/Unit/Validation/Rules/MimesTest.php @@ -0,0 +1,61 @@ +getFilename(), + file_get_contents($file), + $contentType, + [['Content-Type', $contentType]] + ); + + $rule = new Mimes(['image/png']); + $rule->setField('image')->setData(['image' => $bufferedFile]); + + assertTrue($rule->passes()); +}); + +it('fails when file mime type is not allowed', function (): void { + $file = __DIR__ . '/../../../fixtures/files/user.png'; + $fileInfo = new SplFileInfo($file); + $contentType = mime_content_type($file); // image/png + + $bufferedFile = new BufferedFile( + $fileInfo->getFilename(), + file_get_contents($file), + $contentType, + [['Content-Type', $contentType]] + ); + + $rule = new Mimes(['image/jpeg']); + $rule->setField('image')->setData(['image' => $bufferedFile]); + + assertFalse($rule->passes()); + assertStringContainsString('image/jpeg', (string) $rule->message()); +}); + +it('passes when file mime type is in multi-value whitelist', function (): void { + $file = __DIR__ . '/../../../fixtures/files/user.png'; + $fileInfo = new SplFileInfo($file); + $contentType = mime_content_type($file); // image/png + + $bufferedFile = new BufferedFile( + $fileInfo->getFilename(), + file_get_contents($file), + $contentType, + [['Content-Type', $contentType]] + ); + + $rule = new Mimes(['image/jpeg', 'image/png']); + $rule->setField('image')->setData(['image' => $bufferedFile]); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/MinTest.php b/tests/Unit/Validation/Rules/MinTest.php index 099a3ccc..fa6ea426 100644 --- a/tests/Unit/Validation/Rules/MinTest.php +++ b/tests/Unit/Validation/Rules/MinTest.php @@ -47,3 +47,19 @@ public function count(): int false, ], ]); + +it('builds proper min messages for each type', function (int|float $limit, string $field, array $data, string $expectedFragment): void { + $rule = new Min($limit); + $rule->setField($field)->setData($data); + + expect($rule->passes())->toBeFalse(); + + $message = $rule->message(); + + expect($message)->toBeString(); + expect($message)->toContain($expectedFragment); +})->with([ + 'numeric' => [3, 'value', ['value' => 2], 'The value must be at least 3'], + 'string' => [5, 'name', ['name' => 'John'], 'The name must be at least 5 characters'], + 'array' => [3, 'items', ['items' => ['a','b']], 'The items must have at least 3 items'], +]); diff --git a/tests/Unit/Validation/Rules/NotInTest.php b/tests/Unit/Validation/Rules/NotInTest.php new file mode 100644 index 00000000..a166899a --- /dev/null +++ b/tests/Unit/Validation/Rules/NotInTest.php @@ -0,0 +1,20 @@ +setField('val')->setData(['val' => 'b']); + + assertFalse($rule->passes()); + assertStringContainsString('Disallowed', (string) $rule->message()); +}); + +it('passes when value is not inside the forbidden list', function () { + $rule = new NotIn(['a','b','c']); + $rule->setField('val')->setData(['val' => 'x']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/NullableTest.php b/tests/Unit/Validation/Rules/NullableTest.php new file mode 100644 index 00000000..b1c634ab --- /dev/null +++ b/tests/Unit/Validation/Rules/NullableTest.php @@ -0,0 +1,21 @@ +setField('foo')->setData([]); + + assertFalse($rule->passes()); + assertSame(null, $rule->message()); +}); + +it('nullable passes and returns null message when value is null', function () { + $rule = new Nullable(); + $rule->setField('foo')->setData(['foo' => null]); + + assertTrue($rule->passes()); + assertSame(null, $rule->message()); +}); diff --git a/tests/Unit/Validation/Rules/OptionalTest.php b/tests/Unit/Validation/Rules/OptionalTest.php new file mode 100644 index 00000000..083b29b7 --- /dev/null +++ b/tests/Unit/Validation/Rules/OptionalTest.php @@ -0,0 +1,21 @@ +setField('foo')->setData([]); + + assertTrue($rule->passes()); + assertSame(null, $rule->message()); +}); + +it('optional fails when present but empty', function () { + $rule = new Optional(); + $rule->setField('foo')->setData(['foo' => '']); + + assertFalse($rule->passes()); + assertSame(null, $rule->message()); +}); diff --git a/tests/Unit/Validation/Rules/RegExTest.php b/tests/Unit/Validation/Rules/RegExTest.php new file mode 100644 index 00000000..6d4583ec --- /dev/null +++ b/tests/Unit/Validation/Rules/RegExTest.php @@ -0,0 +1,20 @@ +setField('code')->setData(['code' => 'abc']); + + assertFalse($rule->passes()); + assertStringContainsString('format is invalid', (string) $rule->message()); +}); + +it('passes when value matches regex', function () { + $rule = new RegEx('/^[0-9]+$/'); + $rule->setField('code')->setData(['code' => '123']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/RequiredTest.php b/tests/Unit/Validation/Rules/RequiredTest.php new file mode 100644 index 00000000..83835bba --- /dev/null +++ b/tests/Unit/Validation/Rules/RequiredTest.php @@ -0,0 +1,20 @@ +setField('name')->setData([]); + + expect($rule->passes())->toBeFalse(); + expect($rule->message())->toBe('The name field is required.'); +}); + +it('passes required when value present', function (): void { + $rule = new Required(); + $rule->setField('name')->setData(['name' => 'John']); + + expect($rule->passes())->toBeTrue(); +}); diff --git a/tests/Unit/Validation/Rules/SizeTest.php b/tests/Unit/Validation/Rules/SizeTest.php index 7bce5c36..965a4074 100644 --- a/tests/Unit/Validation/Rules/SizeTest.php +++ b/tests/Unit/Validation/Rules/SizeTest.php @@ -4,6 +4,21 @@ use Phenix\Validation\Rules\Size; +it('fails size for string length mismatch', function () { + $rule = new Size(5); + $rule->setField('name')->setData(['name' => 'John']); + + assertFalse($rule->passes()); + assertStringContainsString('must be 5 characters', (string) $rule->message()); +}); + +it('passes size for exact string length', function () { + $rule = new Size(4); + $rule->setField('name')->setData(['name' => 'John']); + + assertTrue($rule->passes()); +}); + it('checks size according to data type', function ( float|int $limit, string $field, diff --git a/tests/Unit/Validation/Rules/StartsWithTest.php b/tests/Unit/Validation/Rules/StartsWithTest.php new file mode 100644 index 00000000..edecc832 --- /dev/null +++ b/tests/Unit/Validation/Rules/StartsWithTest.php @@ -0,0 +1,20 @@ +setField('text')->setData(['text' => 'postfix']); + + assertFalse($rule->passes()); + assertStringContainsString('must start with', (string) $rule->message()); +}); + +it('passes when string starts with needle', function () { + $rule = new StartsWith('pre'); + $rule->setField('text')->setData(['text' => 'prefix']); + + assertTrue($rule->passes()); +}); diff --git a/tests/Unit/Validation/Rules/UidTest.php b/tests/Unit/Validation/Rules/UidTest.php new file mode 100644 index 00000000..f0e465b1 --- /dev/null +++ b/tests/Unit/Validation/Rules/UidTest.php @@ -0,0 +1,18 @@ +setField('id')->setData(['id' => 'not-uuid']); + + assertFalse($uuid->passes()); + assertStringContainsString('valid UUID', (string) $uuid->message()); + + $ulid = (new Ulid())->setField('id')->setData(['id' => 'not-ulid']); + + assertFalse($ulid->passes()); + assertStringContainsString('valid ULID', (string) $ulid->message()); +}); diff --git a/tests/Unit/Validation/Rules/UniqueTest.php b/tests/Unit/Validation/Rules/UniqueTest.php new file mode 100644 index 00000000..a55c0806 --- /dev/null +++ b/tests/Unit/Validation/Rules/UniqueTest.php @@ -0,0 +1,65 @@ + 0)', function (): void { + $connection = $this->getMockBuilder(MysqlConnectionPool::class)->getMock(); + + $connection->expects($this->exactly(1)) + ->method('prepare') + ->willReturnOnConsecutiveCalls( + new Statement(new Result([[ 'COUNT(*)' => 1 ]])), + ); + + $this->app->swap(Connection::default(), $connection); + + $unique = new Unique(DB::from('users'), 'email'); + $unique->setData(['email' => 'user@example.com']); + $unique->setField('email'); + + assertFalse($unique->passes()); + assertSame('The email has already been taken.', (string) $unique->message()); +}); + +it('passes validation when value does not exist (count == 0)', function (): void { + $connection = $this->getMockBuilder(MysqlConnectionPool::class)->getMock(); + + $connection->expects($this->exactly(1)) + ->method('prepare') + ->willReturnOnConsecutiveCalls( + new Statement(new Result([[ 'COUNT(*)' => 0 ]])), + ); + + $this->app->swap(Connection::default(), $connection); + + $unique = new Unique(DB::from('users'), 'email'); + $unique->setData(['email' => 'user@example.com']); + $unique->setField('email'); + + assertTrue($unique->passes()); +}); + +it('passes validation when value does not exist using custom column', function (): void { + $connection = $this->getMockBuilder(MysqlConnectionPool::class)->getMock(); + + $connection->expects($this->exactly(1)) + ->method('prepare') + ->willReturnOnConsecutiveCalls( + new Statement(new Result([[ 'COUNT(*)' => 0 ]])), + ); + + $this->app->swap(Connection::default(), $connection); + + $unique = new Unique(DB::from('users'), 'user_email'); + $unique->setData(['email' => 'user@example.com']); + $unique->setField('email'); + + assertTrue($unique->passes()); +}); diff --git a/tests/Unit/Validation/ValidatorTest.php b/tests/Unit/Validation/ValidatorTest.php index 2ef7c082..9de37967 100644 --- a/tests/Unit/Validation/ValidatorTest.php +++ b/tests/Unit/Validation/ValidatorTest.php @@ -7,9 +7,6 @@ use Phenix\Validation\Exceptions\InvalidCollectionDefinition; use Phenix\Validation\Exceptions\InvalidData; use Phenix\Validation\Exceptions\InvalidDictionaryDefinition; -use Phenix\Validation\Rules\IsDictionary; -use Phenix\Validation\Rules\IsString; -use Phenix\Validation\Rules\Required; use Phenix\Validation\Types\Arr; use Phenix\Validation\Types\ArrList; use Phenix\Validation\Types\Collection; @@ -102,7 +99,7 @@ public function toArray(): array expect($validator->passes())->toBeFalse(); expect($validator->failing())->toBe([ - 'name' => [Required::class], + 'name' => ['The name field is required.'], ]); expect($validator->invalid())->toBe([ @@ -174,8 +171,8 @@ public function toArray(): array expect($validator->passes())->toBeFalsy(); expect($validator->failing())->toBe([ - 'customer' => [IsDictionary::class], - 'customer.email' => [IsString::class], + 'customer' => ['The customer field must be a dictionary.'], + 'customer.email' => ['The customer.email must be a string.'], ]); expect($validator->invalid())->toBe([ @@ -274,7 +271,7 @@ public function toArray(): array expect($validator->passes())->toBeFalsy(); expect($validator->failing())->toBe([ - 'date' => [Required::class], + 'date' => ['The date field is required.'], ]); expect($validator->invalid())->toBe([ @@ -308,7 +305,7 @@ public function toArray(): array expect($validator->passes())->toBeFalse(); expect($validator->failing())->toBe([ - 'date' => [Required::class], + 'date' => ['The date field is required.'], ]); expect($validator->invalid())->toBe([ diff --git a/tests/fixtures/application/lang/en/validation.php b/tests/fixtures/application/lang/en/validation.php new file mode 100644 index 00000000..242bed4b --- /dev/null +++ b/tests/fixtures/application/lang/en/validation.php @@ -0,0 +1,76 @@ + 'The :field is invalid.', + 'required' => 'The :field field is required.', + 'string' => 'The :field must be a string.', + 'array' => 'The :field must be an array.', + 'boolean' => 'The :field field must be true or false.', + 'file' => 'The :field must be a file.', + 'url' => 'The :field must be a valid URL.', + 'email' => 'The :field must be a valid email address.', + 'uuid' => 'The :field must be a valid UUID.', + 'ulid' => 'The :field must be a valid ULID.', + 'integer' => 'The :field must be an integer.', + 'numeric' => 'The :field must be a number.', + 'float' => 'The :field must be a float.', + 'dictionary' => 'The :field field must be a dictionary.', + 'collection' => 'The :field must be a collection.', + 'list' => 'The :field must be a list.', + 'confirmed' => 'The :field does not match :other.', + 'in' => 'The selected :field is invalid. Allowed: :values.', + 'not_in' => 'The selected :field is invalid. Disallowed: :values.', + 'exists' => 'The selected :field is invalid.', + 'unique' => 'The :field has already been taken.', + 'mimes' => 'The :field must be a file of type: :values.', + 'regex' => 'The :field format is invalid.', + 'starts_with' => 'The :field must start with: :values.', + 'ends_with' => 'The :field must end with: :values.', + 'does_not_start_with' => 'The :field must not start with: :values.', + 'does_not_end_with' => 'The :field must not end with: :values.', + 'digits' => 'The :field must be :digits digits.', + 'digits_between' => 'The :field must be between :min and :max digits.', + 'size' => [ + 'numeric' => 'The :field must be :size.', + 'string' => 'The :field must be :size characters.', + 'array' => 'The :field must contain :size items.', + 'file' => 'The :field must be :size kilobytes.', + ], + 'min' => [ + 'numeric' => 'The :field must be at least :min.', + 'string' => 'The :field must be at least :min characters.', + 'array' => 'The :field must have at least :min items.', + 'file' => 'The :field must be at least :min kilobytes.', + ], + 'max' => [ + 'numeric' => 'The :field may not be greater than :max.', + 'string' => 'The :field may not be greater than :max characters.', + 'array' => 'The :field may not have more than :max items.', + 'file' => 'The :field may not be greater than :max kilobytes.', + ], + 'between' => [ + 'numeric' => 'The :field must be between :min and :max.', + 'string' => 'The :field must be between :min and :max characters.', + 'array' => 'The :field must have between :min and :max items.', + 'file' => 'The :field must be between :min and :max kilobytes.', + ], + 'date' => [ + 'is_date' => 'The :field is not a valid date.', + 'after' => 'The :field must be a date after the specified date.', + 'format' => 'The :field does not match the format :format.', + 'equal_to' => 'The :field must be a date equal to :other.', + 'after_to' => 'The :field must be a date after :other.', + 'after_or_equal_to' => 'The :field must be a date after or equal to :other.', + 'before_or_equal_to' => 'The :field must be a date before or equal to :other.', + 'after_or_equal' => 'The :field must be a date after or equal to the specified date.', + 'before_or_equal' => 'The :field must be a date before or equal to the specified date.', + 'equal' => 'The :field must be a date equal to the specified date.', + 'before_to' => 'The :field must be a date before :other.', + 'before' => 'The :field must be a date before the specified date.', + ], + + 'fields' => [ + 'last_name' => 'last name', + 'customer.email' => 'customer email address', + ], +];