Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Cours Laravel 11 – les données – les ressources (1/2)
Samedi 16 mars 2024 18:48

Dans ce chapitre nous allons commencer à étudier les ressources qui permettent de créer des routes "CRUD" (Create, Read, Update, Delete) adaptées à la persistance de données. Comme exemple pratique nous allons prendre le cas d'une table de films.

Pour vous simplifier la vie voilà le code à la fin de cet article.

Les données

On repart d'un Laravel vierge et on crée une base comme on l'a vu précédemment. Appelons la par exemple laravel11 pour faire original. On renseigne le fichier .env en conséquence :

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

La migration

On va créer avec Artisan le modèle Film en même temps que la migration :

php artisan make:model Film --migration

Pour faire simple on va se contenter de 3 colonnes pour le titre du film, son année de sortie et sa description :

public function up(): void
{
    Schema::create('films', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->year('year');
        $table->text('description');
        $table->timestamps();
    });
}

On a les champs :

  • id : entier auto-incrémenté qui sera la clé primaire de la table,
  • title : texte pour le nom du film,
  • year : année de sortie du film,
  • description : description du film,
  • created_at et updated_at créés par la méthode timestamps,

Ensuite on lance la migration :

On a la création des tables de base de Laravel qu'on a déjà vues et qui ne nous intéressent pas pour cet article. Mais on voit aussi la création de la table films :

Le modèle

Le modèle Film qu'on a créé est vide au départ (il n'y a que le trait pour le factory). On va se contenter de prévoir l'assignement de masse avec la propriété $fillable :
protected $fillable = ['title', 'year', 'description'];

La population

Pour nos essais on va remplir un peu la table avec quelques films. On va créer un factory :
php artisan make:factory FilmFactory
On a ce code de base :
<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Film>
 */
class FilmFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            //
        ];
    }
}
On va compléter ainsi :
public function definition(): array
{
    return [
        'title' => fake()->sentence(2, true),           
        'year' => fake()->year,           
        'description' => fake()->paragraph(),
    ];
}
On change ensuite le code du seeder (database/seeds/DatabaseSeeder.php) :
use App\Models\Film;

...

public function run(): void
{
    Film::factory()->count(10)->create();
}
Il ne reste plus qu'à lancer la population :
php artisan db:seed
Si tout va bien on se retrouve avec 10 films dans la table :

Une ressource

Le contrôleur

On va maintenant créer un contrôleur de ressource avec Artisan :

php artisan make:controller FilmController --resource

C'est la commande qu'on a déjà vue pour créer un contrôleur avec en plus l'option --resource.

Vous trouvez comme résultat le contrôleur app/Http/Controllers/FilmController :

Avec ce code :

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class FilmController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index()
    {
        //
    }

    /**
     * Show the form for creating a new resource.
     */
    public function create()
    {
        //
    }

    /**
     * Store a newly created resource in storage.
     */
    public function store(Request $request)
    {
        //
    }

    /**
     * Display the specified resource.
     */
    public function show(string $id)
    {
        //
    }

    /**
     * Show the form for editing the specified resource.
     */
    public function edit(string $id)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     */
    public function update(Request $request, string $id)
    {
        //
    }

    /**
     * Remove the specified resource from storage.
     */
    public function destroy(string $id)
    {
        //
    }
}

Les 7 méthodes créées couvrent la gestion complète des films:

  • index : pour afficher la liste des films,

  • create : pour envoyer le formulaire pour la création d'un nouveau film,

  • store : pour créer un nouveau film,

  • show : pour afficher les données d'un film,

  • edit : pour envoyer le formulaire pour la modification d'un film,

  • update : pour modifier les données d'un film,

  • destroy : pour supprimer un film.

Les routes

Pour créer toutes les routes il suffit de cette unique ligne de code :

use App\Http\Controllers\FilmController;

Route::resource('films', FilmController::class);

On va vérifier ces routes avec Artisan :

Vous trouvez 7 routes, avec chacune une méthode et une url, qui pointent sur les 7 méthodes du contrôleur. Notez également que chaque route a aussi un nom qui peut être utilisé par exemple pour une redirection.

On peut obtenir plus d'information avec la commande "verbose" :

php artisan route:list -v

On retrouve aussi pour chaque route le middleware web dont je vous ai déjà parlé.

Nous allons à présent considérer chacune de ces routes et créer la gestion des données, les vues, et le code nécessaire au niveau du contrôleur.

La validation

On va créer une requête de formulaire pour la création ou la modification d'un film :
php artisan make:request FilmRequest
On a deux champs à vérifier : le titre et l'année. A priori il n'y a aucune différence de validation pour la création et la modification. Voilà le code modifié pour cette classe :
<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class FilmRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, \Illuminate\Contracts\Validation\Rule|array|string>
     */
    public function rules(): array
    {
        return [
            'title' => ['required', 'string', 'max:100'],
            'year' => ['required', 'numeric', 'min:1950', 'max:' . date('Y')],
            'description' => ['required', 'string', 'max:500'],
        ];
    }
}
On prévoie comme règles :
  • title : le champ est obligatoire (required), ça doit être du texte (string), le nombre maximum de caractères (max) doit être de 100
  • year : le champ est obligatoire (required), ça doit être un nombre (number), la valeur minimale (min) doit être 1950, la valeur maximale (max) doit être l'année actuelle (date('Y'))
  • description : le champ est obligatoire (required), ça doit être du texte (string), le nombre maximum de caractères (max) doit être de 500
On pourra injecter cette classe dans les méthodes du contrôleur.

Le template

Laravel s'occupe essentiellement du côté serveur et n'impose rien côté client, même s'il propose des choses. Autrement dit on peut utiliser Laravel avec n'importe quel système côté client. Pour notre exemple je vous propose d'utiliser Bulma pour la mise en forme. Voici un template qui va nous servir pour toutes nos vues :
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Films</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
    @yield('css')
  </head>
  <body>
    <main class="section">
        <div class="container">
            @yield('content')
        </div>
    </main>
  </body>
</html>

La liste des films

La route

La liste des films correspond à cette route :

Le contrôleur

Dans le contrôleur c'est la méthode index qui est concernée. On va donc la coder :
...
use App\Models\Film;
use Illuminate\View\View;

class FilmController extends Controller
{
    public function index(): View
    {
        $films = Film::all();
        return view('index', compact('films'));
    }
...
On va chercher tous les films avec la méthode all du modèle, on appelle la vue index en lui transmettant les films.

La vue index

On crée la vue index : Avec ce code :
@extends('template')

@section('content')
    <div class="card">
        <header class="card-header">
            <p class="card-header-title">Films</p>
        </header>
        <div class="card-content">
            <div class="content">
                <table class="table is-hoverable">
                    <thead>
                        <tr>
                            <th>#</th>
                            <th>Titre</th>
                            <th></th>
                            <th></th>
                            <th></th>
                        </tr>
                    </thead>
                    <tbody>
                        @foreach($films as $film)
                            <tr>
                                <td>{{ $film->id }}</td>
                                <td><strong>{{ $film->title }}</strong></td>
                                <td><a class="button is-primary" href="{{ route('films.show', $film->id) }}">Voir</a></td>
                                <td><a class="button is-warning" href="{{ route('films.edit', $film->id) }}">Modifier</a></td>
                                <td>
                                    <form action="{{ route('films.destroy', $film->id) }}" method="post">
                                        @csrf
                                        @method('DELETE')
                                        <button class="button is-danger" type="submit">Supprimer</button>
                                    </form>
                                </td>
                            </tr>
                        @endforeach
                    </tbody>
                </table>
            </div>
        </div>
    </div>
@endsection
Quelques remarques concernant le code :
  • on utilise la directive @foreach pour faire une boucle sur tous les films
  • la méthode route qui génère une url selon la route peut être accompagnée d'un paramètre, par exemple route('films.show', $film->id) permet de générer l'url de la forme .../films/id
  • les formulaire HTML ne supportent pas les verbes PUT, PATCH et DELETE, du coup on doit utiliser le verbe POST et prévoir dans le formulaire un input caché qui indique le verbe à utiliser en réalité, la directive @method permet de facilement mettre ça en place, on s'en sert pour le bouton de suppression

La pagination

Ici on n'a que 10 films mais imaginez qu'on en ait des centaines ou des milliers ! Dans ce cas une pagination serait la bienvenue. Laravel est bien équipé pour ça. Au niveau du contrôleur le changement est facile :
$films = Film::paginate(5);
On a remplacé la méthode all par paginate en indiquant en paramètre le nombre d'enregistrement par page. Ensuite dans la vue il suffit de prévoir ce code :
        </div>
        <footer class="card-footer">
            {{ $films->links() }}
        </footer>
    </div>
@endsection
Mais le souci c'est que par défaut le marquage généré est celui qui convient à Tailwind, du coup avec Bulma on obtient ça : Ce qui n'est pas du meilleur effet ! Il nous faut donc modifier la vue qui génère ce code. Comme nous ne sommes pas les premiers à avoir ce souci il suffit de chercher sur la toile et on trouve facilement. On commence par publier les vues :
php artisan vendor:publish --tag=laravel-pagination
On se rend compte d’ailleurs qu'il y en a une de prévue pour Semantic qui est aussi un superbe framework, de même pour Bootstrap qui n'est plus à présenter. Pour terminer on remplace le code du fichier tailwind.blade.php par celui-ci :
@if ($paginator->hasPages())
    <nav class="pagination is-centered" role="navigation" aria-label="pagination">
        {{-- Previous Page Link --}}
        <a class="pagination-previous" {{ $paginator->onFirstPage() ? "disabled" : "" }}
        href="{{ $paginator->previousPageUrl() }}">
            @lang('pagination.previous')
        </a>

        <a class="pagination-next" {{ $paginator->hasMorePages() ? "" : "disabled" }}
        href="{{ $paginator->nextPageUrl() }}">
            @lang('pagination.next')
        </a>
        {{-- Next Page Link --}}

        {{-- Pagination Elements --}}
        <ul class="pagination-list">
            @foreach ($elements as $element)
                {{-- "Three Dots" Separator --}}
                @if (is_string($element))
                    <li>
                        <span class="pagination-ellipsis">{{ $element }}</span>
                    </li>
                @endif

                {{-- Array Of Links --}}
                @if (is_array($element))
                    @foreach ($element as $page => $url)
                        <li>
                            <a class="pagination-link {{ $page == $paginator->currentPage() ? "is-current" : "" }}"
                               href="{{ $url }}" aria-label="Goto page {{ $page }}">{{ $page }}</a>
                        </li>
                    @endforeach
                @endif
            @endforeach
        </ul>
    </nav>
@endif
Maintenant c'est un peu mieux : Ça mérite juste d'être un peu aéré et centré. Comme Bulma utilise Flex on va ajouter quelques règles dans la vue index :
@section('css')
    <style>
        .card-footer {
            justify-content: center;
            align-items: center;
            padding: 0.4em;
        }
    </style>
@endsection
Maintenant c'est plus joli : Mais ça serait encore mieux en français ! On a déjà vu dans ce cours comment faire mais je le rappelle. On commence avec cette installation :
composer require laravel-lang/common --dev
Puis on installe le français. Dans le fichier .env :
APP_LOCALE=fr
Et on met à jour :
php artisan lang:update
Maintenant c'est parfait !

L'affichage d'un film

On va voir maintenant l'affichage les données d'un film. On y accède à partir du bouton Voir.

La route

La liste des films correspond à cette route :

Le contrôleur

Dans le contrôleur c'est la méthode show qui est concernée. On va donc la coder. Il me faut toutefois préciser déjà un point important. Dans la version du contrôleur générée par défaut on voit que le film au niveau des arguments des fonctions est référencé par son identifiant, par exemple :
public function show($id)
La variable id contient la valeur passée dans l’url. Par exemple …/films/8 indique qu’on veut voir les informations du film d’identifiant 8. Il suffit donc ensuite d’aller chercher dans la base le film correspondant. On va utiliser une autre stratégie :
public function show(Film $film)
L’argument cette fois est une instance du modèle App\Models\Film. Etant donné qu’il rencontre ce type, Laravel va automatiquement livrer une instance du modèle pour le film concerné ! C’est ce qu’on appelle liaison implicite (Implicit Bindind). Vous voyez encore là à quel point Laravel nous simplifie la vie. Du coup la méthode devient très simple à coder :
public function show(Film $film): View
{
    return view('show', compact('film'));
}

La vue show

On crée cette vue : Avec ce code :
@extends('template')

@section('content')
    <div class="card">
        <header class="card-header">
            <p class="card-header-title">Titre : {{ $film->title }}</p>
        </header>
        <div class="card-content">
            <div class="content">
                <p>Année de sortie : {{ $film->year }}</p>
                <hr>
                <p>{{ $film->description }}</p>
            </div>
        </div>
    </div>
@endsection
Sobre et efficace. J'aurais pu prévoir un bouton de retour mais les navigateurs font déjà ça très bien.

Supprimer un film

La route

La suppression d'un film correspond à cette route :

Le contrôleur

Dans le contrôleur c'est la méthode destroy qui est concernée. On va donc la coder :
use Illuminate\Http\RedirectResponse;

...

public function destroy(Film $film): RedirectResponse
{
    $film->delete();

    return back()->with('info', 'Le film a bien été supprimé dans la base de données.');
}
Comme pour la méthode show on utilise une liaison implicite et on obtient du coup immédiatement une instance du modèle. Comme c'est un peu brutal comme suppression il peut être judicieux de fournir un message de confirmation pour l'utilisateur. Je ne vais pas le faire ici pour ne pas trop alourdir le projet et il s'agit uniquement de traitement côté client. Par contre après la suppression il faut afficher quelque chose pour dire que l'opération s'est réalisée correctement. On voit qu'il y a une redirection avec la méthode back qui renvoie la même page. D'autre part la méthode with permet de flasher une information dans la session. Cette information ne sera valide que pour la requête suivante. Dans notre vue index on va prévoir quelque chose pour afficher cette information :
@section('content')
    @if(session()->has('info'))
        <div class="notification is-success">
            {{ session('info') }}
        </div>
    @endif
    <div class="card">
La directif @if permet de déterminer si une information est présente en session, et si c'est le cas de l'afficher : Classiquement on prévoit un bouton pour fermer la barre de notification. Là encore je vais simplifier parce c'est aussi un traitement purement client. Il suffit d'ajouter un peu de Javascript pour le faire (vous pouvez consulter la documentation de Bulma sur le sujet). Dans le prochain article on verra comment créer et modifier un film.

En résumé

  • Une ressource dans Laravel est constituée d'un contrôleur comportant les 7 méthodes permettant une gestion complète.
  • Les routes vers une ressource sont créées avec une simple ligne de code.
  • Laravel permet de mettre en place facilement une pagination, il faut adapter l'apparence en fonction du framework CSS qu'on utilise.
  • On peut mettre en place dans le routage une liaison implicite pour générer automatiquement une instance de la classe dont l'identifiant est passée dans l'url.


Par bestmomo

Aucun commentaire