Livewire est une innovation relativement récente dans l'écosystème Laravel. Initialement perçu comme une curiosité, il a rapidement gagné en popularité au point de devenir incontournable. À première vue, Livewire peut sembler audacieux : pourquoi utiliser JavaScript ou l'un de ses nombreux frameworks comme Vue ou React, alors qu'il est possible de tout coder en PHP directement au sein de Laravel ? Cette proposition peut surprendre, car PHP est exécuté côté serveur, tandis que JavaScript s'exécute côté client, dans le navigateur. Il est donc crucial d'assurer une liaison fluide entre ces deux environnements.
Cependant, Livewire s'est révélé être un outil remarquablement efficace et agréable à utiliser, permettant de réduire considérablement la quantité de JavaScript nécessaire, tant que l'on reste dans des cas d'usage standard. Livewire est de fait devenu incontournable dans un cours consacré à Laravel. Dans cet article, je vous propose une introduction afin que vous puissiez découvrir ses avantages et apprendre à l'utiliser.
Installation
On va partir d'un Laravel tout neuf avec livewire grâce au starter kit officiel :
laravel new livewire
...
Which starter kit would you like to install? [None]:
[none ] None
[react ] React
[vue ] Vue
[livewire] Livewire
> livewire
livewire
Which authentication provider do you prefer? [Laravel's built-in authentication]:
[laravel] Laravel's built-in authentication
[workos ] WorkOS (Requires WorkOS account)
[none ] No authentication scaffolding
> none
none
Which testing framework do you prefer? [Pest]:
[0] Pest
[1] PHPUnit
> 1
1
Would you like to run npm install and npm run build? (yes/no) [yes]:
> yes
...
> build
> vite build
...
INFO Application ready in [livewire]. You can start your local development using:
➜ cd livewire
➜ composer run dev
New to Laravel? Check out our documentation. Build something amazing!
Et voilà, on est prêt ! Nous verrons dans un article ultérieur le fonctionnement de Vite pour la génération des assets.
Un layout
Livewire utilise des composants. Mais pour utiliser ces composants il faut commencer par créer un layout, c'est-à-dire une struture HTML de base pour accueillir ces composants. On dispose d'une commande Artisan pour créer ce layout :
php artisan livewire:layout

Avec ce code par défaut :
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ $title ?? config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body>
{{ $slot }}
@livewireScripts
</body>
</html>
Les directives @livewireStyles and @livewireScripts insèrent les assets et le code Javascript pour assurer le fonctionnement de Livewire.
Créer un composant
Dans Livewire il existe deux sortes de composants :
- les composants simples qui peuvent être intégrés dans une vue
- les composants "page" qui sont utilisés pour une page complète.
On va commencer avec cette deuxième catégorie de composant. Pour en créer un, c'est tout simple :
php artisan make:livewire pages::mon-composant

L'emoji ⚡est destiné à repérer facilement les composants de Livewire (vous pouvez facilement le désactiver si vous n'aimez pas).
On a ce code par défaut :
<?php
use Livewire\Component;
new class extends Component
{
//
};
?>
<div>
{{-- It always seems impossible until it is done. - Nelson Mandela --}}
</div>
La phrase est générée aléatoirement. Donc, on n'a pas grand-chose pour le moment. Modifiez ainsi le code :
<div>
Coucou !
</div>
Pour ceux qui ont utilisé Livewire avec les versions précédentes de Livewire (jusqu'à la 3) vous reconnaissez l'architecture classique de Volt. On a deux parties :
- la classe PHP
- le code HTML
Autrement dit, ce qui était l'organisation de Volt dans les précédentes versions, est devenu désormais la norme. Pour ceux qui répugnent à ainsi mélanger les genres il est possible de séparer ces différents code dans deux fichiers distincts. Vous trouvez les procédures dans la documentation.
Une route
Pour accéder à notre page toute neuve on a besoin d'une route :
Route::livewire('/composant', 'pages::mon-composant');
Maintenant avec l'URL /composant on a :
Coucou !
On a vérifié que la route fonctionne correctement.
Les propriétés
Les composants de Livewire ont besoin de données. On peut créer des propriétés :
new class extends Component
{
public string $message = 'Coucou !';
};
?>
<div>
<p>{{ $message }}</p>
</div>
Jusque-là, on n'a rien inventé de bien utile. Par contre, ça va devenir plus intéressant avec la liaison de données. On connait bien ça avec Vue.js par exemple, on synchronise la valeur d'un élément sur la page web avec la propriété du composant.
On va recoder notre vue (comme Tailwind est installé par défaut je l'utilise pour un peu de mise en forme) :
<div class="m-3">
<input type="text"
wire:model.live="message"
class="border border-gray-400 p-2 rounded shadow-sm focus:border-blue-500"
/>
<p class="mt-2 text-gray-600">Valeur : {{ $message }}</p>
</div>
Pour que les assets soient générés correctement utilisez ainsi Artisan (nous verrons cet aspect dans un prochain article) :
npm run dev
C'est l'attribut wire:model.live qui relie le champ de saisie à la propriété. Maintenant quand on change le texte dans le champ de saisie, il s'actualise au-dessous :

Un petit schéma pour bien visualiser ça :
Maintenant la question qu'on peut se poser c'est : comment ça fonctionne ?
On peut remarquer qu'à chaque changement, on a une requête, il part une requête POST de la forme .../livewire-0902834d/update avec ce corps :

On a au retour une réponse JSON :

Sous le capot, Livewire utilise Alpine pour gérer le Javascript sur la page.
Les propriétés calculées
On peut aussi avoir des propriétés calculées (comme avec Vue.js), alors là évidemment ça devient plus intéressant parce qu'on a besoin d'information sur le serveur. Imaginons que nous ayons deux utilisateurs dans notre table users.
Pour avoir des utilisateurs c'est tout simple. Dans DatabaseSeeder décommentez cette ligne :
public function run(): void
{
User::factory(10)->create();
Ensuite générez les tables et lancez en même temps la population :
php artisan migrate --seed
Par défaut c'est SQLite qui est utilisé et vous devriez avoir vos utilisateurs :

On peut créer cette propriété calculée (computed) :
use Livewire\Component;
use App\Models\User;
class MonComposant extends Component
{
public $index = 1;
public function getUserProperty()
{
return User::find($this->index);
}
Et dans la vue :
<input type="text"
wire:model.live="index"
class="border border-gray-400 p-2 rounded shadow-sm focus:border-blue-500"
/>
<p class="mt-2 text-gray-600">Valeur : {{ $this->user->name }}</p>
Maintenant, quand on entre l'index dans la zone de texte, le nom de l'utilisateur correspondant apparaît au-dessous :

Là bien sûr ça devient bien plus intéressant, parce qu'on n'a pas à se soucier de toute l'intendance d'Ajax ! Je n'ai pris aucune précaution et si vous entrez un index inconnu et vous allez tomber sur une erreur. C'est juste un exemple épuré pour le principe de fonctionnement.
Un petit schéma à nouveau pour bien visualiser le fonctionnement :

Les actions
Une action dans Livewire c'est la capacité d'écouter une action sur la page web et d'appeler une méthode dans le composant pour modifier la page. C'est donc un pas de plus dans l'interactivité par rapport aux propriétés calculées vues ci-dessus.
Préparation
Pour montrer ça, on va ajouter une colonne dans notre table users. On crée donc une migration :
php artisan make:migration alter_users_table --table=users
On complète le code de la migration :
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->integer('note')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('note');
});
}
};
Et on lance la migration :
php artisan migrate
Maintenant, on dispose d'une colonne note.
Action simple
On va coder ainsi notre composant :
new class extends Component
{
public $note = 0;
public function noter()
{
$user = User::find(1);
$user->note = $this->note;
$user->save();
}
};
On a :
- une propriété note
- une action noter
Dans la partie vue :
<div class="m-3">
<input type="text"
wire:model.defer="note"
class="border border-gray-400 p-2 rounded shadow-sm focus:border-blue-500"
/>
<button wire:click="noter"
wire:loading.attr="disabled"
class="ml-2 inline-flex items-center px-4 py-2 bg-indigo-600 border border-transparent rounded-lg font-semibold text-xs text-white uppercase tracking-widest hover:bg-indigo-700 active:bg-indigo-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50 transition-all duration-150">
<span wire:loading.remove wire:target="noter">
Noter
</span>
<span wire:loading wire:target="noter">
Envoi...
</span>
</button>
</div>
On a :
- une liaison de données avec la propriété note : wire:model.defer="note" (on prévoit defer pour éviter les multiples requêtes)
- un bouton pour noter avec une action sur le click : wire:click="noter"
Maintenant, quand on met une note dans la zone de texte et qu'on clique sur le bouton, on retrouve la valeur de la note dans la colonne note de l'utilisateur (ici, on a pris simplement le premier, mais on pourrait évidemment rendre cela dynamique ou prendre l'utilisateur connecté) :

Passage de paramètre
On peut passer un paramètre pour l'action. Poursuivons notre exemple en transmettant l'index de l'utilisateur. Dans le PHP, on ajoute une propriété $index et on utilise le paramètre dans l'action :
public $note = 0;
public $index = 1;
public function noter($indexUser)
{
$user = User::find($indexUser);
$user->note = $this->note;
$user->save();
}
Dans la partie vue, on ajoute la saisie de l'index et la transmission du paramètre, avec un peu de mise en forme :
<div class="max-w-sm m-6 p-6 bg-white rounded-2xl shadow-sm border border-gray-100">
<div class="space-y-5">
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Index de l'utilisateur</label>
<input wire:model.defer="index"
type="text"
class="w-full border border-gray-300 p-2.5 rounded-lg shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"
placeholder="Ex: 123" />
</div>
<div>
<label class="block text-sm font-semibold text-gray-700 mb-1">Note</label>
<input wire:model.defer="note"
type="text"
class="w-full border border-gray-300 p-2.5 rounded-lg shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all"
placeholder="A+, 18/20..." />
</div>
<button wire:click="noter({{ $index }})"
wire:loading.attr="disabled"
class="w-full inline-flex justify-center items-center px-4 py-3 bg-indigo-600 border border-transparent rounded-lg font-bold text-xs text-white uppercase tracking-widest hover:bg-indigo-700 active:bg-indigo-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-50 transition-all duration-150">
<span wire:loading.remove wire:target="noter">
Valider la note
</span>
<span wire:loading wire:target="noter">
Enregistrement...
</span>
</button>
</div>
</div>
La partie intéressante est le passage du paramètre :
wire:click="noter({{ $index }})"
On a donc deux zones de texte :


On peut désormais choisir l'utilisateur à noter. Bon, c'est sommaire, mais ça donne le principe de fonctionnement. On pourrait par exemple remplir une liste de choix avec les noms des utilisateurs. Je ne vais pas le faire pour rester simplement dans les principes de fonctionnement.
Pour avoir plus d'information sur le wire-loading que j'ai utilisé, la documentation est ici.
La validation
Dans mon exemple précédent, je n'ai prévu aucun contrôle quant aux valeurs transmises. Dans une approche réaliste il nous faut évidemment une validation des valeurs. La bonne nouvelle, c'est que le système est le même que pour la validation dans Laravel.
Validation classique
On va changer le code pour adopter quelque chose de plus réaliste avec un formulaire :
<div class="max-w-md m-6 p-8 bg-white rounded-2xl shadow-lg border border-gray-100">
<form wire:submit.prevent="submit" class="space-y-6">
<div class="flex flex-col">
<label class="text-sm font-bold text-gray-700 mb-2">Index de l'utilisateur</label>
<input wire:model.defer="index"
type="text"
class="w-full px-4 py-3 rounded-xl border @error('index') border-red-500 bg-red-50 @else border-gray-300 @enderror focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 outline-none transition-all shadow-sm"
placeholder="Identifiant unique"/>
@error('index')
<span class="text-xs text-red-600 mt-1.5 font-medium flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"/>
</svg>
{{ $message }}
</span>
@enderror
</div>
<div class="flex flex-col">
<label class="text-sm font-bold text-gray-700 mb-2">Note de l'évaluation</label>
<input wire:model.defer="note"
type="text"
class="w-full px-4 py-3 rounded-xl border @error('note') border-red-500 bg-red-50 @else border-gray-300 @enderror focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 outline-none transition-all shadow-sm"
placeholder="Note sur 20 ou lettre"/>
@error('note')
<span class="text-xs text-red-600 mt-1.5 font-medium flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"/>
</svg>
{{ $message }}
</span>
@enderror
</div>
<button type="submit"
wire:loading.attr="disabled"
class="w-full bg-indigo-600 hover:bg-indigo-700 active:transform active:scale-[0.98] text-white font-bold py-3.5 px-4 rounded-xl shadow-md hover:shadow-indigo-200 transition-all flex justify-center items-center gap-2">
<span wire:loading.remove wire:target="submit">Soumettre la note</span>
<div wire:loading wire:target="submit">
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</button>
</form>
</div>
Pour le PHP, on a maintenant ce code :
new class extends Component {
public $note = 0;
public $index = 1;
protected array $rules = [
'note' => 'required|integer|between:0,20',
'index' => 'required|exists:users,id',
];
public function submit(): void
{
$this->validate();
$user = User::find($this->index);
$user->note = $this->note;
$user->save();
}
};
On a donc deux propriétés :
- note
- index
On prévoit une validation des deux propriétés :
protected array $rules = [
'note' => 'required|integer|between:0,20',
'index' => 'required|exists:users,id',
];
Et pour le reste, c'est pratiquement pareil. Désormais, on a un contrôle de la validité des entrées :

Bon, je n'ai pas traduit en français, mais c'est une autre histoire qu'on a déjà rencontrée plusieurs fois... Mais on peut personnaliser les messages selon l'erreur :
protected $messages = [
'note.integer' => 'C\'est quand même mieux un nombre pour une note !',
];

Imbrication de composants
Les composants de Livewire peuvent être imbriqués (nested), autrement dit, on peut mettre des composants dans des composants. On va poursuivre notre exemple avec cette fois deux composants :
- un composant parent qui va afficher un utilisateur
- un composant enfant qui permet de noter l'utilisateur
On commence par créer les deux composants :
php artisan make:livewire pages::show-user
php artisan make:livewire note-user

Le premier est un composant de page et le second un simple composant.
On prévoit une route avec un paramètre pour préciser l'index de l'utilisateur :
Route::livewire('/user/{user}', 'pages::show-user');
Comme Livewire est parfaitement intégré à Laravel, on peut utiliser la liaison de données (Route Model Binding), donc là, je précise le nom de paramètre user pour que Laravel comprenne ce que je veux.
Avec Livewire, on n'utilise pas de contrôleur, alors comment récupérer le paramètre ? C'est tout simple avec Livewire :
use App\Models\User;
new class extends Component
{
public User $user;
};
Là notre composant récupère bien le paramètre, va chercher dans la base les informations de l'utilisateur, et affecte la propriété $user qui sera disponible dans la vue :
<div>
<p>Nom = {{ $user->name }}</p>
<p>Email = {{ $user->email }}</p>
</div>
Avec une URL de la forme .../user/1 on obtient les renseignements sur l'utilisateur :

On va maintenant inclure le composant pour noter l'utilisateur. On code le PHP pour note-user :
<?php
use Livewire\Component;
use App\Models\User;
new class extends Component {
public $note = 0;
public User $user;
protected array $rules = [
'note' => 'required|integer|between:0,20',
];
public function submit()
{
$this->validate();
$this->user->note = $this->note;
$this->user->save();
}
};
?>
Et la partie HTML :
<div class="max-w-md m-6 p-8 bg-white rounded-2xl shadow-lg border border-gray-100">
<form wire:submit.prevent="submit" class="space-y-6">
<div class="flex flex-col">
<label class="text-sm font-bold text-gray-700 mb-2">Note de l'évaluation</label>
<input wire:model.defer="note"
type="text"
class="w-full px-4 py-3 rounded-xl border @error('note') border-red-500 bg-red-50 @else @enderror focus:ring-4 focus:ring-indigo-500/10 focus:border-indigo-500 outline-none transition-all shadow-sm"
placeholder="Note sur 20 ou lettre"/>
@error('note')
<span class="text-xs text-red-600 mt-1.5 font-medium flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
clip-rule="evenodd"/>
</svg>
{{ $message }}
</span>
@enderror
</div>
<button type="submit"
wire:loading.attr="disabled"
class="w-full bg-indigo-600 hover:bg-indigo-700 active:transform active:scale-[0.98] text-white font-bold py-3.5 px-4 rounded-xl shadow-md hover:shadow-indigo-200 transition-all flex justify-center items-center gap-2">
<span wire:loading.remove wire:target="submit">Soumettre la note</span>
<div wire:loading wire:target="submit">
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
</div>
</button>
</form>
</div>
Il ne reste plus qu'à insérer ce composant dans l'autre, donc dans la vue show-user, en améliorant l'esthétique :
<div class="max-w-md mx-auto my-8 bg-white rounded-2xl shadow-xl overflow-hidden border border-gray-100">
<div class="bg-gradient-to-r from-indigo-500 to-purple-600 px-6 py-4">
<h3 class="text-white font-bold text-lg flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
</svg>
Profil Utilisateur
</h3>
</div>
<div class="p-6 space-y-6">
<div class="grid grid-cols-1 gap-4 bg-gray-50 p-4 rounded-xl border border-gray-200">
<div class="flex items-center justify-between border-b border-gray-200 pb-2">
<span class="text-xs font-bold uppercase text-gray-400 tracking-wider">Nom</span>
<span class="text-gray-900 font-semibold">{{ $user->name }}</span>
</div>
<div class="flex items-center justify-between">
<span class="text-xs font-bold uppercase text-gray-400 tracking-wider">Email</span>
<span class="text-gray-600 italic text-sm">{{ $user->email }}</span>
</div>
</div>
<div class="pt-2">
<h4 class="text-sm font-bold text-gray-800 mb-4 flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Attribuer une note
</h4>
<div class="bg-white">
<livewire:note-user :user="$user" />
</div>
</div>
</div>
</div>
On prend la précaution d'envoyer l'information de l'utilisateur dans le composant enfant.


Et ça devrait fonctionner avec la validation !
Conclusion
Je n'ai fait dans cet article que montrer les fonctionnalités de base du système proposé. Je conseille vivement de consulter la documentation. C'est une autre façon d'envisager la gestion du frontend en allégeant le codage de ce côté. On fait de l'Ajax sans s'en rendre compte.
Par bestmomo
Aucun commentaire