Validator
Validez les entrées utilisateur de façon fluente. Le Validator retourne à la fois les messages d'erreur et les champs en erreur — indispensable pour l'affichage inline des formulaires.
erreur → champ, et protège par défaut contre l'injection de tableaux HTTP.
Instanciation
Passez un tableau de données (typiquement $this->request->all_post()) à la factory statique :
use River\Validator;
$v = Validator::make($this->request->all_post())
->required('name', 'Le nom est requis')
->email('email', 'Email invalide')
->unique('email', $this->db, 'users', exclude_id: $id, msg: 'Email déjà utilisé');
if ($v->fails()) {
// traiter les erreurs
}
Règles disponibles
| Règle | Signature | Comportement si vide |
|---|---|---|
required |
required(field, msg) |
Échoue — c'est son rôle |
email |
email(field, msg) |
Passe silencieusement |
min_length |
min_length(field, int, msg) |
Passe silencieusement |
max_length |
max_length(field, int, msg) |
Passe silencieusement |
unique |
unique(field, db, table, exclude_id, msg) |
Passe silencieusement |
custom |
custom(field, callable, msg) |
Dépend de la closure |
email, min_length, max_length et unique ne s'appliquent que si le champ contient une valeur.
Pour rendre un champ obligatoire, chaînez toujours avec required() en premier.
Détail des règles
required()
Échoue si la valeur est null, une chaîne vide, ou une chaîne de blancs.
->required('name', 'Le nom est requis')
email()
Valide le format via filter_var(..., FILTER_VALIDATE_EMAIL). Passe si le champ est vide.
->required('email', 'Email requis')
->email('email', 'Format email invalide')
min_length() / max_length()
Longueur en caractères multibyte (mb_strlen). Le message par défaut est généré automatiquement si omis.
->min_length('name', 2, 'Minimum 2 caractères')
->max_length('name', 50) // Message par défaut : "Maximum 50 caractères autorisés"
->max_length('bio', 200, 'Bio trop longue')
unique()
Vérifie l'unicité d'une valeur en base de données. Le paramètre exclude_id permet d'exclure l'enregistrement courant lors d'un UPDATE.
// CREATE — pas d'exclusion
->unique('email', $this->db, 'users', msg: 'Email déjà utilisé')
// UPDATE — exclure l'id courant
->unique('email', $this->db, 'users', exclude_id: $id, msg: 'Email déjà utilisé')
$table et $field ne doivent jamais venir de l'input utilisateur.
River valide automatiquement qu'ils ne contiennent que des caractères alphanumériques et underscores — toute valeur suspecte lève une InvalidArgumentException.
custom()
Règle libre via une closure qui reçoit la valeur brute du champ et retourne true (valide) ou false (invalide).
// Âge optionnel entre 0 et 150
->custom('age',
fn($v) => $v === null || $v === '' || (is_numeric($v) && (int)$v >= 0 && (int)$v <= 150),
'L\'âge doit être compris entre 0 et 150'
)
// Regex personnalisée
->custom('slug',
fn($v) => $v === null || preg_match('/^[a-z0-9-]+$/', (string)$v),
'Slug invalide (lettres minuscules, chiffres et tirets uniquement)'
)
Lire les résultats
if ($v->fails()) {
// Tableau plat de tous les messages
$v->errors();
// → ['Le nom est requis', 'Email invalide']
// Liste des champs en erreur (dédupliqués)
$v->error_fields();
// → ['name', 'email']
// Erreurs groupées par champ
$v->errors_by_field();
// → ['name' => ['Le nom est requis'], 'email' => ['Email invalide']]
}
Les trois méthodes de résultat se déduisent l'une de l'autre. errors_by_field() est la source de vérité interne.
Exemple complet — Controller DataTable
Avant le Validator, la validation était dispersée dans 4 méthodes (store, dt_update_from_modal, dt_update_from_page, dt_update_from_row) avec un mapping manuel erreur → champ.
// Avant — mapping manuel dans dt_update_from_row()
$errors = $this->validate_user($name, $email);
$error_fields = [];
if (in_array('Le nom est requis', $errors)) $error_fields[] = 'name';
if (in_array('Email invalide', $errors)) $error_fields[] = 'email';
if (in_array('Cet email est déjà utilisé', $errors)) $error_fields[] = 'email';
// Après — Validator
$v = Validator::make($this->request->all_post())
->required('name', 'Le nom est requis')
->min_length('name', 2)
->email('email', 'Email invalide')
->unique('email', $this->db, 'users', exclude_id: $id, msg: 'Cet email est déjà utilisé');
if ($v->fails()) {
return $this->with_toast(
Toast::danger(implode(' — ', $v->errors())),
$this->partial('admin/users/_row_edit', [
'errors' => $v->errors(),
'error_fields' => $v->error_fields(), // pour surligner les inputs en erreur
'focus' => $v->error_fields()[0] ?? '',
])
);
}
Sécurité
Protection contre l'injection de tableaux HTTP
PHP permet de soumettre name[]=foo en POST, ce qui crée un tableau dans $_POST['name']. Sans protection, (string)['foo'] = "Array" — ce qui ferait passer required() avec une valeur non-scalaire.
Le Validator rejette automatiquement les arrays : toute valeur non-scalaire est traitée comme null (champ absent). required() échoue, les autres règles passent silencieusement.
Protection is_numeric() dans les règles custom()
Ne jamais caster directement en int sans vérifier is_numeric() : (int)'abc' vaut 0 en PHP, ce qui ferait passer une règle >= 0 avec n'importe quelle lettre.
// Mauvais — bypass possible avec une lettre
fn($v) => (int)$v >= 0
// Correct
fn($v) => $v === null || $v === '' || (is_numeric($v) && (int)$v >= 0)