
Cours Laravel 8 – les données – les ressources (1/2)
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.
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 laravel8 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=laravel8 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() { 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 --model=Film
Comme on a déclaré le modèle le code est déjà bien avancé :
<?php namespace Database\Factories; use App\Models\Film; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Str; class FilmFactory extends Factory { /** * The name of the factory's corresponding model. * * @var string */ protected $model = Film::class; /** * Define the model's default state. * * @return array */ public function definition() { return [ // ]; } }
Pour ceux qui ont travaillé avec des versions précédentes de Laravel les factories ont été modifiés complètement pour devenir des classes PHP.
On va compléter ainsi :
public function definition() { return [ 'title' => $this->faker->sentence(2, true), 'year' => $this->faker->year, 'description' => $this->faker->paragraph(), ]; }
On change ensuite le code du seeder (database/seeds/DatabaseSeeder.php) :
use App\Models\Film; ... public function run() { Film::factory(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 :
<?php namespace App\Http\Controllers; use Illuminate\Http\Request; class FilmController extends Controller { /** * Display a listing of the resource. * * @return \Illuminate\Http\Response */ public function index() { // } /** * Show the form for creating a new resource. * * @return \Illuminate\Http\Response */ public function create() { // } /** * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { // } /** * Display the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function show($id) { // } /** * Show the form for editing the specified resource. * * @param int $id * @return \Illuminate\Http\Response */ public function edit($id) { // } /** * Update the specified resource in storage. * * @param \Illuminate\Http\Request $request * @param int $id * @return \Illuminate\Http\Response */ public function update(Request $request, $id) { // } /** * Remove the specified resource from storage. * * @param int $id * @return \Illuminate\Http\Response */ public function destroy($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 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 Film
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 Film extends FormRequest { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { 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://cdnjs.cloudflare.com/ajax/libs/bulma/0.9.0/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; class FilmController extends Controller { public function index() { $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 ce repo.
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 --}} @if ($paginator->onFirstPage()) <a class="pagination-previous" disabled>@lang('pagination.previous')</a> @else <a class="pagination-previous" href="{{ $paginator->previousPageUrl() }}">@lang('pagination.previous')</a> @endif {{-- Next Page Link --}} @if ($paginator->hasMorePages()) <a class="pagination-next" href="{{ $paginator->nextPageUrl() }}">@lang('pagination.next')</a> @else <a class="pagination-next" disabled>@lang('pagination.next')</a> @endif {{-- Pagination Elements --}} <ul class="pagination-list"> @foreach ($elements as $element) {{-- "Three Dots" Separator --}} @if (is_string($element)) <li><span class="pagination-ellipsis">…</span></li> @endif {{-- Array Of Links --}} @if (is_array($element)) @foreach ($element as $page => $url) @if ($page == $paginator->currentPage()) <li><a class="pagination-link is-current" aria-label="Goto page {{ $page }}">{{ $page }}</a></li> @else <li><a href="{{ $url }}" class="pagination-link" aria-label="Goto page {{ $page }}">{{ $page }}</a></li> @endif @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 qu’on peut récupérer les fichiers de langue ici. On copie le dossier ici :
Et dans config/app.php on change cette ligne :
'locale' => 'fr',
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) { return view('show', compact('film')); }
La vue show
On crée cette vue :
@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 :
public function destroy(Film $film) { $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.


24 commentaires
bertrando51350
Deja merci pour ce tuto complet :
J’utilise ce tuto pour modeliser des evenements
Tout fonctionner l’index etc mais quand j’ai fais la fonction destroy j’ai cette erreur quand je veux afficher l’index :
Property [id] does not exist on this collection instance. En gros tous les attributs de ma table ne sont reconnus j’ai l’impression.
J’ai essyayer php artisan migrate:fresh ou de supprimer et recreer la table mais toujours cette erreur.
Que faire ?
noubis
bonsoir merci pour ce cours. je rencontre actuellement un probleme lors de l’affichage de ‘index.blade.php’ on me signal cette erreur: ‘ »Possible typo $fimls
Did you mean $films? » et »$fimls is undefined ».
j’aimerais savoir d’ou vient l’erreur svp.
merci d’avance.
bestmomo
Bonjour,
Apparemment, il y a une erreur de saisie, tu as tapé $fimls au lieu de $films dans le code.
noubis
bonsoir merci je ne l’avais pas vu du tout vu.
mais maintenant que jai rétifié mon affiche cette erreur :Undefined property: Illuminate\Pagination\LengthAwarePaginator::$id (View: C:\laragon\www\laravel8\resources\views\index.blade.php).
jai encore peut etre fait une erreur quelque part svp aidez-moi. merci d’avance
bestmomo
Il faudrait vérifier la bonne écriture du code au niveau de la pagination.
fred_s
Bonjour,
Je rencontre un petit problème dans le chapitre « L’affichage d’un film ».
J’ai l’impression que le lien avec l’instance du modèle App\Models\Film ne fonctionne pas, lorsque j’affiche la fiche d’un film, j’ai bien la page, mais aucune données ne sont récupérées pour l’élément affiché (pas de titre,etc…).
Avez-vous une idée d’ou cela peut provenir ?
Merci d’avance.
fred_s
J’ai finalement trouvé, juste un petit probleme de naming a priori
fabBlab
Bonjour,
Toujours aussi bon ce tuto 🙂
D’abord une petite erreur, qui sent le copier/coller malheureux :
id : entier auto-incrémenté qui sera la clé primaire de la table,
name : texte pour le nom,
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
Il n’y a pas de champ « name » par la suite.
Et une question concernant l’affirmation : « On peut limiter le nombre de routes si on n’a pas besoin de toutes les avoir. »
J’ai essayé de simplement commenter une des méthodes du contrôleur, dans mon exemple destroy().
Mais la route apparait toujours :
| DELETE | films/{film} | films.destroy | App\Http\Controllers\FilmController@destroy | web
C’est un détail, mais cela peut être utile dans certains cas.
Merci.
bestmomo
Bonjour,
Merci pour la coquille, c’est corrigé.
Pour les routes il faut agir au niveau du fichier des routes en utilisant only ou except, c’est expliqué ici dans la documentation.
fabBlab
Merci ! Je n’ai pas regardé au bon endroit dans la doc.
foxbille
Bonjour,
J’ai une question sur la génération du lien par laravel quand on code un href dans une vue
id) }} »>Voir
Quand la route porte sur les fonctions du « controller » générées par la commande
php artisan make:controller xxxxController –resource
Ca marche très bien, mais lorsqu’il s’agit d’une fonction autre, comme celle de mon exemple, le lien généré n’est pas conforme :
http://localhost/test/laravel8/xxxxx/public/ecritures/liste?2
je recopie ici les lignes du routeur …/route/web.php
[…]
Route::get(‘ecritures/{compte_id}/liste’, [App\Http\Controllers\EcrituresController::class, ‘liste’])->name(‘ecritures.liste’);
Route::get(‘ecritures/liste’, [App\Http\Controllers\EcrituresController::class, ‘liste’])->name(‘ecritures.liste’);
Route::resource(‘comptes’, ComptesController::class)->middleware(‘auth’);
[…]
Qu’est ce que je n’ai pas fait comme il le fallait ?
Merci pour ces pages très utiles, la doc n’étant vraiment pas suffisante pour mon niveau de compétence.
Eric
bestmomo
Bonjour,
Le code de l’exemple n’est pas passé dans le commentaire. Sous quelle forme apparaissent les urls « conformes » ? Y a-t-il en local un host créé ?
foxbille
Bonjour,
Les liens sont, pour celui qui ne marche pas :
route(‘ecritures.liste’, $compte->id)
et pour celui qui marche
route(‘comptes.edit’, $compte->id)
En fait, pour reproduire le problème, créer un controller :
php artisan make:controller FilmController –resource
ajouter une function identique à index(), par exemple liste($id) donc, qui prend un parametre : id
et créer une vue avec un lien pour déclencher la function liste() et un autre pour edit().
lorsqu’on passe la souris sur les liens, l’url est differente.
Pour liste() c’est ../liste?2 et pour edit : ../2/edit
Je n’ai pas créé de host. Je ne le fait jamais et ça marche toujours… sauf là ! 🙂 ca vient de là ?
Merci pour la réponse
Eric
bestmomo
Salut,
Ca donne quoi la liste des routes avec php artisan route:list ?
foxbille
voila un extrait, désolé pour le formatage
| | POST | ecritures | ecritures.store | App\Http\Controllers\EcrituresController@store | web |
| | GET|HEAD | ecritures | ecritures.index | App\Http\Controllers\EcrituresController@index | web |
| | GET|HEAD | ecritures/create | ecritures.create | App\Http\Controllers\EcrituresController@create | web |
| | GET|HEAD | ecritures/liste | ecritures.liste | App\Http\Controllers\EcrituresController@liste | web |
| | GET|HEAD | ecritures/{compte_id}/liste | ecritures.liste | App\Http\Controllers\EcrituresController@liste | web |
| | GET|HEAD | ecritures/{ecriture} | ecritures.show | App\Http\Controllers\EcrituresController@show | web |
| | PUT|PATCH | ecritures/{ecriture} | ecritures.update | App\Http\Controllers\EcrituresController@update | web |
| | DELETE | ecritures/{ecriture} | ecritures.destroy | App\Http\Controllers\EcrituresController@destroy | web |
| | GET|HEAD | ecritures/{ecriture}/edit | ecritures.edit | App\Http\Controllers\EcrituresController@edit | web |
| | GET|HEAD | ecritures/{id}/clone | ecritures.clone | App\Http\Controllers\EcrituresController@clone | web
bestmomo
Tu as deux routes avec le même nom : ecritures.liste
foxbille
Ok, merci ! désolé d’avoir été si long…
delacoche
bonjour , encore une fois merci beaucoup pour ce cour .
j’aimerais une aide , quand j’ajoute :
public function show(Film $film)
{
return view(‘show’, compact(‘film’));
}
dans le FilmController , cela est surligné en rouge , montrant une erreur. comment puis je remedier à cela?
bestmomo
Bonjour,
Le surlignement en rouge dépend de l’éditeur utilisé qui n’arrive pas forcément à trouver les dépendances. Il existe un package pour aider les IDE à s’y retrouver. Sinon avec Visual Studio Code en ajoutant les bons plugins ça se passe bien.
delacoche
merci , mais je travaille bien avec vsCode
delacoche
App\Http\Controllers\FilmController::show
Display the specified resource.
<?php
public function show($id) { }
@param int $id
@return \Illuminate\Http\Response
App\Http\Controllers\FilmController::show
<?php
public function show(Film $film) { }
@param \App\Models\Film $film
voici ce qui m'est retourné.
delacoche
Symfony\Component\ErrorHandler\Error\FatalError
Cannot redeclare App\Http\Controllers\FilmController::show()
delacoche
merci j’ai vu
si ça peut aider quelqu’un qui a fait la meme erreur que moi :
https://laracasts.com/discuss/channels/laravel/fatalerrorexception-in-postscontrollerphp-line-66-cannot-redeclare-apphttpcontrollerspostscontrollershow
Mody
bonjour mon coach! franchement merci davantage c’est bien saisi