Dans mon précédent article je vous ai proposé de créer une application simple de gestion de tâches pour décovrir Laravel 12. Nous avons alors utilisé l'intendance classique du framework. Dans le présent article je vous propose de construire exactement la même application, mais en utilisant Livewire. Voyons quels sont ses avantages :
- Simplicité d'utilisation : Livewire permet aux développeurs de créer des applications interactives en utilisant principalement PHP, ce qui est plus familier pour les développeurs Laravel. Cela réduit la nécessité d'écrire du JavaScript complexe.
-
Réactivité en temps réel : Livewire permet de mettre à jour des parties spécifiques de la page web sans recharger toute la page, offrant ainsi une expérience utilisateur plus fluide et réactive.
-
Intégration avec Blade : Livewire s'intègre parfaitement avec le moteur de templates Blade de Laravel, permettant aux développeurs d'utiliser les composants Livewire directement dans leurs vues Blade.
-
Gestion de l'état simplifiée : Livewire gère automatiquement l'état des composants, ce qui simplifie la gestion des données et des interactions utilisateur.
-
Moins de JavaScript : pour les développeurs qui préfèrent travailler avec PHP, Livewire réduit la quantité de JavaScript nécessaire pour créer des interfaces dynamiques, ce qui peut accélérer le développement et réduire les erreurs.
-
Communauté et écosystème : Livewire bénéficie d'une communauté active et d'un écosystème en pleine croissance, avec de nombreux plugins et extensions disponibles pour étendre ses fonctionnalités.
-
Développement rapide : grâce à sa simplicité et à son intégration avec Laravel, Livewire permet de développer rapidement des fonctionnalités interactives, ce qui peut réduire le temps de développement.
Vous conviendrez que ce serait vraiment dommage de se priver de tous ces avantages !
On va donc recréer notre gestionnaire de tâches, mais avec Livewire. Je passerai plus rapidement sur certains points dont vouos avez le détail dans mon précédent article. Vous pouvez télécharger le code final de l'article.
Installation avec Laravel Installer
Nous allons à nouveau utiliser l’installeur officiel. Il faut commencer par installer globalement l’installeur avec Composer, si vous ne l'avez pas encore fait :
composer global require laravel/installer
On commence l'installation :
laravel new todolistlivewire13
On tombe sur une première question :

Là on choisit livewire. La question suivante :

Comme on ne va pas utiliser l'authentification on répond none. Pour les tests :

Choisissee PHPUnit. Refusez ensuite l'assitant AI de codage :

Après un moment pour l'installation vous avez cette question :

On choisit la première option sqlite qui est l'option par défaut.
Vous avez ensuite une dernière question :

Répondez yes. Et c'est terminé ! Vous arrivez sur la page d'accueil de Laravel :

On est prêt à commencer !
Base de données
Les migrations
Il est maintenant temps de se poser la question des données nécessaires pour notre application. Pour chaque tâche on va avoir :
- un titre ("Tondre la pelouse")
- un texte de détail ("Ne pas oublier de demander la tondeuse au voisin la veille")
- une date de création
- une date de modification
- un état (a priori deux : "à faire" et "fait")
On va donc créer une table pour mémoriser tout ça. On a vu qu'avec Laravel on utilise une migration. D'autre part, Laravel est équipé d'un ORM efficace : Eloquent. Chaque table est représentée par une classe qui permet de manipuler les données. On dispose ainsi d'un modèle pour chaque table à manipuler. On va demander à Artisan de créer à la fois une table et son modèle Eloquent associé :
php artisan make:model Task -m
On trouve le modèle ici :

La migration pour la table tasks a été créée ici :

Par défaut on a ce code :
public function up(): void
{
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
}
On a :
- une clé id
- deux colonnes (created_at et updated_at) créées par la méthode timestamps.
On ajoute les autres colonnes nécessaires pour notre projet :
Schema::create('tasks', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('title');
$table->text('detail');
$table->boolean('state')->default(false);
});
Et on lance la migration :

On dispose à présent de la table tasks dans notre base SQLite.
Organisation des vues
Livewire fonctionne avec des composants. Il en existe deux sortes : en ligne ou pleine page. Nous utiliserons cette deuxième possibilité. Dans ce cas un composant va chercher le layout resources/views/components/layouts/app.blade.php. Il existe une commande pour le créer :
php artisan livewire:layout

Pour notre layout on prévoit le même code que dans notre précédente version :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mes tâches</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-100">
<div class="container mx-auto p-4 max-w-4xl">
<h1 class="text-2xl font-bold mb-4">{{ $title }}</h1>
{{ $slot }}
</div>
</body>
</html>
Le $slot affiche le contenu inséré dans cette section depuis une vue enfant. C'est une fonctionnalité de Blade qui permet d'injecter du contenu dynamique.
Laravel utilise Vite pour inclure les fichiers CSS et JavaScript compilés. Vite est un outil de construction qui optimise le chargement des ressources.
Lors de l'installation on a eu la commande :
npm run build
Cette commande a permis de construire les fichiers de ressources (css et javascript). Mais si vous ajoutez par la suite des éléments Tailwind qui n'étaient pas présents au départ, ils ne se retrouveront pas dans les ressources. Il faudrait relancer la commande, ce qui seraient laborieux. C'est pour cette raison que lors du développement on utlise plutôt la commande :
npm run dev
On aura de cette façon une construction "à la volée" dès qu'on ajoute des éléments.
Créer une tâche
Le composant
On crée le composant Livewire pour la création d'une tâche :
php artisan make:livewire pages::task-create

Voyons le code généré :
<?php
use Livewire\Component;
new class extends Component
{
//
};
?>
<div>
{{-- Simplicity is the essence of happiness. - Cedric Bledsoe --}}
</div>
La phrase est générée aléatoirement. Donc on n'a pas grand chose pour le moment...
La route
On a besoin d'une route pour accéder au composant (routes.web) :
Route::livewire('/tasks/create', 'pages::task-create')->name('tasks.create');
Le code PHP
Il nous faut coder le code PHP de notre composant :
<?php
use Livewire\Attributes\Validate;
use Livewire\Attributes\Layout;
use Livewire\Component;
use App\Models\Task;
new #[Layout('layouts::app', ['title' => 'Créer une tâche'])] class extends Component
{
#[Validate('required|max:100')]
public string $title = '';
#[Validate('required|max:500')]
public string $detail = '';
public string $messageOk = '';
public function save()
{
$this->validate();
$task = new Task;
$task->title = $this->title;
$task->detail = $this->detail;
$task->save();
$this->messageOk = 'Tâche créée avec succès.';
$this->title = '';
$this->detail = '';
}
};
?>
Le HTML
Et voici la partie HTML du composant :
<form wire:submit="save">
@csrf
<div class="mt-3 mb-4 list-disc list-inside text-sm text-green-600">
{{ $messageOk }}
</div>
<!-- Titre -->
<div class="mb-4">
<label for="title" class="block text-gray-700">Titre :</label>
<input type="text" id="title" wire:model="title" class="w-full px-3 py-2 border rounded" required />
@error('title')
<div class="mt-3 mb-4 list-disc list-inside text-sm text-red-600">
{{ $message }}
</div>
@enderror
</div>
<!-- Détail -->
<div class="mb-4">
<label for="detail" class="block text-gray-700">Détail :</label>
<textarea id="detail" wire:model="detail" class="w-full px-3 py-2 border rounded" required ></textarea>
@error('detail')
<div class="mt-3 mb-4 list-disc list-inside text-sm text-red-600">
{{ $message }}
</div>
@enderror
</div>
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">Envoyer</button>
</form>
Maintenant avec l'url todolistlivewire13.oo/tasks/create on obtient la page avec le formulaire :

Vous pouvez vérifier que la validation fonctionne :

Pour le moment le texte de l'erreur est en anglais, on va s'en occuper plus loin.
Si la validation est bonne, la tâche est créée:
L'avantage de cette version Livewire, par rapport à ce qu'on a vu dans la précédente version traditionnelle, c'est que le composant qu'on a créé gère l'affichage du formulaire ainsi que sa soumission.
Les erreurs en français
Par défaut, les messages d'erreur sont en anglais. Pour avoir ces textes en français, vous devez utiliser le package ici. Dans un premier temps, changez cette ligne dans le fichier .env :
APP_LOCALE=fr
Puis pour faire les choses simplement, faites cette installation :
composer require laravel-lang/common
Puis faites un update :
php artisan lang:update

Vous devriez à présent avoir vos erreurs en français :

DRY
Comme pour la précédente version on va optimiser le code en évitant de se répéter.
Les erreurs de validation
Prenons l'exemple des erreurs de validation. Nous avons pour nos deux contrôles un code pratiquement identique :
@error('title')
<div class="mt-3 mb-4 list-disc list-inside text-sm text-red-600">
{{ $message }}
</div>
@enderror
</div>
...
@error('detail')
<div class="mt-3 mb-4 list-disc list-inside text-sm text-red-600">
{{ $message }}
</div>
@enderror
Il est donc judicieux de créer un composant dans ce cas :
php artisan make:component error --view

Avec ce code :
@error($field)
<div class="mt-3 mb-4 list-disc list-inside text-sm text-red-600">
{{ $message }}
</div>
@enderror
Il n'y a plus qu'à substituer dans le formulaire :
<!-- Titre -->
<div class="mb-4">
<label for="title" class="block text-gray-700">Titre :</label>
<input type="text" id="title" wire:model="title" class="w-full px-3 py-2 border rounded" required />
<x-error field="title" />
</div>
<!-- Détail -->
<div class="mb-4">
<label for="detail" class="block text-gray-700">Détail :</label>
<textarea id="detail" wire:model="detail" class="w-full px-3 py-2 border rounded" required ></textarea>
<x-error field="detail" />
</div>
On obtient un fonctionnement identique avec du code plus léger et clair.
Le label
On peut faire pareil pour le label :
php artisan make:component label --view

Avec ce code :
<label for="{{ $for }}" class="block text-gray-700">{{ $label }}</label>
Et dans le formulaire :
<!-- Titre -->
<div class="mb-4">
<x-label for="title" label="Titre :" />
<input type="text" id="title" wire:model="title" class="w-full px-3 py-2 border rounded" required />
<x-error field="title" />
</div>
<!-- Détail -->
<div class="mb-4">
<x-label for="detail" label="Détail :" />
<textarea id="detail" wire:model="detail" class="w-full px-3 py-2 border rounded" required ></textarea>
<x-error field="detail" />
</div>
Modifier une tâche
Le composant
On crée le composant Livewire pour la création d'une tâche :
php artisan make:livewire pages::task-edit

La route
On a besoin d'une route pour accéder au composant (routes.web) :
Route::livewire('/tasks/{task}/edit', 'pages::task-edit')->name('tasks.edit');
Le PHP
Il nous faut coder le PHP de notre composant :
use Livewire\Attributes\Layout;
use Livewire\Attributes\Validate;
use Livewire\Attributes\Title;
use Livewire\Component;
use App\Models\Task;
new #[Layout('layouts::app', ['title' => 'Modifier une tâche'])]
class extends Component {
public Task $task;
#[Validate('required|max:100')]
public string $title = '';
#[Validate('required|max:500')]
public string $detail = '';
public bool $state = false;
public string $messageOk = '';
public function mount(Task $task)
{
$this->task = $task;
$this->fill($this->task);
}
public function save()
{
$this->validate();
$this->task->title = $this->title;
$this->task->detail = $this->detail;
$this->task->state = $this->state;
$this->task->save();
$this->messageOk = 'Tâche modifiée avec succès.';
}
}
Le code ressemble beaucoup à celui de la création. La principale différence réside dans le fait qu'il faut récupérer les valeurs actuelles de la tâche.
La vue
On code le HTML qui va beaucoup ressembler à celui de la création :
<form wire:submit="save">
@csrf
<div class="mt-3 mb-4 list-disc list-inside text-sm text-green-600">
{{ $messageOk }}
</div>
<!-- Titre -->
<div class="mb-4">
<x-label for="title" label="Titre :" />
<input type="text" id="title" wire:model="title" class="w-full px-3 py-2 border rounded" required />
<x-error field="title" />
</div>
<!-- Détail -->
<div class="mb-4">
<x-label for="detail" label="Détail :" />
<textarea id="detail" wire:model="detail" class="w-full px-3 py-2 border rounded" required ></textarea>
<x-error field="detail" />
</div>
<!-- Tâche accomplie -->
<div class="mb-4">
<input id="state" type="checkbox" class="w-4 h-4 px-3 py-2 border rounded" name="state" wire:model="state">
<span class="text-gray-700">{{ __('Tâche accomplie') }}</span>
</div>
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded">Envoyer</button>
</form>
J'ai juste ajouté la case à cocher pour la tâche accomplie.
On peut à présent modifier nos tâches :

Voir une tâche
On va partir du principe qu'on aura un tableau avec juste le titre des tâches et pas le détail, il faut donc prévoir de pouvoir afficher chaque tâche.
Le composant
On crée le composant Livewire pour la création d'une tâche :
php artisan make:livewire pages::task-view

La route
On a besoin d'une route pour accéder au composant (routes.web) :
Route::livewire('/tasks/{task}', 'pages::task-view')->name('tasks.show');
Le PHP
On code le PHP :
use App\Models\Task;
use Livewire\Attributes\Layout;
use Livewire\Component;
new #[Layout('layouts::app', ['title' => 'Voir une tâche'])]
class extends Component {
public Task $task;
public function mount(Task $task)
{
$this->task = $task;
}
};
Le HTML
On code le HTML avec le même code de notre précédente version :
<div class="max-w-2xl mx-auto bg-white shadow-md rounded-lg p-6 space-y-6">
<div class="border-b pb-4">
<h3 class="font-semibold text-xl text-gray-800">Titre</h3>
<p class="text-gray-600 mt-2">{{ $task->title }}</p>
</div>
<div class="border-b pb-4">
<h3 class="font-semibold text-xl text-gray-800">Détail</h3>
<p class="text-gray-600 mt-2">{{ $task->detail }}</p>
</div>
<div class="border-b pb-4">
<h3 class="font-semibold text-xl text-gray-800">Etat</h3>
<p class="text-gray-600 mt-2">
@if($task->state)
<span class="text-green-600">La tâche a été accomplie !</span>
@else
<span class="text-red-600">La tâche n'a pas encore été accomplie.</span>
@endif
</p>
</div>
<div class="border-b pb-4">
<h3 class="font-semibold text-xl text-gray-800">Date de création</h3>
<p class="text-gray-600 mt-2">{{ $task->created_at->format('d/m/Y') }}</p>
</div>
@if(!$task->created_at->isSameDay($task->updated_at))
<div>
<h3 class="font-semibold text-xl text-gray-800">Dernière modification</h3>
<p class="text-gray-600 mt-2">{{ $task->updated_at->format('d/m/Y') }}</p>
</div>
@endif
</div>
Et avec une URL de la forme todolistlivewire13.oo/tasks/1 on affiche les éléments d'une tâche :

On n'affiche la date de mise à jour (en fait le jour) que si elle est différente de celle de la création.
Liste des tâches
Maintenant qu'on sait créer, modifier et afficher des tâches, on va voir comment en afficher la liste, en prévoyant des boutons pour les différentes actions, ainsi qu'un bouton pour ouvrir le formulaire de création d'une tâche.
On crée le composant Livewire pour la création d'une tâche :
php artisan make:livewire pages::task-index

La route
On a besoin d'une route pour accéder au composant (routes.web) :
Route::livewire('/tasks', 'pages::task-index');
Le PHP
On code la classe TaskIndex :
use Livewire\Attributes\Layout;
use Illuminate\Database\Eloquent\Collection;
use App\Models\Task;
use Livewire\Component;
new #[Layout('layouts::app', ['title' => 'Liste des tâches'])] class extends Component
{
public Collection $tasks;
public function mount()
{
$this->tasks = Task::all();
}
public function destroy(Task $task)
{
$task->delete();
$this->tasks = Task::all();
}
};
Le HTML
Comme il n'y aura pas de très nombreuses tâches, on ne prévoie pas de pagination, mais Laravel sait très bien s'occuper de ça également.
On ajoute un composant pour les boutons dans la liste :
php artisan make:component link-button --view

Avec ce code :
<a
role="button"
{{ $attributes->merge([
'class' => 'inline-flex items-center px-2 py-1 rounded-md font-semibold text-xs uppercase tracking-widest transition ease-in-out duration-150 focus:outline-none focus:ring ring-gray-300 disabled:opacity-25 ' .
($attributes->get('color') ?? 'bg-gray-800 border-transparent text-white hover:bg-gray-700 active:bg-gray-900 focus:border-gray-900') .
($attributes->get('delete-cursor') ? ' cursor-not-allowed' : '')
]) }}
>
{{ $slot }}
</a>
On code le HTML de task-index :
<div class="container flex justify-center mx-auto relative">
<div class="flex flex-col w-full">
<div class="border-b border-gray-200 shadow overflow-x-auto pt-6">
<div class="flex justify-end mb-4">
<x-link-button href="{{ route('tasks.create') }}" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Ajouter une tâche
</x-link-button>
</div>
<table class="min-w-full bg-white">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-2 text-xs text-gray-500">#</th>
<th class="px-4 py-2 text-xs text-gray-500">Titre</th>
<th class="px-4 py-2 text-xs text-gray-500">Etat</th>
<th class="px-4 py-2 text-xs text-gray-500 text-center">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
@foreach($tasks as $task)
<tr class="whitespace-nowrap">
<td class="px-4 py-4 text-sm text-gray-500">{{ $task->id }}</td>
<td class="px-4 py-4 text-sm font-medium text-gray-900">{{ $task->title }}</td>
<td class="px-4 py-4">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $task->state ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
{{ $task->state ? 'Effectuée' : 'A faire' }}
</span>
</td>
<td class="px-4 py-4 flex justify-center space-x-2">
<x-link-button href="{{ route('tasks.show', $task->id) }}" class="text-blue-600 hover:text-blue-400">
Voir
</x-link-button>
<x-link-button href="{{ route('tasks.edit', $task->id) }}" class="text-yellow-600 hover:text-yellow-400">
Modifier
</x-link-button>
<x-link-button delete-cursor color="bg-red-600 hover:bg-red-400" wire:click="destroy({{ $task->id }})">
Supprimer
</x-link-button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
Et cet aspect :

Les boutons permettent d'accéder aux tâches qu'on a codées précédemment. Par rapport à notre précédente version ici la suppression a un code plus élégant.
Je n'ai pas prévu de boîte de dialogue d'avertissement avant la suppression, mais ça serait à ajouter dans une application réelle. Pour cette suppression, j'ai prévu un formulaire caché pour chaque tâche, et un peu de javascript pour la soumission. Ce n'est évidemment pas la seule façon de faire, mais ça ne concerne pas directement Laravel.
Les tests
Laravel permet de faire facilement de tests. Il utilise Pest ou PHPUnit, et on trouve par défaut le fichier de configuration phpunit.xml à la racine. Par défaut, la base de donnée est celle définie dans le fichier .env. Les fichiers de test se trouvent dans le dossier tests :

Lancez les tests :

On voit qu'il y a déjà des tests. Voyons comment ils sont constitués en analysant par exemple la classe ExampleTest
class ExampleTest extends TestCase
{
public function test_the_application_returns_a_successful_response(): void
{
$response = $this->get('/');
$response->assertStatus(200);
}
}
On envoie une requête HTTP GET sur l'url "/". Le test consiste à vérifier (assertStatus) qu'on a bien une réponse 200.
Remarquez l'importance du libellé de la méthode (test_the_application_returns_a_successful_response) pour obtenir un texte explicite dans le test (the application returns a successful response).
Maintenant qu'on a vu le principe des tests, on a va en créer un pour notre application, par exemple la création d'une tâche. On crée d'abord la classe de test :
php artisan make:test CreateTaskTest

On ajoute cette fonction dans la classe :
class CreateTaskTest extends TestCase
{
use RefreshDatabase;
public function test_can_create_task()
{
$response = $this->post('/tasks', [
'title' => 'Ma nouvelle tâche',
'detail' => 'Tous les details de ma nouvelle tâche',
]);
$this->assertDatabaseHas('tasks', [
'title' => 'Ma nouvelle tâche'
]);
$this->get('/tasks')->assertSee('Ma nouvelle tâche');
}
}

On fait les tests suivants :
- la tâche est bien dans la table
- on trouve la tâche dans la liste des tâches
On peut tester ainsi tous les aspects de l'application, je vous renvoie à la documentation détaillée pour tous les détails.
Conclusion
J'espère que ce petit exemple pourra donner envie de découvrir ce framework dans sa version Livewire. Il est à la fois très chargé pour un débutant et frustrant pour quelqu'un de plus avancé mais son seul objectif est de permettre la découverte de Laravel au travers d'un exemple léger mais réaliste.
Par bestmomo
Aucun commentaire