Skip to content

PHP Tutorial Practical PHP Development

Validation Architecture

Lesson 324 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

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

index.php
PHP
$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

index.php
PHP
<?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";
Output
OUTPUT
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

LayerValidates
Request / controllerThe shape and format of input (required, email, length)
Service / domainBusiness rules (stock available, coupon valid)
DatabaseIntegrity (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.

index.php
PHP
<?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 . ">";
Output
OUTPUT
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.

info
Why this matters in Laravel
Laravel’s validation uses the same pipe-separated rule strings ("required|email|max:255"), so the syntax above will look very familiar.