PHP Validation

Summary: in this tutorial, you’ll learn how to build a reusable PHP validation library from scratch.

In previous tutorials, you learned how to validate a form field using the filter_input() and filter_var() functions. For example, the following sanitizes and validates the email field in the POST request:

// validate email $email = filter_input(INPUT_POST, 'email', FILTER_SANITIZE_EMAIL); $inputs['email'] = $email; if ($email) { $email = filter_var($email, FILTER_SANITIZE_EMAIL); if (!$email) { $errors['email'] = 'Please enter a valid email'; } } else { $errors['email'] = 'Please enter an email'; }
Code language: PHP (php)

This code works fine. However, it’s pretty lengthy. Also, it’s not reusable. So let’s build a reusable validation library.

Define validation rules

Each field has one or more validation rules. For example, the email field is required and must be a valid email address, so the email field has two rules:

  • required
  • email

The username is required and between 3 and 255 characters. Also, it should contain only letters and numbers. Therefore, the username field has three rules:

  • required
  • between 3 and 20 characters
  • alphanumeric

To define rules for fields, you can define a $fields array like this:

$fields = [ 'email': 'required | email', 'username': 'username' => 'required | alphanumeric | between: 3,255 ];
Code language: PHP (php)

Each element in the $fields array has the key as the field name and value as the rules. You use the | character to separate two rules.

If a rule has parameters, e.g., between: 3, 25, you use the : character to separate the rule name and its parameters. Also, we use the comma , to separate two parameters.

You’ll create the following rules:

RuleRule nameParameterMeaning
requiredrequiredNoThe field is set and not empty
alphanumeric alphanumericNoThe field only contains letters and numbers
emailemailNoThe field is a valid email address
securesecureNoThe field must have between 8 and 64 characters and contain at least one number, one upper case letter, one lower case letter, and one special character. This rule is for the password field.
min: 3minAn integer specifies the minimum length of the fieldThe length of the field must be greater than or equal to min length, e.g., 3
max: 255maxAn integer specifies the maximum length of the fieldThe length of the field must be less than or equal to min length e.g., 255
same: another_fieldsameThe name of another fieldThe field value must be the same as the value of the another_field
between: min, maxbetweenmin and max are integers that specify the minimum and maximum length of the fieldThe length of the field must be between min and max.
unique: table, columnuniquecolumn and table in a relational database. Or field and collection in a NoSQL databaseThe field value must be unique in the column of the table in a database

The following example defines the fields with rules:

$fields = [ 'firstname' => 'required, max:255', 'lastname' => 'required, max: 255', 'address' => 'required | min: 10, max:255', 'zipcode' => 'between: 5,6', 'username' => 'required | alphanumeric | between: 3,255 | unique: users,username', 'email' => 'required | email | unique: users,email', 'password' => 'required | secure', 'password2' => 'required | same:password' ];
Code language: PHP (php)

The validate() function

The validation library should have the validate() function that accepts an associative array of data to validate $data and the $fields array that contains the validation rules:

function validate(array $data, array $fields)
Code language: PHP (php)

It should return an array that contains the validation errors, in which the key of each element is the field name and the value is the error message:

function validate(array $data, array $fields): array { // implementation }
Code language: PHP (php)

The validate() function will need to iterate over the $fields. For each field, it loops through rules and validates the value against each rule. If the validation fails, it’ll add an error message to an $errors array:

function validate(array $data, array $fields) : array { $errors = []; foreach ($fields as $field => $option) { // get the rules of the field $rules = split($option); foreach ($rules as $rule) { // run a validation rule for each field } } return $errors; }
Code language: PHP (php)

Since the rules of a field is a string, you need to separate the rules using the | character. Also, you need to strip all the whitespaces. To do that, you can define an arrow function to:

  • First, split a string by a separator.
  • Then, trim each item in the result array and return it.

The arrow function will look like this:

$split = fn($str, $separator) => array_map('trim', explode($separator, $str));
Code language: PHP (php)

The arrow function accepts a string and a separator. The explode() function splits the $str by the $separator. The result of the explode() function is an array of strings.

The array_map() function executes the trim() function on each item and returns a new array of items with the whitespaces removed.

The validate() function is updated to include the arrow function as follows:

function validate(array $data, array $fields) { $split = fn($str, $separator) => array_map('trim', explode($separator, $str)); $errors = []; foreach ($fields as $field => $option) { // get the rules of the field $rules = $split($option, '|'); foreach ($rules as $rule) { // run a validation on each rule } } return $errors; }
Code language: PHP (php)

The $rule may or may not have parameters. If it has parameters, it’ll contain the : character. The following code extracts the rule name and its parameters:

if (strpos($rule, ':')) { [$rule_name, $paramStr] = $split($rule, ':'); $params = $split($$param_str, ','); } else { $rule_name = trim($rule); }
Code language: PHP (php)

You use the $split arrow function in this code to split the rule by the : character. For example:

between: 3,255
Code language: PHP (php)

will become two strings:

'between' '3,255'
Code language: PHP (php)

The following uses the array destructuring to assign the first and second elements of the result of the $split function to the $rule_name and$param_str:

[$rule_name, $param_str] = $split($rule, ':');
Code language: PHP (php)

For example, the rule "between : 3, 255" will result in:

$rule_name = 'between'; $param_str = '3, 255'
Code language: PHP (php)

To get the parameter list, you pass the $param_str to the $split() function:

$params = $split($param_str, ',');
Code language: PHP (php)

The validate() function will look like the following:

function validate(array $data, array $fields): array { $errors = []; $split = fn($str, $separator) => array_map('trim', explode($separator, $str)); foreach ($fields as $field => $option) { $rules = $split($option, '|'); foreach ($rules as $rule) { $params = []; if (strpos($rule, ':')) { [$rule_name, $param_str] = $split($rule, ':'); $params = $split($param_str, ','); } else { $rule_name = trim($rule); } } } return $errors; }
Code language: PHP (php)

Now, you have fields and their rule names with parameters.

To execute validation for each rule, you can have a big if-elseif statement like this:

if($rule_name === 'required') { } elseif($rule_name === 'email') { } ...
Code language: PHP (php)

However, this approach has two issues:

  • First, a big if-elseif is hard to maintain.
  • Second, if you want to add a new rule, you need to change the validate() function, which is not ideal.

You want the validate() function to be more dynamic so that we don’t need to change the function when you add a new rule later. To do that, you can use the variable functions.

For each rule, you call the validation function dynamically like this:

$rule_name($data, $field, ...$params);
Code language: PHP (php)

The first and second arguments of the validation function are $data and $field. The third argument is a list of arguments spread from the array $params.

For example, if the $params is [3,255], the ...$params will return 3 and 255.

So the rule 'between: 3, 255' of the username with the value "john" will result in the following function call:

between($data, 'username', 3, 255)
Code language: PHP (php)

To prevent the validation function from collie with the standard function like min, you can prefix our validation function with the string is_. For example, the between rule will execute the is_between validation function.

The following shows the updated validate() function:

function validate(array $data, array $fields): array { $errors = []; $split = fn($str, $separator) => array_map('trim', explode($separator, $str)); foreach ($fields as $field => $option) { $rules = $split($option, '|'); foreach ($rules as $rule) { $params = []; if (strpos($rule, ':')) { [$rule_name, $param_str] = $split($rule, ':'); $params = $split($param_str, ','); } else { $rule_name = trim($rule); } $fn = 'is_' . $rule_name; if (is_callable($fn)) { $pass = $fn($data, $field, ...$params); } } } return $errors; }
Code language: PHP (php)

In this validate() function, you use the is_callable() function to check if the is_rule_name is a callable before calling it.

Returning error messages

Each rule should have a specific error message. If validation fails, you need to store the error message in the $error array. Also, you should be able to return an error message with the parameters.

For example, if the following rule fails:

'username' => 'between: 3, 255'
Code language: PHP (php)

You should return the error message:

The username should be between 3 and 255 characters.
Code language: PHP (php)

To do that, first, you define the default validation error messages:

const DEFAULT_VALIDATION_ERRORS = [ 'required' => 'Please enter the %s', 'email' => 'The %s is not a valid email address', 'min' => 'The %s must have at least %s characters', 'max' => 'The %s must have at most %s characters', 'between' => 'The %s must have between %d and %d characters', 'same' => 'The %s must match with %s', 'alphanumeric' => 'The %s should have only letters and numbers', 'secure' => 'The %s must have between 8 and 64 characters and contain at least one number, one upper case letter, one lower case letter and one special character', 'unique' => 'The %s already exists', ];
Code language: PHP (php)

In the DEFAULT_VALIDATION_ERRORS array, the key is the rule name while the value is the error message. The error message has %s or %d placeholders so that you can use the sprintf() function to format the error message with parameters like this:

sprintf(DEFAULT_VALIDATION_ERRORS[$rule_name], $field, ...$params);
Code language: PHP (php)

Here’s the validate() function with error messages:

function validate(array $data, array $fields): array { $errors = []; $split = fn($str, $separator) => array_map('trim', explode($separator, $str)); foreach ($fields as $field => $option) { $rules = $split($option, '|'); foreach ($rules as $rule) { $params = []; if (strpos($rule, ':')) { [$rule_name, $param_str] = $split($rule, ':'); $params = $split($param_str, ','); } else { $rule_name = trim($rule); } $fn = 'is_' . $rule_name; if (is_callable($fn)) { $pass = $fn($data, $field , ...$params); if (!$pass) { $errors[$field] = sprintf(DEFAULT_VALIDATION_ERRORS[$rule_name], $field, ...$params); } } } } return $errors; }
Code language: PHP (php)

Adding custom error messages

The validate() function uses the default error messages. However, sometimes, you need to pass our own custom error message.

For example, you may want to use a message like 'The username is required' for the required rule instead of using the default one.

To do that, you can add a third parameter to the validate() function to store the custom error messages. This parameter is an array of items whose key is field name and value is the error message. For example:

['required' => 'The %s is required']
Code language: PHP (php)

If you pass the custom error messages array to the validate() function, it’ll use the custom error message instead of the default one.

Here’s the validate() function with the custom error messages:

function validate(array $data, array $fields, array $messages = []): array { $split = fn($str, $separator) => array_map('trim', explode($separator, $str)); // set the validation messages $validation_errors = array_merge(DEFAULT_VALIDATION_ERRORS, $messages); $errors = []; foreach ($fields as $field => $option) { $rules = $split($option, '|'); foreach ($rules as $rule) { $params = []; if (strpos($rule, ':')) { [$rule_name, $paramStr] = $split($rule, ':'); $params = $split($paramStr, ','); } else { $rule_name = trim($rule); } $fn = 'is_' . $rule_name; if (is_callable($fn)) { $pass = $fn($data, $field, ...$params); if (!$pass) { $errors[$field] = sprintf($validation_errors[$rule_name], $field, ...$params); } } } } return $errors; }
Code language: PHP (php)

In the validate() function:

First, merge the default error messages with the custom error messages. The custom error message will overwrite the default error messages:

$validation_errors = array_merge(DEFAULT_VALIDATION_ERRORS, $messages);
Code language: PHP (php)

Second, get the error messages from the $validation_errors instead of the DEFAULT_VALIDATION_ERRORS array:

$errors[$field] = sprintf($validation_errors[$rule_name], $field, ...$params);
Code language: PHP (php)

Add a custom error message for each field and rule

So far, a custom error message is applied to a specific rule of all fields. For example:

['required' => 'The % is required']
Code language: CSS (css)

And you have two fields:

[ 'username' => 'required', 'password' => 'required' ]
Code language: PHP (php)

The custom error message will apply to both username and password fields.

Sometimes, you want to apply a custom error message for a specific field. For example, you want the required rule of the username to have a message like:

Please use your username to sign in
Code language: PHP (php)

And the custom error message for the password field is:

Use your password to sign in
Code language: PHP (php)

To do that, you need to enhance the validate() function:

function validate(array $data, array $fields, array $messages = []): array
Code language: PHP (php)

The $messages variable will look like the following to support a custom message for a specific rule and field:

[ 'required' => 'The %s is required', 'username' => ['required'=> 'Please use your username to sign in'] ]
Code language: PHP (php)

The first element will set the error message for the required rule of all fields:

'required' => 'The %s is required'
Code language: PHP (php)

And the second element will set the error message for the required rule of the username:

'username' => ['required'=> 'Please use your username to sign in']
Code language: PHP (php)

If the element’s value is a string, it is the error message for a rule of all fields. The key is the rule name, and the value is an error message.

However, if the element’s value is an array, the key is the field name, and the value is an array of rules and error messages.

The following returns an array that contains a list of rules and error messages:

$rule_messages = array_filter($messages, fn($message) => is_string($message));
Code language: PHP (php)

Once having this $rule_messages array, you can merge it with the default error messages:

$validation_errors = array_merge(DEFAULT_VALIDATION_ERRORS, $rule_messages);
Code language: PHP (php)

The following get the error message for a field and rule if it exists:

$message[$field][$rule]
Code language: PHP (php)

Otherwise, you can get the error message from the $validation_errors above:

$validation_errors[$rule_name]
Code language: PHP (php)

The following shows the validate() function with the new logic for custom error messages:

/** * Validate * @param array $data * @param array $fields * @param array $messages * @return array */ function validate(array $data, array $fields, array $messages = []): array { // Split the array by a separator, trim each element // and return the array $split = fn($str, $separator) => array_map('trim', explode($separator, $str)); // get the message rules $rule_messages = array_filter($messages, fn($message) => is_string($message)); // overwrite the default message $validation_errors = array_merge(DEFAULT_VALIDATION_ERRORS, $rule_messages); $errors = []; foreach ($fields as $field => $option) { $rules = $split($option, '|'); foreach ($rules as $rule) { // get rule name params $params = []; // if the rule has parameters e.g., min: 1 if (strpos($rule, ':')) { [$rule_name, $param_str] = $split($rule, ':'); $params = $split($param_str, ','); } else { $rule_name = trim($rule); } // by convention, the callback should be is_<rule> e.g.,is_required $fn = 'is_' . $rule_name; if (is_callable($fn)) { $pass = $fn($data, $field, ...$params); if (!$pass) { // get the error message for a specific field and rule if exists // otherwise get the error message from the $validation_errors $errors[$field] = sprintf( $messages[$field][$rule_name] ?? $validation_errors[$rule_name], $field, ...$params ); } } } } return $errors; }
Code language: PHP (php)

So far, you have completed the validate() function. Let’s define the validation function for each rule.

Validation functions

Each validation function must have the following signature:

function validation_function(array $data, string $field, ....$params) : bool
Code language: PHP (php)

required rule

The following is_required() function returns true if the value is set and not empty:

function is_required(array $data, string $field): bool { return isset($data[$field]) && trim($data[$field]) !== ''; }
Code language: PHP (php)

email rule

The following is_email() function returns true if the value is a valid email address:

function is_email(array $data, string $field): bool { if (empty($data[$field])) { return true; } return filter_var($data[$field], FILTER_VALIDATE_EMAIL); }
Code language: PHP (php)

If $data[$field] is not set or empty, the is_email() function also returns true .

min rule

The following is_min() function returns true if the field value’s length is greater than or equal to $min:

function is_min(array $data, string $field, int $min): bool { if (!isset($data[$field])) { return true; } return mb_strlen($data[$field]) >= $min; }
Code language: PHP (php)

max rule

The following is_max() function returns true if the field value length is less than or equal to $max:

function is_max(array $data, string $field, int $max): bool { if (!isset($data[$field])) { return true; } return mb_strlen($data[$field]) <= $max; }
Code language: PHP (php)

between rule

The following is_between() function returns true if the field value’s length is between $min and $max:

function is_between(array $data, string $field, int $min, int $max): bool { if (!isset($data[$field])) { return true; } $len = mb_strlen($data[$field]); return $len >= $min && $len <= $max; }
Code language: PHP (php)

alphanumeric rule

The is_alphanumeric() returns true if the field value contains only letters or characters:

function is_alphanumeric(array $data, string $field): bool { if (!isset($data[$field])) { return true; } return ctype_alnum($data[$field]); }
Code language: PHP (php)

same rule

The is_same() function returns true if the field value is the same as the other field’s value:

function is_same(array $data, string $field, string $other): bool { if (isset($data[$field], $data[$other])) { return $data[$field] === $data[$other]; } if (!isset($data[$field]) && !isset($data[$other])) { return true; } return false; }
Code language: PHP (php)

secure rule

The is_secure() returns true if the field value must have between 8 and 64 characters and contain at least one number, one upper case letter, one lower case letter, and one special character:

function is_secure(array $data, string $field): bool { if (!isset($data[$field])) { return false; } $pattern = "#.*^(?=.{8,64})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$#"; return preg_match($pattern, $data[$field]); }
Code language: PHP (php)

unique rule

For the unique rule, you need a database connection and select the value from the $column of a $table specified in the rule’s parameters.

First, create a new file called database.php in the config folder and add the following constants:

<?php const DB_HOST = 'localhost'; const DB_NAME = 'auth'; const DB_USER = 'root'; const DB_PASSWORD = '';
Code language: PHP (php)

Second, load the config/database.php and define a function to connect to the database:

function db() { static $pdo; if (!$pdo) { $pdo = connect(DB_HOST, DB_NAME, DB_USER, DB_PASSWORD); } return $pdo; }
Code language: PHP (php)

The following is_unique() function returns true if the value is unique is the $column of the $table:

function is_unique(array $data, string $field, string $table, string $column): bool { if (!isset($data[$field])) { return true; } $sql = "SELECT $column FROM $table WHERE $column = :value"; $stmt = db()->prepare($sql); $stmt->bindValue(":value", $data[$field]); $stmt->execute(); return $stmt->fetchColumn() === false; }
Code language: PHP (php)

Putting it all together

The following shows all functions in the validation library:

<?php require __DIR__ . '/../config/database.php'; const DEFAULT_VALIDATION_ERRORS = [ 'required' => 'Please enter the %s', 'email' => 'The %s is not a valid email address', 'min' => 'The %s must have at least %s characters', 'max' => 'The %s must have at most %s characters', 'between' => 'The %s must have between %d and %d characters', 'same' => 'The %s must match with %s', 'alphanumeric' => 'The %s should have only letters and numbers', 'secure' => 'The %s must have between 8 and 64 characters and contain at least one number, one upper case letter, one lower case letter and one special character', 'unique' => 'The %s already exists', ]; /** * Validate * @param array $data * @param array $fields * @param array $messages * @return array */ function validate(array $data, array $fields, array $messages = []): array { // Split the array by a separator, trim each element // and return the array $split = fn($str, $separator) => array_map('trim', explode($separator, $str)); // get the message rules $rule_messages = array_filter($messages, fn($message) => is_string($message)); // overwrite the default message $validation_errors = array_merge(DEFAULT_VALIDATION_ERRORS, $rule_messages); $errors = []; foreach ($fields as $field => $option) { $rules = $split($option, '|'); foreach ($rules as $rule) { // get rule name params $params = []; // if the rule has parameters e.g., min: 1 if (strpos($rule, ':')) { [$rule_name, $param_str] = $split($rule, ':'); $params = $split($param_str, ','); } else { $rule_name = trim($rule); } // by convention, the callback should be is_<rule> e.g.,is_required $fn = 'is_' . $rule_name; if (is_callable($fn)) { $pass = $fn($data, $field, ...$params); if (!$pass) { // get the error message for a specific field and rule if exists // otherwise get the error message from the $validation_errors $errors[$field] = sprintf( $messages[$field][$rule_name] ?? $validation_errors[$rule_name], $field, ...$params ); } } } } return $errors; } /** * Return true if a string is not empty * @param array $data * @param string $field * @return bool */ function is_required(array $data, string $field): bool { return isset($data[$field]) && trim($data[$field]) !== ''; } /** * Return true if the value is a valid email * @param array $data * @param string $field * @return bool */ function is_email(array $data, string $field): bool { if (empty($data[$field])) { return true; } return filter_var($data[$field], FILTER_VALIDATE_EMAIL); } /** * Return true if a string has at least min length * @param array $data * @param string $field * @param int $min * @return bool */ function is_min(array $data, string $field, int $min): bool { if (!isset($data[$field])) { return true; } return mb_strlen($data[$field]) >= $min; } /** * Return true if a string cannot exceed max length * @param array $data * @param string $field * @param int $max * @return bool */ function is_max(array $data, string $field, int $max): bool { if (!isset($data[$field])) { return true; } return mb_strlen($data[$field]) <= $max; } /** * @param array $data * @param string $field * @param int $min * @param int $max * @return bool */ function is_between(array $data, string $field, int $min, int $max): bool { if (!isset($data[$field])) { return true; } $len = mb_strlen($data[$field]); return $len >= $min && $len <= $max; } /** * Return true if a string equals the other * @param array $data * @param string $field * @param string $other * @return bool */ function is_same(array $data, string $field, string $other): bool { if (isset($data[$field], $data[$other])) { return $data[$field] === $data[$other]; } if (!isset($data[$field]) && !isset($data[$other])) { return true; } return false; } /** * Return true if a string is alphanumeric * @param array $data * @param string $field * @return bool */ function is_alphanumeric(array $data, string $field): bool { if (!isset($data[$field])) { return true; } return ctype_alnum($data[$field]); } /** * Return true if a password is secure * @param array $data * @param string $field * @return bool */ function is_secure(array $data, string $field): bool { if (!isset($data[$field])) { return false; } $pattern = "#.*^(?=.{8,64})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*\W).*$#"; return preg_match($pattern, $data[$field]); } /** * Connect to the database and returns an instance of PDO class * or false if the connection fails * * @return PDO */ function db(): PDO { static $pdo; // if the connection is not initialized // connect to the database if (!$pdo) { return new PDO( sprintf("mysql:host=%s;dbname=%s;charset=UTF8", DB_HOST, DB_NAME), DB_USER, DB_PASSWORD, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] ); } return $pdo; } /** * Return true if the $value is unique in the column of a table * @param array $data * @param string $field * @param string $table * @param string $column * @return bool */ function is_unique(array $data, string $field, string $table, string $column): bool { if (!isset($data[$field])) { return true; } $sql = "SELECT $column FROM $table WHERE $column = :value"; $stmt = db()->prepare($sql); $stmt->bindValue(":value", $data[$field]); $stmt->execute(); return $stmt->fetchColumn() === false; }
Code language: PHP (php)

Testing the validation library

To test the unique rule, you need to create a new database and a table with the username field, for example.

First, create a new database called auth:

CREATE DATABASE auth;
Code language: SQL (Structured Query Language) (sql)

Second, create a new users table:

CREATE TABLE users( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(300) NOT NULL UNIQUE );
Code language: SQL (Structured Query Language) (sql)

Note that for the testing purpose, we don’t need to have the password field.

Third, insert a row into the users table:

INSERT INTO users(username, email) VALUES('bob', 'bob@phptutorial.net');
Code language: SQL (Structured Query Language) (sql)

The following example uses the validate() function to run a test:

<?php require __DIR__ . '/validation.php'; $data = [ 'firstname' => '', 'username' => 'bob', 'address' => 'This is my address', 'zipcode' => '999', 'email' => 'jo@', 'password' => 'test123', 'password2' => 'test', ]; $fields = [ 'firstname' => 'required, max:255', 'lastname' => 'required, max: 255', 'address' => 'required | min: 10, max:255', 'zipcode' => 'between: 5,6', 'username' => 'required | alphanumeric | between: 3,255 | unique: users,username', 'email' => 'required | email | unique: users,email', 'password' => 'required | secure', 'password2' => 'required | same:password' ]; $errors = validate($data, $fields, [ 'required' => 'The %s is required', 'password2' => ['same'=> 'Please enter the same password again']] ); print_r($errors);
Code language: PHP (php)

Output:

Array ( [firstname] => The firstname is required [lastname] => The lastname is required [zipcode] => The zipcode must have between 5 and 6 characters [username] => The username already exists [email] => The email is not a valid email address [password] => The password must have between 8 and 64 characters and contain at least one number, one upper case letter, one lower case letter and o ne special character [password2] => Please enter the same password again )
Code language: PHP (php)

Adding a new validation rule

To add a new rule, you need to do two things:

First, add a default error message to the DEFAULT_VALIDATION_ERRORS array.

const DEFAULT_VALIDATION_ERRORS = [ // ... new_rule => "Error message for the new rule" ];
Code language: JavaScript (javascript)

Second, define a validation function with the following signature:

function is_new_rule(array $data, string $field, ....$params) : bool
Code language: PHP (php)

In this tutorial, you have learned how to build a reusable validation library in PHP.

Did you find this tutorial useful?