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.

Pourquoi River\Validator ? Le Validator remplace la validation manuelle dispersée dans chaque méthode de controller. Il centralise les règles, élimine le mapping manuel 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
Règle du passe-si-vide : 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é')
Sécurité : Les paramètres $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)