errors[$field][] = $message ?: "{$field} is required."; } return $this; } public function maxLength(string $field, mixed $value, int $max, string $message = ''): self { if (strlen((string) $value) > $max) { $this->errors[$field][] = $message ?: "{$field} must be {$max} characters or fewer."; } return $this; } public function numeric(string $field, mixed $value, string $message = ''): self { if (!is_numeric($value)) { $this->errors[$field][] = $message ?: "{$field} must be numeric."; } return $this; } public function email(string $field, mixed $value, string $message = ''): self { if ((string) $value !== '' && filter_var($value, FILTER_VALIDATE_EMAIL) === false) { $this->errors[$field][] = $message ?: "{$field} must be a valid email address."; } return $this; } public function date(string $field, mixed $value, string $format = 'Y-m-d', string $message = ''): self { $str = (string) $value; if ($str === '') { return $this; } $parsed = \DateTimeImmutable::createFromFormat($format, $str); if ($parsed === false || $parsed->format($format) !== $str) { $this->errors[$field][] = $message ?: "{$field} must be a valid date ({$format})."; } return $this; } public function minLength(string $field, mixed $value, int $min, string $message = ''): self { if (strlen((string) $value) < $min) { $this->errors[$field][] = $message ?: "{$field} must be at least {$min} characters."; } return $this; } public function min(string $field, mixed $value, int|float $min, string $message = ''): self { if (!is_numeric($value) || (float) $value < $min) { $this->errors[$field][] = $message ?: "{$field} must be at least {$min}."; } return $this; } public function max(string $field, mixed $value, int|float $max, string $message = ''): self { if (!is_numeric($value) || (float) $value > $max) { $this->errors[$field][] = $message ?: "{$field} must be no more than {$max}."; } return $this; } public function in(string $field, mixed $value, array $allowed, string $message = ''): self { if (!in_array($value, $allowed, true)) { $this->errors[$field][] = $message ?: "{$field} must be one of: " . implode(', ', $allowed) . '.'; } return $this; } public function passes(): bool { return empty($this->errors); } public function fails(): bool { return !$this->passes(); } public function errors(): array { return $this->errors; } }