Logomark

LARAVEL

Un framework qui rend heureux
Voir cette catégorie
Vers le bas
Ma première application Laravel 12 (version Livewire)
Mercredi 19 mars 2025 21:44

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 :

  1. 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.
  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. 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 todolistlivewire12

On tombe sur une première question :

Là on choisit none. Après quelques minutes d'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 :

Maintenant on installe Livewire :

composer require livewire/livewire

Et on est prêts à 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 TaskCreate

On se retrouve avec la classe :

Et la vue :

Voyons le code généré pour la classe :

<?php

namespace App\Livewire;

use Livewire\Component;

class TaskCreate extends Component
{
    public function render()
    {
        return view('livewire.task-create');
    }
}

On a une méthode render et on retourne la vue générée. Dans la vue on a :

<div>
    {{-- In work, do what you enjoy. --}}
</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) :

use App\Livewire\TaskCreate;

Route::get('/tasks/create', TaskCreate::class)->name('tasks.create');

La classe PHP

Il nous faut coder la classe PHP TaskCreate de notre composant :

<?php

namespace App\Livewire;

use Livewire\Attributes\Validate; 
use Livewire\Attributes\Title;
use Livewire\Component;
use App\Models\Task;

class TaskCreate 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 = '';
    }

    #[Title('Créer une tâche')] 
    public function render()
    {
        return view('livewire.task-create');
    }
}

La vue

Et voici la vue task-create 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 todolistlivewire12.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 obtenir ceci :

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 TaskEdit

On se retrouve avec la classe :

Et la vue :

La route

On a besoin d'une route pour accéder au composant (routes.web) :

use App\Livewire\TaskEdit;

Route::get('/tasks/edit/{task}', TaskEdit::class)->name('tasks.edit');

La classe PHP

Il nous faut coder la classe PHP TaskEdit de notre composant :

<?php

namespace App\Livewire;

use Livewire\Attributes\Validate;
use Livewire\Attributes\Title;
use Livewire\Component;
use App\Models\Task;

class TaskEdit 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.';
    }

    #[Title('Modifier une tâche')] 
    public function render()
    {
        return view('livewire.task-edit');
    }
}

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 la vue task-edit qui va beaucoup ressembler à celle 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 TaskView

On se retrouve avec la classe :

Et la vue :

La route

On a besoin d'une route pour accéder au composant (routes.web) :

use App\Livewire\TaskView;

Route::get('/tasks/show/{task}', TaskView::class)->name('tasks.show');

La classe PHP

On code la classe TaskView :

<?php

namespace App\Livewire;

use Livewire\Attributes\Title;
use Livewire\Component;
use App\Models\Task;

class TaskView extends Component
{
    public Task $task;
    
    public function mount(Task $task)
    {
        $this->task = $task;
    }

    #[Title('Voir une tâche')] 
    public function render()
    {
        return view('livewire.task-view');
    }
}

La vue

On code la vue task-view 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 todolistlivewire12.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 TaskIndex

On se retrouve avec la classe :

Et la vue :

La route

On a besoin d'une route pour accéder au composant (routes.web) :

use App\Livewire\TaskIndex;

Route::get('/tasks', TaskIndex::class)->name('task.index');

La classe PHP

On code la classe TaskIndex :

<?php

namespace App\Livewire;

use Livewire\Component;
use Livewire\Attributes\Title;
use App\Models\Task;
use Illuminate\Database\Eloquent\Collection;

class TaskIndex extends Component
{
    public Collection $tasks;

    public function mount()
    {
        $this->tasks = Task::all();
    }

    public function destroy(Task $task)
    {
        $task->delete();
        $this->tasks = Task::all();
    }

    #[Title('Liste des tâches')] 
    public function render()
    {
        return view('livewire.task-index');
    }
}

La vue

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 :

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 la vue 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. Vous pouvez utiliser sqlite en mémoire en décommentant ces lignes pour éviter d'impacter votre base de données avec les tests :

<server name="DB_CONNECTION" value="sqlite"/>
<server name="DB_DATABASE" value=":memory:"/>

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

Nombre de commentaires : 2