Passer au contenu principal

Qu’est-ce qu’une règle de validation personnalisée

Laravel dispose d’un large éventail de règles de validation intégrées, mais parfois vous avez besoin d’une logique de validation spécifique à votre application. Les règles personnalisées vous permettent de définir une logique de validation réutilisable sous forme de classes ou de closures, utilisables comme les règles standards. Il existe principalement deux façons de définir des règles personnalisées :
  • Objet règle — très réutilisable et facile à tester
  • Closure — adapté aux règles simples utilisées une seule fois

Objet règle

Générer une classe de règle

La commande Artisan make:rule génère une nouvelle classe de règle. Elle est placée dans le répertoire app/Rules.
php artisan make:rule Uppercase

Implémenter l’interface ValidationRule

Implémentez la méthode validate dans la classe générée. Cette méthode appelle la closure $fail en cas d’échec de la validation.
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class Uppercase implements ValidationRule
{
    /**
     * Exécute la règle de validation
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (strtoupper($value) !== $value) {
            $fail('The :attribute must be uppercase.');
        }
    }
}
La chaîne passée à la closure $fail peut contenir le placeholder :attribute, que Laravel remplace par le nom du champ.

Appliquer l’objet règle

Passez une instance de l’objet règle dans le tableau de validation.
use App\Rules\Uppercase;

$request->validate([
    'name' => ['required', 'string', new Uppercase],
]);
La méthode rules() d’un form request s’utilise de la même façon.
public function rules(): array
{
    return [
        'name' => ['required', 'string', new Uppercase],
    ];
}

Messages d’erreur via des clés de traduction

Plutôt que de coder en dur le message d’erreur, vous pouvez utiliser une clé de traduction.
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    if (strtoupper($value) !== $value) {
        $fail('validation.uppercase')->translate();
    }
}
Ajoutez le message dans le fichier de traduction lang/fr/validation.php.
return [
    'uppercase' => 'Le champ :attribute doit être en majuscules.',
    // ...
];

Ajouter plusieurs messages d’erreur

Pour reporter plusieurs erreurs sur un même champ, appelez $fail plusieurs fois.
public function validate(string $attribute, mixed $value, Closure $fail): void
{
    if (! is_string($value)) {
        $fail('The :attribute must be a string.');
        return;
    }

    if (strlen($value) < 8) {
        $fail('The :attribute must be at least 8 characters.');
    }

    if (! preg_match('/[A-Z]/', $value)) {
        $fail('The :attribute must contain at least one uppercase letter.');
    }
}

Règles via closure

Les règles simples utilisées une seule fois dans l’application peuvent être définies via une closure, sans créer de classe.
use Illuminate\Support\Facades\Validator;
use Closure;

$validator = Validator::make($request->all(), [
    'title' => [
        'required',
        'max:255',
        function (string $attribute, mixed $value, Closure $fail) {
            if ($value === 'foo') {
                $fail("The {$attribute} is invalid.");
            }
        },
    ],
]);
Les règles closure sont pratiques car définies inline, mais elles sont difficiles à réutiliser et à tester de façon isolée. Pour une logique utilisée à plusieurs endroits, préférez extraire dans un objet règle.

Règles implicites (exécutées même si la valeur est vide)

Par défaut, si un champ est vide ou absent, les règles personnalisées ne sont pas exécutées. Pour exécuter la règle même sur les valeurs vides, générez la classe avec l’option --implicit.
php artisan make:rule Uppercase --implicit
La classe générée implémente l’interface ImplicitRule. Cette interface elle-même n’ajoute pas de méthode ; elle sert de signal à Laravel.
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ImplicitRule;
use Illuminate\Contracts\Validation\ValidationRule;

class RequiredIfJapanese implements ValidationRule, ImplicitRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        if (empty($value)) {
            $fail('The :attribute is required.');
            return;
        }

        if (! preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー]+$/u', $value)) {
            $fail('The :attribute must contain only Japanese characters.');
        }
    }
}
ImplicitRule indique simplement à Laravel que « l’attribut est requis ». C’est à l’implémentation de validate de faire réellement échouer la validation lorsque la valeur est vide.

Accès aux données

DataAwareRule — accéder à toutes les données du formulaire

Pour valider en fonction des valeurs d’autres champs, implémentez l’interface DataAwareRule. La méthode setData est appelée automatiquement avant le démarrage de la validation.
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\DB;

class UniqueForTenant implements DataAwareRule, ValidationRule
{
    /**
     * Toutes les données à valider
     *
     * @var array<string, mixed>
     */
    protected array $data = [];

    public function __construct(
        protected string $table,
        protected string $column = 'value',
    ) {}

    /**
     * Définit les données de validation
     *
     * @param  array<string, mixed>  $data
     */
    public function setData(array $data): static
    {
        $this->data = $data;

        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $tenantId = $this->data['tenant_id'] ?? null;

        $exists = DB::table($this->table)
            ->where('tenant_id', $tenantId)
            ->where($this->column, $value)
            ->exists();

        if ($exists) {
            $fail('The :attribute has already been taken for this tenant.');
        }
    }
}
Exemple d’utilisation :
$request->validate([
    'tenant_id' => 'required|integer',
    'email' => ['required', 'email', new UniqueForTenant('users', 'email')],
]);

ValidatorAwareRule — accéder à l’instance du validateur

Pour accéder à toutes les informations du validateur (règles échouées, messages personnalisés, etc.), implémentez l’interface ValidatorAwareRule.
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Contracts\Validation\ValidatorAwareRule;
use Illuminate\Validation\Validator;

class ConditionalFormat implements ValidationRule, ValidatorAwareRule
{
    protected Validator $validator;

    public function setValidator(Validator $validator): static
    {
        $this->validator = $validator;

        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // Sauter si un autre champ a déjà échoué à la validation
        if ($this->validator->errors()->has('type')) {
            return;
        }

        $type = $this->validator->getData()['type'] ?? null;

        if ($type === 'phone' && ! preg_match('/^\+?[0-9\-\s]+$/', $value)) {
            $fail('The :attribute must be a valid phone number.');
        }

        if ($type === 'email' && ! filter_var($value, FILTER_VALIDATE_EMAIL)) {
            $fail('The :attribute must be a valid email address.');
        }
    }
}

Cas d’usage pratiques

Vérification de caractères spécifiques

Une règle qui valide un jeu de caractères précis.
php artisan make:rule JapaneseOnly
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class JapaneseOnly implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // Autorise uniquement hiragana, katakana, kanji et le tiret long
        if (! preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー\s]+$/u', $value)) {
            $fail('Le champ :attribute doit contenir uniquement des caractères japonais (hiragana, katakana, kanji).');
        }
    }
}
$request->validate([
    'name_kana' => ['required', 'string', new JapaneseOnly],
]);

Vérification d’un format de numéro de téléphone

Une règle validant le format d’un numéro de téléphone japonais.
php artisan make:rule JapanesePhone
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class JapanesePhone implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // Prend en charge avec/sans tirets, et le format international
        $normalized = preg_replace('/[\s\-\(\)]/', '', $value);

        $patterns = [
            '/^0\d{9,10}$/',        // Numéros fixes et mobiles courants
            '/^\+81\d{9,10}$/',     // Format international
        ];

        foreach ($patterns as $pattern) {
            if (preg_match($pattern, $normalized)) {
                return;
            }
        }

        $fail('Le champ :attribute doit être au format d\'un numéro de téléphone valide.');
    }
}

Contrainte d’unicité avec tenant ID

Une contrainte d’unicité au sein du scope d’un tenant, courante dans les applications multi-tenant.
1

Créer la classe de règle

php artisan make:rule TenantUnique
<?php

namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\DataAwareRule;
use Illuminate\Contracts\Validation\ValidationRule;
use Illuminate\Support\Facades\DB;

class TenantUnique implements DataAwareRule, ValidationRule
{
    protected array $data = [];

    public function __construct(
        protected string $table,
        protected string $column,
        protected ?int $ignoreId = null,
    ) {}

    public function setData(array $data): static
    {
        $this->data = $data;

        return $this;
    }

    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $tenantId = $this->data['tenant_id'] ?? null;

        $query = DB::table($this->table)
            ->where('tenant_id', $tenantId)
            ->where($this->column, $value);

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

        if ($query->exists()) {
            $fail('Le champ :attribute est déjà utilisé.');
        }
    }
}
2

Utiliser dans une Form Request

<?php

namespace App\Http\Requests;

use App\Rules\TenantUnique;
use Illuminate\Foundation\Http\FormRequest;

class CreateProjectRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'tenant_id' => 'required|integer|exists:tenants,id',
            'name' => [
                'required',
                'string',
                'max:100',
                new TenantUnique('projects', 'name'),
            ],
        ];
    }

    public function authorize(): bool
    {
        return true;
    }
}
3

Exclure l'ID lors de la mise à jour

Lors de la mise à jour d’un enregistrement existant, excluez son propre ID de la vérification de doublon.
class UpdateProjectRequest extends FormRequest
{
    public function rules(): array
    {
        $projectId = $this->route('project');

        return [
            'tenant_id' => 'required|integer|exists:tenants,id',
            'name' => [
                'required',
                'string',
                'max:100',
                new TenantUnique('projects', 'name', ignoreId: $projectId),
            ],
        ];
    }
}

Enregistrement de règles dans un service provider

Ajouter une règle avec Validator::extend()

Validator::extend() permet d’utiliser une règle personnalisée sous forme de chaîne ('rule_name'). Enregistrez-la dans la méthode boot() de AppServiceProvider.
<?php

namespace App\Providers;

use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Validator as ValidatorInstance;

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Validator::extend('japanese_only', function (string $attribute, mixed $value, array $parameters, ValidatorInstance $validator): bool {
            return preg_match('/^[\p{Hiragana}\p{Katakana}\p{Han}ー\s]+$/u', $value) === 1;
        });

        Validator::replacer('japanese_only', function (string $message, string $attribute): string {
            return str_replace(':attribute', $attribute, 'Le champ :attribute doit être en japonais.');
        });
    }
}
Les règles enregistrées avec Validator::extend() peuvent être spécifiées comme chaîne.
$request->validate([
    'name_kana' => 'required|japanese_only',
]);
Validator::extend() est une méthode d’enregistrement plus ancienne que les objets règle. Pour les nouveaux développements, il est recommandé d’utiliser des objets règle implémentant l’interface ValidationRule.

Ajouter comme méthode statique de la classe Rule

En ajoutant une macro à la façade Rule, vous pouvez fournir une API fluide comme Rule::myRule().
use Illuminate\Validation\Rule;

Rule::macro('tenantUnique', function (string $table, string $column, ?int $ignoreId = null) {
    return new \App\Rules\TenantUnique($table, $column, $ignoreId);
});
// Exemple d'utilisation
$request->validate([
    'name' => ['required', Rule::tenantUnique('projects', 'name')],
]);

Détails de l’implémentation interne

Voyons comment Illuminate\Validation\Validator invoque les règles personnalisées. Au sein du validateur, validateAttribute() traite chaque champ. Si la règle implémente l’interface ValidationRule, la méthode validateUsingCustomRule() est appelée.
// Version simplifiée de Illuminate\Validation\Validator::validateUsingCustomRule()
protected function validateUsingCustomRule($attribute, $value, $rule)
{
    // Si DataAwareRule, injecter toutes les données
    if ($rule instanceof DataAwareRule) {
        $rule->setData($this->getData());
    }

    // Si ValidatorAwareRule, injecter le validateur lui-même
    if ($rule instanceof ValidatorAwareRule) {
        $rule->setValidator($this);
    }

    // Appeler validate()
    $rule->validate($attribute, $value, function ($message, $translate = false) use ($attribute, $rule) {
        // Closure $fail — ajoute le message d'erreur
        $this->errors()->add($attribute, $this->makeReplacements(
            $message,
            $attribute,
            get_class($rule),
            [], // Remplacements de placeholders supplémentaires (ex : ['min' => '8'])
        ));
    });
}
Le traitement de ImplicitRule est déterminé par la méthode isImplicit(), qui exécute la vérification même sur les valeurs vides.
// Détermine si la règle doit être exécutée sur les valeurs vides
protected function isImplicit($rule): bool
{
    return $rule instanceof ImplicitRule
        || in_array($rule, $this->implicitRules);
}
Vous pouvez implémenter DataAwareRule et ValidatorAwareRule simultanément. Dans une classe qui implémente les deux interfaces, les deux injections sont effectuées.

Pages associées

Validation (introduction)

Consultez la méthode standard de validation dans les contrôleurs et les form requests.
Dernière modification le 13 juillet 2026