In small scripts, each form has its own if chains. With many forms, you repeat the same checks (required, email, length) again and again. A validator component centralises them.
The goal
$validator = new Validator([
"name" => "required|min:2|max:100",
"email" => "required|email",
"age" => "int|between:18,120",
]);
$errors = $validator->validate($_POST);
One description of the rules, one place that runs them, one format of errors.
A small implementation
<?php
final class Validator
{
public function __construct(private array $rules) {}
/** @return array<string, string> field => first error message */
public function validate(array $input): array
{
$errors = [];
foreach ($this->rules as $field => $ruleString) {
$value = $input[$field] ?? null;
$value = is_string($value) ? trim($value) : $value;
foreach (explode("|", $ruleString) as $rule) {
[$name, $argument] = array_pad(explode(":", $rule, 2), 2, null);
$message = $this->check($field, $value, $name, $argument);
if ($message !== null) {
$errors[$field] = $message;
break; // report the first problem per field
}
}
}
return $errors;
}
private function check(string $field, mixed $value, string $rule, ?string $arg): ?string
{
$blank = $value === null || $value === "";
if ($rule === "required") {
return $blank ? "The $field field is required." : null;
}
if ($blank) {
return null; // optional and empty: nothing more to check
}
return match ($rule) {
"email" => filter_var($value, FILTER_VALIDATE_EMAIL) === false ? "The $field must be a valid email address." : null,
"int" => filter_var($value, FILTER_VALIDATE_INT) === false ? "The $field must be a whole number." : null,
"min" => mb_strlen((string) $value) < (int) $arg ? "The $field must have at least $arg characters." : null,
"max" => mb_strlen((string) $value) > (int) $arg ? "The $field may not have more than $arg characters." : null,
"between" => $this->between($field, $value, (string) $arg),
"in" => in_array($value, explode(",", (string) $arg), true) ? null : "The $field is not an allowed value.",
default => throw new InvalidArgumentException("Unknown rule: $rule"),
};
}
private function between(string $field, mixed $value, string $arg): ?string
{
[$low, $high] = array_map("intval", explode(",", $arg));
$number = filter_var($value, FILTER_VALIDATE_INT);
return ($number === false || $number < $low || $number > $high)
? "The $field must be between $low and $high."
: null;
}
}
$validator = new Validator([
"name" => "required|min:2|max:20",
"email" => "required|email",
"age" => "int|between:18,120",
"plan" => "in:free,pro",
]);
print_r($validator->validate(["name" => "A", "email" => "isha@", "age" => "17", "plan" => "gold"]));
echo count($validator->validate(["name" => "Isha", "email" => "isha@example.com", "age" => "29", "plan" => "pro"])) . " errors for valid data";
Array
(
[name] => The name must have at least 2 characters.
[email] => The email must be a valid email address.
[age] => The age must be between 18 and 120.
[plan] => The plan is not an allowed value.
)
0 errors for valid data
What this design gives you
- Consistent messages and error format.
- Reuse across every form and API endpoint.
- Rules written as data, easy to read and change.
- An unknown rule is an error, so typos do not pass silently.
Where validation lives
| Layer | Validates |
|---|---|
| Request / controller | The shape and format of input (required, email, length) |
| Service / domain | Business rules (stock available, coupon valid) |
| Database | Integrity (UNIQUE, NOT NULL, foreign keys) as the last line of defence |
Do not rely on only one. The database constraint is what finally stops two simultaneous sign-ups with the same email.
Return typed data, not only errors
A good pattern: validation returns either errors or a clean, typed object.
<?php
final class SignupData
{
private function __construct(public readonly string $name, public readonly string $email) {}
public static function from(array $input): self
{
return new self(trim((string) $input["name"]), strtolower(trim((string) $input["email"])));
}
}
$data = SignupData::from(["name" => " Isha ", "email" => "Isha@Example.COM"]);
echo $data->name . " <" . $data->email . ">";
Isha <isha@example.com>
After validation, the rest of the app receives a SignupData, not a loose array of strings.
Use a library for big needs
Packages such as Respect\Validation, Symfony Validator and Laravel’s validator offer many more rules, translated messages and nested validation. Writing a small one is useful for learning and for small projects.
Show errors to the user
Pair the errors with accessible form markup (labels, aria-describedby, an error summary). See Validation Errors.
Why this matters in Laravel
"required|email|max:255"), so the syntax above will look very familiar.