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â
| Rule | Checks |
|---|---|
required | Present and not empty |
nullable | Allows an empty value; skips the remaining rules |
present | Key exists, may be empty |
accepted | "yes", "on", 1, true |
string, integer, numeric, boolean | Type |
array, object, json | Structure |
email, url, date | Format |
ip, ip4, ip6, mac_address | Network format |
lowercase, uppercase | Casing |
digits | Digits only |
min, max, size, between | Length or magnitude |
digit_between | Digit count within a range |
same | Matches 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()
| Sanitizer | WordPress function |
|---|---|
sanitize:text | sanitize_text_field() |
sanitize:textarea | sanitize_textarea_field() |
sanitize:email | sanitize_email() |
sanitize:url | esc_url_raw() |
sanitize:key | sanitize_key() |
sanitize:title | sanitize_title() |
sanitize:user | sanitize_user() |
sanitize:file_name | sanitize_file_name() |
sanitize:html_class | sanitize_html_class() |
sanitize:wpkses_post | wp_kses_post() |
sanitize:wpkses | wp_kses() with the allowed tags you pass |
sanitize:escape | esc_html() |
sanitize:trim, sanitize:capitalize, sanitize:lowercase, sanitize:uppercase, sanitize:ucfirst | String shaping |
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.