Laravel 10

Ma première application Laravel 10

Je vous avais proposé dans un précédent article de voir comment créer une simple application Laravel 9 en détaillant toutes les étapes. Il s’adressait donc aux débutants qui désirent découvrir ce framework et peut-être aux moins débutants qui aimeraient se rafraichir un peu les idées ! Comme les choses ont un peu évoluées avec Laravel 10 je vous propose cette version actualisée.

Évidemment je ne vais pas exposer tous les aspects de Laravel ici mais juste les éléments essentiels à prendre en compte. Toutefois on arrivera à une application totalement fonctionnelle.

On va ainsi créer un simple gestionnaire de tâches.

Vous pouvez télécharger le code final de l’article.

Les prérequis

Laravel, qui en est actuellement à sa version 10, a besoin de quelques éléments côté serveur :

  • PHP >= 8.1
  • des extensions PHP (que du classique) :
    • Extension PDO,
    • Extension Mbstring,
    • Extension OpenSSL,
    • Extension Session,
    • Extension Tokenizer,
    • Extension XML.‌,
    • Extension Ctype,
    • Extension Fileinfo,
    • Extension DOM,
    • Extentsion PCRE,
    • Extension cUrl,
    • Extension Filter,
    • Extension Hash

Vous pouvez trouver tout ça facilement sur un serveur local comme WAMPP ou XAMPP mais franchement si vous ne voulez pas vous compliquer la vie utilisez Laragon !

On va aussi utiliser MySQL comme serveur de données.

Vous aurez aussi besoin de Composer pour gérer les librairies PHP.

Enfin pour le frontend il vous faudra node.js.

Vous avez tout ça ? Alors c’est parti !

Installation de Laravel

Si vous utilisez Laragon l’installation est d’une extrême simplicité :

Il suffit ensuite de préciser le nom :

Et c’est parti ! Vous obtenez :

  • un nouveau dossier todolist avec une installation fraîche de Laravel dernière version
  • une base de données MySQL du même nom
  • et un hôte local todolist.oo (chez moi c’est oo mais peut-être que pour vous ce sera une autre extension)

Si vous n’utilisez pas Laragon c’est plus laborieux, dans la console entrez :

composer create-project laravel/laravel todolist

Et évidemment il vous faudra créer manuellement la base. D’autre part vous n’aurez pas automatiquement un hôte local.

Laravel dispose d’un serveur PHP local léger qu’on démarre en utilisant l’outil Artisan de Laravel :

php artisan serve

Mais il vaut mieux utiliser un serveur plus complet comme celui proposé par Laragon.

Si tout se passe bien vous devez arriver sur cette page avec todolist.oo (ou l’extension que vous utilisez) :

Base de données

Vous avez créé la base de données mais il faut renseigner Laravel pour qu’il s’en serve. Pour ça on va utiliser le fichier de configuration .env qui se situe à la racine :

Si pour une raison quelconque ce fichier n’existe pas faite une copie de .env.example pour le créer.

Dans ce fichier on va préciser le nom de la base et les identifiants pour y accéder :

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=todolist
DB_USERNAME=root
DB_PASSWORD=

Pour l’instant la base est vide. Laravel dispose d’un outil de migration pour créer des tables, les modifier, créer des index…

On trouve ça dans le dossier database :

On y trouve 4 fichiers de migration :

  • pour la table users
  • pour la table password_resets
  • pour la table failed_jobs
  • pour la table personnal_access_tokens

Comme on ne se servira pas des deux dernières on va supprimer ces fichiers. Il ne va donc plus nous rester que la migration pour la table users et password_resets. Si on regarde un peu le code pour users on a une fonction up :

public function up()
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->unique();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password');
        $table->rememberToken();
        $table->timestamps();
    });
}

C’est ici qu’on a le code pour créer la table et ses colonnes. Je ne vais pas entrer dans le détail mais le code est explicite.

Pour créer les tables à partir de cette migration on utilise encore Artisan :

Là il peut vous sembler étrange que, bien qu’ayant supprimé la migration pour la table personnal_access_tokens, elle soit quand même créée. Cela est dû au fait que le package Laravel\Sanctum (qui sert à faciliter l’authentification pour les application SPA) est installé par défaut et comporte cette migration. Pour corriger ça vous allez intervenir dans la classe App\Providers\AppServiceProvider :

use Laravel\Sanctum\Sanctum;

...

public function register()
{
    Sanctum::ignoreMigrations();
}

On lance une commande pour raffraichir la base :

Si on regarde dans la base on se retrouve avec 3 tables :

On a bien nos tables users et password_reset_tokens mais aussi une table migrations qui mémorise les actions de migration de Laravel. C’est une table d’intendance que vous n’avez pas à toucher.

On a dans la table users les colonnes telles que définies dans la migration :

L’authentification

Laravel n’est pas équipé à la base d’un système d’authentification (c’était le cas avec d’anciennes versions) mais on peut l’ajouter avec un package complémentaire, c’est ce qu’on va faire en utilisant Composer :

composer require laravel/breeze --dev

Composer a ajouté la librairie dans le fichier composer.json :

"require-dev": {
    ..
    "laravel/breeze": "^1.21",
    ...
},

Et l’a chargée dans le dossier vendor :

On va poursuivre l’installation de ce package :

Ici on a le choix, on va sélectionner la première option blade. Répondez non aux autres questions.

On va enfin compiler les assets :

npm install
npm run dev

En cas d’erreur à ce niveau vérifiez que vous utilisez la dernière version de node.js

On va avoir la création d’un dossier node_modules avec toutes les dépendances.

Laravel utilise Vite pour la compilation depuis sa version 10, et comme son nom l’indique il est rapide !

Comme on est en mode dev toute modification du code sera immédiatement compilée.

Au niveau de la page d’accueil on se retrouve avec deux liens :

On a ainsi un formulaire pour le login :

Et un pour l’inscription :

Il est d’autre part possible de réinitialiser le mot de passe. Vous pouvez explorer tout ça…

Les langues

Évidemment à la base tout est en anglais mais Laravel sait très bien gérer l’aspect linguistique. Vous avez toutes les traductions de base ici.

On va installer le français :

composer require laravel-lang/common --dev

php artisan lang:add fr

php artisan lang:update

Et les fichiers apparaissent comme par magie :

Ensuite vous changez la locale dans config/app.php :

'locale' => 'fr',

Vous aurez ainsi en français les messages pour l’authentification et la validation. Par exemple pour le login :

Par contre on voit que çe ne change rien sur la page d’accueil :

Toutes les vues (fichiers qui génèrent les pages HTML) de Laravel sont dans le dossier resources/views :

La package qu’on a installé à ajouté ici les vues du dossier auth. La vue de l’accueil est welcome.blade.php. On a les deux textes dans ce code :

<a href="{{ route('login') }}" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Log in</a>

@if (Route::has('register'))
    <a href="{{ route('register') }}" class="ml-4 font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Register</a>
@endif

Je peux faire un changement brutal et mettre les textes en français dans la vue, mais alors mon application ne sera plus multilangues. Après tout, si je veux mon site exclusivement en français ça ne pose aucun problème. Mais si je veux conserver la possibilité d’avoir plusieurs langues je dois trouver une méthode plus élégante…

J’utilise un helper de Blade :

<a href="{{ route('login') }}" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">@lang('Log in')</a>

@if (Route::has('register'))
    <a href="{{ route('register') }}" class="ml-4 font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">@lang('Register')</a>
@endif

On obtient le même résultat mais maintenant le site est multilingues !

Vous pouvez vous demander où sont les traductions correspondantes, allez voir dans le fichier lang/fr.json.

"Login": "Connexion",
"Logout": "Déconnexion",

Les données

Il est maintenant temps de se poser la question des données nécessaires pour notre application. On va partir sur cette base, pour chaque tâche on a 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 états : à 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é :

On trouve le modèle ici :

On a déjà le modèle par défaut User pour gérer les données de la table users.

La migration 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éée par la méthode timestamps.

On va ajouter les colonnes nécessaires :

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 doit trouver la table dans la base avec ses colonnes :

Le contrôleur et les routes

Laravel est basé sur le modèle MVC :

  • Modèle : c’est Eloquent qui se charge de cet aspect et on a créé un modèle (Task) pour nos tâches,
  • Vue : on a déjà des vues pour la partie authentification, on a devoir en créer aussi pour nos tâches,
  • Contrôleur : c’est le chef d’orchestre de l’application, on va créer à présent un contrôleur pour gérer toutes les actions nécessaires pour les tâches.
php artisan make:controller TaskController --resource

On trouve ce contrôleur ici :

C’est un contrôleur de ressource, ce qui signifie qu’il est déjà équipé de 7 fonctions pour les actions suivantes :

Verbe URI Action Route
GET /tasks index tasks.index
GET /tasks/create create tasks.create
POST /tasks store tasks.store
GET /tasks/{task} show tasks.show
GET /tasks/{task}/edit edit tasks.edit
PUT/PATCH /tasks/{task} update tasks.update
DELETE /tasks/{task} destroy tasks.destroy

On va créer aussi les routes pour accéder à ces actions dans le fichier routes/web.php :

use App\Http\Controllers\TaskController;
 
Route::resource('tasks', TaskController::class)->middleware('auth');

Si vous utilisez la commande php artisan route:list vous obtenez toutes les routes de l’application et en particulier les 7 pour le contrôleur :

Maintenant que tout ça est en place on va coder ces actions et créer les vues correspondantes !

Un petit mot sur le middleware auth que j’ai ajouté pour ces routes. Un middleware est une étape pour la requête HTTP qui arrive, ça permet d’accomplir des vérifications, ici on demande que l’accès à ces routes soit limitées aux utilisateurs authentifiés. Ceux qui ne le sont pas seront automatiquement renvoyés à la page de connexion. Pour la suite de ce tutoriel vous devrez donc créer un utilisateur avec les formulaires qu’on a déjà mis en place.

Remarquez que lorsqu’un utilisateur est connecté avec Breeze il se retrouve avec un Dashboard :

Si comme moi vous trouvez la traduction débile à adopter cette nouvelle norme pour soi-disant lisser les genres, alors qu’elle ne fait que complexifier et enlaidir une belle langue, vous pouvez la modifier.

L’utilisateur a aussi accès à une page de profil que je vous laisse découvrir.

Organisation des vues

Par défaut Breeze utilise Tailwind avec un thème spécifique et toutes les vues de l’authentification ainsi que le tableau de bord et le profil l’utilisent, on peut évidemment changer tout ça si on n’aime pas Tailwind, ce qui est d’ailleurs mon cas. Pour cet article je vais me contenter de prendre la situation telle qu’elle est proposée de base pour ne pas alourdir cet article, mais évidemment on reste totalement libre d’utiliser ce qu’on veut au niveau du frontend.

Laravel propose plusieurs façon d’organiser les vues, on peut les combiner avec un simple héritage ou utiliser des composants. Breeze met en place de nombreux composants et en fait une utilisation intensive, alors voyons un peu de quoi il s’agit. Si vous regardez la vue du dashboard (views/dashboard.blade.php) on a ce code :

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Dashboard') }}
        </h2>
    </x-slot>

    <div class="py-12">
        <div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
            <div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
                <div class="p-6 text-gray-900">
                    {{ __("You're logged in!") }}
                </div>
            </div>
        </div>
    </div>
</x-app-layout>

La syntaxe un peu particulière x-app-layout indique qu’on utilise un composant, mais lequel et où le trouver ?

Regardez ici :

Un composant possède classiquement une vue et une classe associée. Ici on s’intéresse à la classe AppLayout. Dans cette classe on a en paticulier ce code :

public function render(): View
{
    return view('layouts.app');
}

On voit qu’on retourne la vue views/layouts/app.blade.php. Donc quand j’utilise le composant <x-app-layout> je fais en fait appel à cette vue. Si vous ouvrez ce fichier vous allez trouver le code HTML de base de la page.

On trouve dans cette vue ce code :

@include('layouts.navigation')

Qui veut dire que l’on inclut ici la vue views/layouts/navigation.blade.php qui correspond à cette partie de la page :

Parmi les nombreux composants on en trouve un pour un contrôle simple de formulaire :

Le code en est simple :

@props(['disabled' => false])

<input {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) !!}>

Mais l’avantage est de mutualiser ce code et de l’utiliser pour tous les formulaires. Pour notre usage on va créer un composant équivalent pour les textarea :

Avec ce code :

@props(['disabled' => false])

<textarea {{ $disabled ? 'disabled' : '' }} {!! $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) !!}>{{ $slot }}</textarea>

On va aussi ajouter le composant tasks-card :

Avec ce code :

<div class="mt-8 flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
    <div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
        {{ $slot }}
    </div>
</div>

Je ne vais pas entrer dans tous les détails des vues, vous pouvez les trouver dans la documentation.

Créer une tâche

Le formulaire

Pour la création d’une tâche on va avoir besoin d’un formulaire. On crée un dossier tasks et la vue create :

Avec ce code :

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Create a task') }}
        </h2>
    </x-slot>

    <x-tasks-card>

        <!-- Message de réussite -->
        @if (session()->has('message'))
            <div class="mt-3 mb-4 list-disc list-inside text-sm text-green-600">
                {{ session('message') }}
            </div>
        @endif

        <form action="{{ route('tasks.store') }}" method="post">
            @csrf

            <!-- Titre -->
            <div>
                <x-input-label for="title" :value="__('Title')" />

                <x-text-input  id="title" class="block mt-1 w-full" type="text" name="title" :value="old('title')" required autofocus />

                <x-input-error :messages="$errors->get('title')" class="mt-2" />
            </div>

            <!-- Détail -->
            <div class="mt-4">
                <x-input-label for="detail" :value="__('Detail')" />

                <x-textarea class="block mt-1 w-full" id="detail" name="detail">{{ old('detail') }}</x-textarea>

                <x-input-error :messages="$errors->get('detail')" class="mt-2" />
            </div>

            <div class="flex items-center justify-end mt-4">
                <x-primary-button class="ml-3">
                    {{ __('Send') }}
                </x-primary-button>
            </div>
        </form>

    </x-tasks-card>
</x-app-layout>

Il y a beaucoup à dire sur ce code mais je ne vais pas entrer dans les détails.

Le formulaire est défini avec ce code :

<form action="{{ route('tasks.store') }}" method="post">

On utilise l’helper route qui, à partir du nom de la route, génère l’url. Pour insérer des éléments avec PHP on utilise des accolades.

On a ensuite cette commande Blade :

@csrf

On demande ici à Blade d’insérer un token dans un champ caché qui va permettre à Laravel de contrer les attaques CSRF, le code généré ressemble à ça :

<input type="hidden" name="_token" value="sUTuW5dYrt2l2iPgAJ5EVXn5UwMjEGFLAZaH4jHz">

C’est automatique et on n’a pas besoin de s’en préoccuper ensuite. Un middleware se charge à l’arrivée de ce contrôle de sécurité.

On va compléter le code de la fonction create du contrôleur TaskController pour appeler la vue :

public function create()
{
    return view('tasks.create');
}

Maintenant avec l’url todolist.oo/tasks/create on obtient la page avec le formulaire (si on est connecté !) :

On a avancé mais ça serait quand même mieux en français… On va compléter le fichier lang/fr.json :

{
    ...
    "Create a task": "Créer une tâche",
    "Title": "Titre",
    "Detail": "Détail",
    "Send": "Envoyer"
}

Et ça fonctionne :

La soumission

Pour créer effectivement la tâche on va devoir gérer la soumission du formulaire dans la méthode store du contrôleur :

use App\Models\Task;

...

public function store(Request $request)
{
    $data = $request->validate([
        'title' => 'required|max:100',
        'detail' => 'required|max:500',
    ]);

    $task = new Task;
    $task->title = $request->title;
    $task->detail = $request->detail;
    $task->save();

    return back()->with('message', "La tâche a bien été créée !");
}

On commence par vérifier les entrées avec la validation. Les deux champs sont requis et on vérifie une longueur maximale.

Ensuite on utilise Eloquent pour créer la tâche.

Pour finir on renvoie dans la vue.

Vous pouvez vérifier que la validation fonctionne  :

Si la validation est bonne, la tâche est créée et on retourne la même vue (back) mais cette fois on flashe une information en session (ce qui signifie qu’elle ne sera valable que pour la prochaine requête) avec with :

return back()->with('message', "La tâche a bien été créée !");

Dans la vue on vérifie s’il y a une information en session et si c’est le cas on l’affiche :

@if (session()->has('message'))
    <div class="mt-3 mb-4 list-disc list-inside text-sm text-green-600">
        {{ session('message') }}
    </div>
@endif

On trouve les données dans la table :

Par défaut l’état est à 0 (false). Remarquez aussi le renseignement automatique des deux dates.

Pour accéder à ce formulaire de création on va jouter un item dans le menu, ça se passe dans la vue views/layouts/navigation.blade.php :

    <!-- Navigation Links -->
    <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
        <x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
            {{ __('Dashboard') }}
        </x-nav-link>
    </div>
    <!-- Lien pour la création d'une tâche -->
    <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
        <x-nav-link :href="route('tasks.create')" :active="request()->routeIs('tasks.create')">
            {{ __('Create a task') }}
        </x-nav-link>
    </div>
</div>

Modifier une tâche

Le formulaire

Pour la modification d’une tâche on va avoir besoin d’un formulaire. On crée la vue edit :

Évidemment cette vue va beaucoup ressembler à celle de la création mais il va falloir renseigner les champ on va ajouter la possibilité de modifier l’état :

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            {{ __('Edit a task') }}
        </h2>
    </x-slot>

    <x-tasks-card>

        <!-- Message de réussite -->
        @if (session()->has('message'))
            <div class="mt-3 mb-4 list-disc list-inside text-sm text-green-600">
                {{ session('message') }}
            </div>
        @endif

        <form action="{{ route('tasks.update', $task->id) }}" method="post">
            @csrf
            @method('put')

            <!-- Titre -->
            <div>
                <x-input-label for="title" :value="__('Title')" />

                <x-text-input id="title" class="block mt-1 w-full" type="text" name="title" :value="old('title', $task->title)" required autofocus />
            
                <x-input-error :messages="$errors->get('title')" class="mt-2" />
            </div>

            <!-- Détail -->
            <div class="mt-4">
                <x-input-label for="detail" :value="__('Detail')" />

                <x-textarea class="block mt-1 w-full" id="detail" name="detail">{{ old('detail', $task->detail) }}</x-textarea>
                
                <x-input-error :messages="$errors->get('detail')" class="mt-2" />            
            </div>

            <!-- Tâche accomplie -->
            <div class="block mt-4">
                <label for="state" class="inline-flex items-center">
                    <input id="state" type="checkbox" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50" name="state" @if(old('state', $task->state)) checked @endif>
                    <span class="ml-2 text-sm text-gray-600">{{ __('Task done') }}</span>
                </label>
            </div>

            <div class="flex items-center justify-end mt-4">
                <x-primary-button class="ml-3">
                    {{ __('Send') }}
                </x-primary-button>
            </div>
        </form>

    </x-tasks-card>
</x-app-layout>

Dans la déclaration du formulaire on a changé l’url :

<form action="{{ route('tasks.update', $task->id) }}" method="post">

On précise la route et il faut ajouter l’identifiant de la tâche à modifier ($task->id). Remarquez que la route attend une méthode PUT mais que là on déclare une méthode POST. C’est parce que les navigateurs gèrent encore très mal ce genre de méthode, alors on déclare un POST mais après on précise qu’on veut un PUT :

@method('put')

Cela crée un champ caché qui va servir à Laravel pour savoir de quelle méthode il s’agit :

<input type="hidden" name="_method" value="put">

Pour le reste ça ne change pas beaucoup de la création, sauf la case à cocher ajoutée.

Dans le contrôleur on appelle la vue :

public function edit(Task $task)
{
    return view('tasks.edit', compact('task'));
}

Remarquez le paramètre qui est du type du modèle Task. C’est une façon de dire à Laravel : le paramètre transmis est un nombre mais en fait c’est l’identifiant d’une instance de Task. Du coup Eloquent peut aller chercher dans la table la tâche correspondante. Il suffit ensuite de transmettre ça à la vue.

Maintenant avec une url de la forme todolist.oo/tasks/1/edit on atteint le formulaire.

La soumission

Pour modifier effectivement la tâche on va devoir gérer la soumission du formulaire dans la méthode update du contrôleur :

public function update(Request $request, Task $task)
{
    $data = $request->validate([
        'title' => 'required|max:100',
        'detail' => 'required|max:500',
    ]);

    $task->title = $request->title;
    $task->detail = $request->detail;
    $task->state = $request->has('state');
    $task->save();

    return back()->with('message', "La tâche a bien été modifiée !");        
}

On a la même validation que pour la création. D’ailleurs il faudrait pour bien faire mutualiser ce code pour éviter cette redondance qui n’est jamais une bonne chose (principe DRY). Laravel dispose d’une stratégie élégante pour la validation avec les Form Request, vous pouvez trouver les détails dans la documentation.

On met à jour les valeurs de la tâche. Pour la case à cocher il faut savoir qu’on ne retrouve une valeur que si elle est cochée. Donc on va vérifier si la valeur existe pour mettre à jour dans la table.

On va aussi compléter les traductions :

    "Task done": "Tâche accomplie"
}

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.

On crée la vue :

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            @lang('Show a task')
        </h2>
    </x-slot>

    <x-tasks-card>
        <h3 class="font-semibold text-xl text-gray-800">@lang('Title')</h3>
        <p>{{ $task->title }}</p>
        <h3 class="font-semibold text-xl text-gray-800 pt-2">@lang('Detail')</h3>
        <p>{{ $task->detail }}</p>
        <h3 class="font-semibold text-xl text-gray-800 pt-2">@lang('State')</h3>
        <p>
          @if($task->state)
            La tâche a été accomplie !
          @else
            La tâche n'a pas encore été accomplie.
          @endif
        </p>
        <h3 class="font-semibold text-xl text-gray-800 pt-2">@lang('Date creation')</h3>
        <p>{{ $task->created_at->format('d/m/Y') }}</p>
        @if($task->created_at != $task->updated_at)
          <h3 class="font-semibold text-xl text-gray-800 pt-2">@lang('Last update')</h3>
          <p>{{ $task->updated_at->format('d/m/Y') }}</p>
        @endif
    </x-tasks-card>
</x-app-layout>

On complète le contrôleur :

public function show(Task $task)
{
    return view('tasks.show', compact('task'));
}

Et avec une url de la forme todolist.oo/tasks/1 on affiche les éléments d’une tâche :

On a encore besoin d’ajouter quelques traductions :

    "Task done": "Tâche accomplie",
    "Show a task": "Voir une tâche",
    "State": "Etat",
    "Date creation": "Date de création",
    "Last update": "Dernière modification"
}

On n’affiche la date de mise à 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.

Au niveau du contrôleur on va récupérer toutes les tâches et les envoyer dans une vue :

public function index()
{
    $tasks = Task::all();

    return view('tasks.index', compact('tasks'));
}

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 va ajouter un composant pour les boutons dans la liste :

Avec ce code :

<td class="px-2 py-2">
    <a role="button" {{ $attributes->merge(['class' => 'inline-flex items-center px-2 py-1 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150']) }}>
        {{ $slot }}
    </a>
</td>

On crée la vue :

<x-app-layout>
    <x-slot name="header">
        <h2 class="font-semibold text-xl text-gray-800 leading-tight">
            @lang('Tasks List')
        </h2>
    </x-slot>
    <div class="container flex justify-center mx-auto">
      <div class="flex flex-col">
          <div class="w-full">
              <div class="border-b border-gray-200 shadow pt-6">
                <table>
                  <thead class="bg-gray-50">
                    <tr>
                      <th class="px-2 py-2 text-xs text-gray-500">#</th>
                      <th class="px-2 py-2 text-xs text-gray-500">@lang('Title')</th>
                      <th class="px-2 py-2 text-xs text-gray-500">Etat</th>
                      <th class="px-2 py-2 text-xs text-gray-500"></th>
                      <th class="px-2 py-2 text-xs text-gray-500"></th>
                      <th class="px-2 py-2 text-xs text-gray-500"></th>
                    </tr>
                  </thead>
                  <tbody class="bg-white">
                    @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">{{ $task->title }}</td>
                        <td class="px-4 py-4">@if($task->state) {{ __('Done') }} @else {{ __('To do') }} @endif</td>
                        <x-link-button href="{{ route('tasks.show', $task->id) }}">
                            @lang('Show')
                        </x-link-button>
                        <x-link-button href="{{ route('tasks.edit', $task->id) }}">
                            @lang('edit')
                        </x-link-button>
                        <x-link-button onclick="event.preventDefault(); document.getElementById('destroy{{ $task->id }}').submit();">
                            @lang('Delete')
                        </x-link-button>
                        <form id="destroy{{ $task->id }}" action="{{ route('tasks.destroy', $task->id) }}" method="POST" style="display: none;">
                            @csrf
                            @method('DELETE')
                        </form>
                      </tr>
                    @endforeach
                  </tbody>
                </table>
              </div>
          </div>
      </div>
</x-app-layout>

On se retrouve avec un tableau des tâches :

On va ajouter quelques traductions :

    "Done": "Effectuée",
    "Tasks List": "Liste des tâches",
    "To do": "A faire",
    "Show": "Voir",
    "Edit": "Editer"
}

On ajoute un item dans le menu (navigation.blade.php) :

    <!-- Lien pour la liste des tâches -->
    <div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
        <x-nav-link :href="route('tasks.index')" :active="request()->routeIs('tasks.index')">
            {{ __('Tasks list') }}
        </x-nav-link>
    </div>
</div>

Une petite traduction :

"Tasks list": "Liste des tâches"

Les boutons permettent d’accéder aux tâches qu’on a codées précédemment sauf la suppression. On va coder pour cela cette fonction dans le contrôleur :

public function destroy(Task $task)
{
    $task->delete();
}

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 URLs en français

On a vu que pour la création on utilise une url de la forme …tasks/create et pour la modification …tasks/1/edit. Il serait plus élégant d’avoir des mots en français là aussi. On peut le réaliser en ajoutant ce code dans le fichier AppServiceProvider.php :

use Illuminate\Support\Facades\Route;

...

public function boot(): void
{
    Route::resourceVerbs([
        'edit' => 'modification',
        'create' => 'creation',
    ]);
}

Maintenant les urls sont de la forme …tasks/creation et pour la modification …tasks/1/modification.

Les tests

Laravel permet de faire facilement de tests. Il utilise 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à de nombreux tests. La plupart ont été ajoutés par Breeze et concernent l’authentification. Voyons comment sont constitués ces tests en analysant la classe AuthenticationTest :

class AuthenticationTest extends TestCase
{
    use RefreshDatabase;

    public function test_login_screen_can_be_rendered()
    {
        $response = $this->get('/login');

        $response->assertStatus(200);
    }
    ...

On commence par vider la base de données avec RefreshDatabase.

Ensuite on envoie une requête HTTP GET sur l’url /login. Le test consiste à vérifier (assertStatus) qu’on a bien une réponse 200.

Ca correspond à cette ligne dans les tests :

Remarquez l’importance du libellé de la méthode (test_login_screen_can_be_rendered) pour obtenir un texte explicite dans le test (login screen can be rendered).

Voyons la seconde méthode :

public function test_users_can_authenticate_using_the_login_screen()
{
    $user = User::factory()->create();

    $response = $this->post('/login', [
        'email' => $user->email,
        'password' => 'password',
    ]);

    $this->assertAuthenticated();
    $response->assertRedirect(RouteServiceProvider::HOME);
}

Là on veut vérifier qu’un utilisateur peut effectivement se connecter. On commence donc par en créer un :

$user = User::factory()->create();

Ensuite on envoie un requête POST sur l’url /login en précisant les identifiant de l’utilisateur qu’on vient de créer :

$response = $this->post('/login', [
    'email' => $user->email,
    'password' => 'password',
]);

On vérifie ensuite qu’il est bien authentifié :

$this->assertAuthenticated();

Laravel propose de nombreuse assertions, vous les trouvez ici.

On vérifie enfin qu’on a la bonne redirection :

$response->assertRedirect(RouteServiceProvider::HOME);

On retrouve ce test ici :

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 va prévoir cette fonction dans la classe :

...

use App\Models\User;

class CreateTaskTest extends TestCase
{
    use RefreshDatabase;

    public function test_auth_can_create_task()
    {
        $user = User::factory(\App\User::class)->create();

        $response = $this->actingAs($user)->post('/tasks', [
            'title' => 'Ma nouvelle tâche',
            'detail' => 'Tous les détails 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 :

  • un utilisateur authentifié soumet le formulaire
  • 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. 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.

 

 

 

Print Friendly, PDF & Email

4 commentaires

  • Jean Dugenet

    Bonjour,
    Je voudrais savoir si vous pourriez être intéressé par la réalisation d’un site web de « jounalisme citoyen ». Il s’agit d’un blog multi-auteurs ouvert aux commentaires avec un systèem de modération incluant une procédure participative de sélection des articles choisis. Je suis en mesure de vous donnez une description assez précise du projet.

  • Bougla

    Bonjour bestmomo,

    Tout merci pour vos contenus qui sont d’une très grande qualité et d’une grande aide pour des novices comme moi 🙂

    Je recontre un problème avec la suppression d’une tâche, malgré la présence dans la balise form de la vue Liste des tâches des directives @csrf et @method(‘DELETE’). J’ai le message d’erreur « The POST method is not supported for route tasks/8. Supported methods: GET, HEAD, PUT, PATCH, DELETE. »

    Comment puis-je solutionner ce problème ?

    • bestmomo

      Bonjour et merci d’apprécier !

      Pour le souci, le message d’erreur indique un problème dans les informations transmises par l’action sur le bouton DELETE. Comme les pages HTML ne supportent pas les actions PUT, PATCH, ou DELETE, on utilise un subterfuge en utilisant POST, mais en ajoutant un input caché :

      input type= »hidden » name= »_method » value= »DELETE »

      Pour nous simplifier la vie, Laravel possède une commande Blade : @method('DELETE')

      À l’arrivée, Laravel repère cet input et sait qu’il s’agit non pas d’un POST, mais d’un DELETE.

      Il faudrait voir ce qui se passe en utilisant les outils de débogage du navigateur (en général appelés avec F12). En regardant ce qui passe avec l’onglet Réseau, on doit voir quelque chose comme ça :

      _token=Fdfxg7qfiIvyYwD64T8bxylzjoHRzWmJ7tBhyGiY&_method=DELETE

Laisser un commentaire