Module 4 – Formulaires avancés et validation
Niveau 5.1 – Laravel Livewire
Validation côté serveur (#[Rule], validate(), validateOnly()), affichage des erreurs dans la vue, formulaire d’édition (données pré-remplies) et upload de fichiers. En fin de module : validation conditionnelle, gestion des fichiers temporaires, intégration avec les policies.
Objectif
À la fin de ce module, vous saurez :
- Déclarer des règles de validation sur les propriétés d’un composant avec les attributs #[Rule] (Livewire 3) ou la méthode rules().
- Valider à la soumission avec $this->validate() et afficher les erreurs dans la vue avec @error et
$message. - Mettre en place une validation temps réel (à la sortie d’un champ ou à chaque modification) avec updated() et validateOnly($property).
- Gérer l’upload de fichiers avec le trait WithFileUploads, wire:model sur un input file, et les règles image, max, mimes.
Nous prenons l’exemple d’un formulaire d’inscription utilisateur (nom, email, mot de passe) et d’un upload de photo. Chaque notion est détaillée avec le flux (qui fait quoi), des exemples complets (classe + vue Blade entière), et les pièges à éviter.
Flux formulaire avec Livewire
Pour bien comprendre « qui fait quoi » quand l’utilisateur soumet un formulaire :
- L’utilisateur remplit les champs (liés avec wire:model aux propriétés du composant). À chaque modification (ou au blur / au submit selon le modificateur), Livewire peut envoyer la valeur au serveur et updated() est appelé si besoin.
- À la soumission (bouton ou Enter dans un champ), wire:submit="register" déclenche une requête AJAX : l’état complet du composant (toutes les propriétés publiques) est envoyé à Laravel, ainsi que le nom de l’action (register).
- Laravel réinstancie le composant, met à jour les propriétés avec les valeurs reçues, puis appelle register(). Dans register() vous appelez $this->validate().
- Si la validation échoue : Laravel lève une ValidationException. Livewire la capture, renvoie les erreurs au client, et le composant se re-rend avec $errors rempli. Dans la vue, @error('champ') affiche le message à côté du champ.
- Si la validation réussit : vous exécutez la logique (User::create, redirection, session()->flash('message', ...)). Le composant se re-rend ; l’utilisateur voit le message de succès ou une nouvelle page.
Aucun rechargement complet : tout se fait en AJAX. Le CSRF est géré automatiquement par Livewire. Pour désactiver le bouton pendant l’envoi, utilisez wire:loading (module 5).
Validation côté serveur
En Livewire, la validation s’appuie sur le moteur de validation Laravel (mêmes règles que dans les Form Request). Vous pouvez :
- Déclarer les règles sur les propriétés avec l’attribut #[Rule('required|string|min:3')] (Livewire 3).
- Appeler $this->validate() (ou $this->validateOnly($property)) dans une méthode (ex. save(), register()). Si la validation échoue, Laravel lève une ValidationException ; Livewire attrape l’exception, renvoie les erreurs au client, et le composant se re-rend avec les messages d’erreur accessibles dans la vue via @error('nomDuChamp').
Les messages d’erreur sont disponibles dans la vue comme en Blade classique : $errors et @error('champ').
Attribut #[Rule] et validate()
Exemple : formulaire d’inscription
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\Attributes\Rule;
class RegisterUser extends Component
{
#[Rule('required|string|min:3')]
public string $name = '';
#[Rule('required|email')]
public string $email = '';
#[Rule('required|string|min:8|confirmed')]
public string $password = '';
public string $password_confirmation = '';
public function register(): void
{
$this->validate();
// User::create([...]);
session()->flash('message', 'Compte créé avec succès.');
}
public function render()
{
return view('livewire.register-user');
}
}
- #[Rule('required|string|min:3')] : la propriété $name doit être présente, de type string, et d’au moins 3 caractères. Idem pour $email (required, format email) et $password (min 8, avec confirmation).
- $this->validate() : valide toutes les propriétés qui ont une règle (ou celles passées en argument). En cas d’échec, l’exception est gérée par Livewire ; les erreurs sont renvoyées et affichées dans la vue.
- password_confirmation : pour la règle confirmed, Laravel attend une propriété (ou un champ) password_confirmation. Pensez à lier un second champ dans la vue avec wire:model="password_confirmation".
Vue Blade complète (resources/views/livewire/register-user.blade.php) – avec labels, structure et feedback :
<div class="max-w-md mx-auto p-6 bg-white rounded-lg shadow">
<h2 class="text-xl font-semibold mb-4">Créer un compte</h2>
@if (session('message'))
<p class="mb-4 p-3 bg-green-100 text-green-800 rounded">{{ session('message') }}</p>
@endif
<form wire:submit="register" class="space-y-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Nom</label>
<input type="text" id="name" wire:model="name"
class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('name')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700">Email</label>
<input type="email" id="email" wire:model="email"
class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('email')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700">Mot de passe</label>
<input type="password" id="password" wire:model="password"
class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
@error('password')
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="password_confirmation" class="block text-sm font-medium text-gray-700">Confirmer le mot de passe</label>
<input type="password" id="password_confirmation" wire:model="password_confirmation"
class="mt-1 block w-full rounded border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
</div>
<button type="submit"
class="w-full rounded bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500">
S’inscrire
</button>
</form>
</div>
- Chaque champ a un label (accessibilité et UX). Les @error('champ') affichent le message Laravel juste sous le champ concerné.
- wire:model sur chaque input : les valeurs sont synchronisées avec les propriétés du composant ; à la soumission, register() reçoit déjà $this->name, $this->email, etc. à jour.
- Pour désactiver le bouton pendant l’envoi, ajoutez wire:loading.attr="disabled" et wire:target="register" (module 5).
Règles dynamiques et messages personnalisés
Vous pouvez définir les règles dans une méthode rules() si vous avez besoin de règles dynamiques :
protected function rules(): array
{
return [
'name' => 'required|string|min:3',
'email' => 'required|email|unique:users,email',
];
}
public function register(): void
{
$this->validate();
// ...
}
Pour des messages personnalisés :
$this->validate(
['name' => 'required|min:3'],
['name.required' => 'Le nom est obligatoire.', 'name.min' => 'Le nom doit faire au moins 3 caractères.']
);
Ou utilisez les fichiers de traduction Laravel (lang/.../validation.php) pour garder les messages centralisés.
Règles courantes (rappel) :
| Règle | Signification |
|---|---|
| required | Le champ doit être présent et non vide. |
| string | Doit être une chaîne. |
| Format email valide. | |
| min:3 | Minimum 3 caractères (ou valeur min pour nombre). |
| max:255 | Maximum 255 caractères. |
| confirmed | Doit avoir un champ champ_confirmation identique (ex. password_confirmation). |
| unique:users,email | La valeur ne doit pas exister dans la table users, colonne email. |
| image | Fichier image (jpeg, png, gif, etc.). |
| mimes:jpeg,png | Extension autorisée. |
Formulaire d’édition (données pré-remplies)
Pour éditer une ressource existante (ex. un utilisateur), vous devez charger les données au premier rendu dans mount(), puis les afficher dans les champs. À la soumission, vous appelez update() (ou save()) qui valide et met à jour le modèle.
Exemple : composant EditUser qui reçoit l’ID de l’utilisateur en paramètre.
<?php
namespace App\Livewire;
use App\Models\User;
use Livewire\Component;
use Livewire\Attributes\Rule;
class EditUser extends Component
{
public int $userId;
#[Rule('required|string|min:3')]
public string $name = '';
#[Rule('required|email')]
public string $email = '';
public function mount(int $id): void
{
$user = User::findOrFail($id);
$this->userId = $id;
$this->name = $user->name;
$this->email = $user->email;
}
public function update(): void
{
$this->validate();
$user = User::findOrFail($this->userId);
$user->update([
'name' => $this->name,
'email' => $this->email,
]);
session()->flash('message', 'Utilisateur mis à jour.');
}
public function render()
{
return view('livewire.edit-user');
}
}
- mount($id) : appelé une seule fois au chargement. On récupère l’utilisateur et on pré-remplit $name et $email. L’utilisateur voit le formulaire avec les valeurs actuelles.
- update() : valide, puis met à jour le modèle avec $user->update([...]). Pensez à authorize('update', $user) si vous utilisez des policies (module 9).
- Dans la vue : même structure que l’inscription, avec wire:submit="update" et wire:model sur name et email.
Validation temps réel (validateOnly)
Pour valider un seul champ dès que l’utilisateur le quitte (ou à chaque modification), utilisez le hook updated($propertyName). À chaque mise à jour d’une propriété (via wire:model), Livewire appelle updated('nomDeLaPropriete'). Vous pouvez alors appeler $this->validateOnly('nomDeLaPropriete') pour afficher l’erreur sur ce champ uniquement.
public function updated($property): void
{
$this->validateOnly($property);
}
- $property : le nom de la propriété qui vient d’être mise à jour (ex.
name,email). Vous pouvez aussi écrire updatedName(), updatedEmail(), etc. pour cibler un champ précis. - validateOnly($property) : ne valide que cette propriété ; les autres erreurs éventuelles ne sont pas effacées (elles restent jusqu’au prochain validate() ou re-render). Idéal pour un feedback immédiat sous le champ sans bloquer le reste du formulaire.
Attention : si vous utilisez wire:model.live sur tous les champs, updated() sera appelé à chaque frappe. Pour la validation temps réel, on préfère souvent wire:model.lazy pour valider au blur, ce qui limite les requêtes et évite d’afficher des erreurs trop tôt (ex. « Champ requis » tant que l’utilisateur n’a pas quitté le champ).
Upload de fichiers avec WithFileUploads
Livewire gère l’upload de fichiers via le trait WithFileUploads. Les fichiers sont temporairement stockés (dossier livewire-tmp par défaut), puis vous pouvez les déplacer ou les enregistrer dans storage (ou S3) dans une méthode du composant.
Classe :
<?php
namespace App\Livewire;
use Livewire\Component;
use Livewire\WithFileUploads;
class UploadPhoto extends Component
{
use WithFileUploads;
public $photo = null;
public function save(): void
{
$this->validate([
'photo' => 'required|image|max:1024', // 1 Mo max
]);
$path = $this->photo->store('photos', 'public');
// Enregistrer $path en BDD, attacher au modèle, etc.
session()->flash('message', 'Photo enregistrée.');
}
public function render()
{
return view('livewire.upload-photo');
}
}
- $photo : propriété qui recevra le fichier uploadé. Pas de type strict ; Livewire gère l’objet UploadedFile (temporaire).
- $this->photo->store('photos', 'public') : enregistre le fichier dans storage/app/public/photos (disque public). Pensez à lancer php artisan storage:link pour rendre le dossier accessible via une URL.
- Règles possibles : image (jpeg, png, bmp, gif, svg, webp), max:1024 (taille en Ko), mimes:jpeg,png, dimensions:min_width=100,max_width=2000, etc.
Vue :
<div>
<form wire:submit="save">
<input type="file" wire:model="photo" accept="image/*">
@error('photo')
<span class="text-red-600">{{ $message }}</span>
@enderror
<button type="submit">Enregistrer la photo</button>
</form>
@if ($photo)
<p>Fichier sélectionné : {{ $photo->getClientOriginalName() }}</p>
@endif
</div>
- wire:model="photo" : lie l’input file à la propriété $photo. Pendant l’upload (fichier volumineux), vous pouvez afficher un indicateur avec wire:loading (module 5).
- $photo->getClientOriginalName() : nom du fichier côté client ; disponible une fois l’upload temporaire terminé.
Bonnes pratiques : limiter la taille (max) et les types (image, mimes) pour éviter les abus. Pour les très gros fichiers, envisagez un upload direct vers le stockage (S3, etc.) avec un package dédié ; Livewire convient bien pour des photos de profil ou des pièces jointes raisonnables.
Pièges à éviter
- Ne jamais faire confiance au client : les propriétés publiques peuvent être modifiées dans le payload. Toujours valider dans la méthode (validate) et, pour l’édition, vérifier que l’entité appartient à l’utilisateur ou que l’utilisateur a le droit de la modifier (authorize(), module 9).
- Règle confirmed : Laravel attend une propriété password_confirmation. Si vous oubliez le second champ dans la vue ou le wire:model="password_confirmation", la validation échouera sans message clair.
- Validation temps réel trop agressive : avec wire:model.live sur tous les champs, updated() est appelé à chaque frappe. Préférez wire:model.lazy pour validateOnly au blur, sinon vous multipliez les requêtes et vous pouvez afficher « Champ requis » dès que l’utilisateur quitte un champ vide (avant même qu’il ait fini de remplir le formulaire).
- Upload : sans max (taille) et image ou mimes, un utilisateur peut envoyer des fichiers énormes ou des types dangereux. Toujours restreindre.
À retenir
- Flux : soumission → register() / update() → $this->validate() → si échec, re-render avec $errors ; si succès, logique métier + message flash ou redirection.
- #[Rule('...')] sur les propriétés pour déclarer les règles ; $this->validate() à la soumission. rules() pour des règles dynamiques ; second argument de validate() pour les messages personnalisés.
- $this->validateOnly($property) dans updated($property) pour une validation temps réel (idéalement wire:model.lazy pour valider au blur).
- Afficher les erreurs avec @error('champ') … @enderror et
{{ $message }}. Toujours un label par champ pour l’accessibilité. - Formulaire d’édition : mount($id) pour charger le modèle et pré-remplir les propriétés ; à la soumission, validate() puis $model->update([...]).
- Upload : WithFileUploads, wire:model sur l’input file, validate(['photo' => 'image|max:1024']), $this->photo->store('photos', 'public'), et @error('photo') dans la vue.
Checklist formulaire robuste
- Chaque champ a un label (accessibilité) et un @error('champ') pour afficher le message Laravel.
- validate() (ou validateOnly()) est appelé avant toute logique métier (create, update) ; jamais de User::create($this->all()) sans validation.
- Pour l’édition : mount($id) charge le modèle et pré-remplit les propriétés ; à la soumission, validate() puis $model->update([...]). Pensez à authorize('update', $model) (module 9).
- Règle confirmed : le champ password_confirmation existe dans la vue avec wire:model="password_confirmation".
- Upload : max (taille) et image ou mimes dans les règles ; @error('photo') dans la vue ; php artisan storage:link si vous utilisez le disque public.
Approfondissement
- Validation conditionnelle : dans rules(), vous pouvez retourner des règles différentes selon l’état : par exemple si $this->isEdit alors
'email' => 'required|email|unique:users,email,' . $this->userId, sinon'email' => 'required|email|unique:users,email'. Ou appeler $this->validate(['field' => Rule::when($condition, 'required')]). - Fichiers temporaires : avec WithFileUploads, le fichier est d’abord stocké en temporaire (dossier livewire-tmp). $this->photo->store('photos', 'public') le déplace vers le stockage final. Pour un brouillon, vous pouvez garder le fichier temporaire et ne le déplacer qu’à la confirmation ; attention à la durée de vie des fichiers temporaires (nettoyage Laravel).
- Taille max et PHP : la règle max:1024 (Ko) dans Livewire s’appuie aussi sur upload_max_filesize et post_max_size dans php.ini. Pour des uploads plus gros, augmentez ces valeurs côté serveur.
- Policies et formulaires : dans mount() pour un formulaire d’édition, appelez $this->authorize('update', $model) pour refuser l’accès dès l’affichage si l’utilisateur n’a pas le droit. Dans la méthode update(), authorize() avant update() (module 9).
À retenir
- Flux : soumission → méthode (ex. register()) → $this->validate() → si échec, re-render avec $errors ; si succès, logique métier + flash ou redirection.
- #[Rule] + validate() ; validateOnly($property) dans updated($property) pour la validation temps réel (idéalement avec wire:model.lazy). @error('champ') et
{{ $message }}dans la vue. - Édition : mount($id) pour charger et pré-remplir ; update() avec validate() et $model->update([...]). Upload : WithFileUploads, wire:model, validate, store(), @error('photo').
Dans le prochain module, nous voyons l’optimisation UI/UX : wire:loading, wire:target, désactivation visuelle des boutons, et bonnes pratiques pour un feedback utilisateur clair.