The Problem: Repetitive Validation

In almost every project I’ve worked on, validation ends up scattered across controllers, models, and even service classes. You write the same isset checks, trim calls, and regex patterns over and over. This duplication makes the code harder to read, increases the chance of missing an edge case, and turns refactoring into a nightmare when a validation rule changes.

Introducing a Filter Wrapper

I built a small, reusable wrapper around PHP’s filter_var and filter_input functions that lets me declare validation rules in a declarative array. The wrapper returns either the cleaned value or throws a specific exception that can be caught upstream. By centralizing the logic, I keep my controllers thin and my tests focused on business rules rather than boilerplate.

How It Works

The core is a class called InputFilter. Its constructor accepts an associative array where the key is the input name and the value is an array describing the filter, flags, and optional callbacks. The public method process() iterates over the definition, applies the appropriate filter, and stores the result in a protected property. If any filter fails, it throws ValidationException with details about the offending field.


class ValidationException extends \Exception {
    public function __construct(string $field, string $message) {
        parent::__construct(sprintf('Validation failed for field "%s": %s', $field, $message));
        $this->field = $field;
    }
    public function getField(): string {
        return $this->field;
    }
}

class InputFilter {
    /** @var array */
    private $definition;
    /** @var array */
    private $filtered = [];

    public function __construct(array $definition) {
        $this->definition = $definition;
    }

    /**
     * @param array $data Raw input (usually $_GET, $_POST, or json_decode)
     * @return self
     * @throws ValidationException
     */
    public function process(array $data): self {
        foreach ($this->definition as $field => $rule) {
            $value = $data[$field] ?? null;
            $filtered = $this->applyFilter($value, $rule);
            $this->filtered[$field] = $filtered;
        }
        return $this;
    }

    public function get(string $field): mixed {
        return $this->filtered[$field] ?? null;
    }

    private function applyFilter(mixed $value, array $rule): mixed {
        $filter = $rule['filter'] ?? FILTER_DEFAULT;
        $flags  = $rule['flags']  ?? 0;
        $options= $rule['options'] ?? null;

        if ($filter === FILTER_CALLBACK && isset($rule['callback'])) {
            $callback = $rule['callback'];
            return call_user_func($callback, $value);
        }

        $result = filter_var($value, $filter, $flags, $options);
        if ($result === false || $result === null) {
            throw new ValidationException($field, 'Invalid value');
        }
        return $result;
    }
}

Real‑World Example: User Registration

Imagine a registration endpoint that receives JSON with email, password, and age. Using the wrapper, the validation definition becomes a readable configuration:


$definition = [
    'email' => [
        'filter' => FILTER_VALIDATE_EMAIL,
        'flags'  => 0,
    ],
    'password' => [
        'filter' => FILTER_CALLBACK,
        'callback' => fn($p) => strlen($p) >= 8 ? $p : false,
    ],
    'age' => [
        'filter' => FILTER_VALIDATE_INT,
        'options' => ['min_range' => 13, 'max_range' => 120],
    ],
];

try {
    $input = new InputFilter($definition);
    $input->process(json_decode(file_get_contents('php://input'), true));

    $email    = $input->get('email');
    $password = $input->get('password');
    $age      = $input->get('age');

    // Proceed to create the user entity
    $user = new User($email, password_hash($password, PASSWORD_DEFAULT), $age);
    $userRepository->save($user);
    http_response_code(201);
    echo json_encode(['message' => 'User created']);
} catch (ValidationException $e) {
    http_response_code(400);
    echo json_encode([
        'error'   => 'validation_failed',
        'field'   => $e->getField(),
        'message' => $e->getMessage()
    ]);
} catch (\Exception $e) {
    http_response_code(500);
    echo json_encode(['error' => 'internal_error']);
}

Notice how the controller now focuses on orchestration: decode input, run the filter, and act on the clean data. All the messy checks live in the definition array, which can be unit‑tested in isolation.

Why This Approach Wins

  • Readability: The validation rules are expressed as data, making it easy to see at a glance what each field expects.
  • Reusability: The same InputFilter class can be used for HTTP requests, CLI arguments, or even data imported from CSV files.
  • Testability: You can instantiate the class with a mock data set and assert that the filtered output matches expectations without touching the framework.
  • Maintainability: Changing a rule — say, tightening the password length — requires editing a single line in the definition array, not hunting through multiple controllers.

Extending the Wrapper

Over time I’ve added a few convenience features:

  1. Support for nested arrays using dot‑notation keys (e.g., "user.address.city").
  2. Optional filters that allow missing fields to be skipped rather than throwing.
  3. A sanitizer mode that applies FILTER_SANITIZE_STRING or custom callbacks when validation isn’t needed.

These extensions stay true to the original goal: keep validation logic declarative, centralized, and easy to reason about.

When you find yourself writing the same isset/trim/preg_match pattern for the third time, pause. A small wrapper like this can save you hours of debugging and make your codebase feel lighter.