Skip to main content

Validation

Introduction​

Validation comes from bitapps/wp-validator. You never construct the validator yourself — call validate() on the request with an array of rules per field:

$validated = $request->validate([
'title' => ['required', 'string', 'sanitize:text'],
'email' => ['required', 'email'],
'age' => ['nullable', 'integer'],
]);

It returns only the fields that passed, already sanitized. On failure it answers with a 422 and stops the request:

{
"status": "error",
"code": "VALIDATION",
"data": { "email": ["The email must be a valid email address"] }
}

Available rules​

RuleChecks
requiredPresent and not empty
nullableAllows an empty value; skips the remaining rules
presentKey exists, may be empty
accepted"yes", "on", 1, true
string, integer, numeric, booleanType
array, object, jsonStructure
email, url, dateFormat
ip, ip4, ip6, mac_addressNetwork format
lowercase, uppercaseCasing
digitsDigits only
min, max, size, betweenLength or magnitude
digit_betweenDigit count within a range
sameMatches another field

Rules taking parameters use a colon, with commas between multiple values:

$request->validate([
'title' => ['required', 'max:255'],
'age' => ['integer', 'between:18,65'],
'password' => ['required', 'min:8'],
'confirm' => ['required', 'same:password'],
]);

A rule name maps to a class by studly-casing it, so digit_between resolves to DigitBetweenRule. An unknown name throws RuleErrorException rather than passing silently.

Sanitizing​

sanitize: rules transform the value instead of testing it. The sanitized value is what validate() returns:

'title' => ['required', 'sanitize:text'], // sanitize_text_field()
'email' => ['required', 'sanitize:email'], // sanitize_email()
'slug' => ['required', 'sanitize:title'], // sanitize_title()
'website' => ['nullable', 'sanitize:url'], // esc_url_raw()
SanitizerWordPress function
sanitize:textsanitize_text_field()
sanitize:textareasanitize_textarea_field()
sanitize:emailsanitize_email()
sanitize:urlesc_url_raw()
sanitize:keysanitize_key()
sanitize:titlesanitize_title()
sanitize:usersanitize_user()
sanitize:file_namesanitize_file_name()
sanitize:html_classsanitize_html_class()
sanitize:wpkses_postwp_kses_post()
sanitize:wpkseswp_kses() with the allowed tags you pass
sanitize:escapeesc_html()
sanitize:trim, sanitize:capitalize, sanitize:lowercase, sanitize:uppercase, sanitize:ucfirstString shaping
warning

Validation rules test; they do not clean. ['required', 'string'] returns whatever the client sent. Pair a sanitize: rule with every field you persist or echo.

Nested fields​

Dot notation reaches into arrays, and * applies a rule to every element:

$request->validate([
'auth_details' => ['required', 'array'],
'auth_details.token' => ['required', 'string', 'sanitize:text'],
'nodes.*.id' => ['required', 'integer'],
]);

Custom messages and labels​

The second argument overrides messages, keyed field.rule; the third renames the field in the default messages:

$request->validate(
['title' => ['required', 'max:255']],
['title.required' => 'Give the tag a name.'],
['title' => 'Tag name']
);

Built-in messages interpolate :attribute and the rule's parameters, e.g. The :attribute may not be greater than :max characters.

Custom rules​

For checks the built-ins don't cover, extend Rule in backend/app/Rules and implement validate() and message():

namespace YourPlugin\Rules;

if (!defined('ABSPATH')) {
exit;
}

use YourPlugin\Deps\BitApps\WPValidator\Rule;

class UniqueRule extends Rule
{
private $message = 'The :attribute must be unique.';

private $model;

private $column;

private $ignoreId;

public function __construct($model, $column, $customMessage = null)
{
$this->model = $model;
$this->column = $column;

if ($customMessage) {
$this->message = $customMessage;
}
}

public function ignore($id)
{
$this->ignoreId = $id;

return $this;
}

public function validate($value)
{
$query = $this->model::where($this->column, sanitize_text_field($value));

if ($this->ignoreId) {
$query->where('id', '!=', $this->ignoreId);
}

return !$query->first();
}

public function message()
{
return $this->message;
}
}

Pass the instance in the rules array alongside string rules:

// create
'title' => ['required', 'sanitize:text', new UniqueRule(Tag::class, 'title')],

// update, excluding the row being edited
'title' => ['required', 'sanitize:text', (new UniqueRule(Tag::class, 'title'))->ignore($request->id)],

A rule class can read the other fields under validation through getInputDataContainer(), which is how cross-field checks like same are implemented.