Un tutoriel pour mettre en place les opérations CRUD (Create, Read, Update, Delete) avec upload d’image dans un projet Laravel. Exemple d'un blog.
Le terme CRUD est étroitement lié avec la gestion des données numériques. Il résume les opérations ou fonctions qu’un utilisateur a besoin pour créer et gérer les données.
L'acronyme CRUD désigne les quatre opérations de base de la gestion et la persistance des données :
Nous voulons voir dans ce guide comment créer un CRUD pour la gestion d’articles d’un blog avec upload d’image pour la couverture d'un article dans un projet Laravel.
Pour présenter un article de blog, appelons-le « post », nous avons besoin des informations suivantes dans une table de la base de données, appelons cette table « posts » :
$post->id
$post->title
$post->picture
$post->content
$post->created_at
et $post->updated_at
Le schéma de ces informations doit être décrit dans une migration. Nous pouvons générer le modèle et la migration en exécutant la commande artisan suivante :
php artisan make:model Post -m
Le paramètre « -m » permet de générer la migration « …_create_posts_table.php » du modèle « Post.php ». Ce qui nous donne deux fichiers :
Décrivons le schéma de la table « posts » dans la fonction up()
de la migration /database/migrations/…_create_posts_table.php :
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostsTable extends Migration
{
public function up()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('picture');
$table->text('content');
$table->timestamps();
});
}
}
Pour importer (migrer) la table « posts » dans la base de données, on exécute la commande artisan suivante :
php artisan migrate
Pour gérer les actions ou opérations du CRUD des articles (posts), nous avons besoin d'un contrôleur, appelons-le « PostController ».
Pour générer le contrôleur PostController.php en l'associant au modèle app/Models/Post.php, on exécute la commande artisan suivante :
php artisan make:controller PostController --resource --model=Post
Les paramètres :
index()
, create()
, store()
, show()
, edit()
, update()
et destroy()
store()
, show()
, edit()
, update()
et destroy()
Ce qui nous donne le code suivant au fichier /app/Http/Controllers/PostController.php :
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function index() { }
public function create() { }
public function store(Request $request) { }
public function show(Post $post) { }
public function edit(Post $post) { }
public function update(Request $request, Post $post) { }
public function destroy(Post $post) { }
}
Nous allons décrire ces méthodes après qu'on ait parlé de routes du CRUD.
Pour faire pointer les routes du CRUD, nommons ces routes « posts.* » (posts.index, posts.create, posts.edit, ...), aux actions du contrôleur PostController, il suffit d'insérer une route-ressource au fichier /routes/web/php :
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PostController;
// La route-ressource => Les routes "post.*"
Route::resource("posts", PostController::class);
Pour voir les nouvelles routes crées, exécutons la commande artisan suivante :
php artisan route:list
Ce qui nous affiche les routes suivantes :
Méthode | URI | Action | Nom de la route |
---|---|---|---|
GET | /posts |
index | posts.index |
GET | /posts/create |
create | posts.create |
POST | /posts |
store | posts.store |
GET | /posts/{post} |
show | posts.show |
GET | /posts/{post}/edit |
edit | posts.edit |
PUT/PATCH | /posts/{post} |
update | posts.update |
DELETE | /posts/{post} |
destroy | posts.destroy |
Revenons sur les méthodes du contrôleur /app/Http/Controllers/PostController.php pour décrire les actions des routes :
La méthode ou l'action index()
dont la route est nommée « posts.index » permet d'afficher une liste d'une ressource. La « ressource » dont nous parlons ici est le « Post » (article).
Méthode | URI | Action | Nom de la route |
---|---|---|---|
GET | /posts |
index | posts.index |
Pour afficher la liste de Post, nous devons récupérer tous les posts de la base de données puis les transmettre à la vue (template Blade) /resources/views/posts/index.blade.php :
public function index() {
//On récupère tous les Post
$posts = Post::latest()->get();
// On transmet les Post à la vue
return view("posts.index", compact("posts"));
}
Pour présenter les posts, on parcourt (avec la directive @forelse
ou @foreach
) la collection de Post sur la vue /resources/views/posts/index.blade.php :
@extends("layouts.app")
@section("title", "Tous les articles")
@section("content")
<h1>Tous les articles</h1>
<p>
<!-- Lien pour créer un nouvel article : "posts.create" -->
<a href="{{ route('posts.create') }}" title="Créer un article" >Créer un nouveau post</a>
</p>
<!-- Le tableau pour lister les articles/posts -->
<table border="1" >
<thead>
<tr>
<th>Titre</th>
<th colspan="2" >Opérations</th>
</tr>
</thead>
<tbody>
<!-- On parcourt la collection de Post -->
@foreach ($posts as $post)
<tr>
<td>
<!-- Lien pour afficher un Post : "posts.show" -->
<a href="{{ route('posts.show', $post) }}" title="Lire l'article" >{{ $post->title }}</a>
</td>
<td>
<!-- Lien pour modifier un Post : "posts.edit" -->
<a href="{{ route('posts.edit', $post) }}" title="Modifier l'article" >Modifier</a>
</td>
<td>
<!-- Formulaire pour supprimer un Post : "posts.destroy" -->
<form method="POST" action="{{ route('posts.destroy', $post) }}" >
<!-- CSRF token -->
@csrf
<!-- <input type="hidden" name="_method" value="DELETE"> -->
@method("DELETE")
<input type="submit" value="x Supprimer" >
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
@endsection
Sur cette vue, nous avons ajouté les liens suivants :
{{ route('posts.create') }}
pour créer un nouveau Post{{ route('posts.show', $post) }}
pour afficher un Post $post
{{ route('posts.edit', $post) }}
pour éditer un Post $post
Et le formulaire pour supprimer un Post $post
:
<form method="POST" action="{{ route('posts.destroy', $post) }}" >
<!-- CSRF token -->
@csrf
<!-- <input type="hidden" name="_method" value="DELETE"> -->
@method("DELETE")
<input type="submit" value="x Supprimer" >
</form>
Notez-bien : Toutes les vues vont hériter (@extends("layouts.app")
) du template principal /resources/views/layouts/app.blade.php :
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Le titre de la page -->
<title>@yield("title")</title>
</head>
<body>
<!-- Le contenu -->
@yield("content")
</body>
</html>
La méthode ou l'action create()
dont la route est nommée « posts.create » permet de présenter un formulaire pour créer une nouvelle ressource :
Méthode | URI | Action | Nom de la route |
---|---|---|---|
GET | /posts/create |
create | posts.create |
Pour créer un nouveau Post, retournons la vue /resources/views/posts/edit.blade.php où nous allons placer le formulaire d'édition d'un article :
public function create() {
// On retourne la vue "/resources/views/posts/edit.blade.php"
return view("posts.edit");
}
Le formulaire pour éditer un nouveau Post peut se présenter de la manière suivante sur la vue /resources/views/posts/edit.blade.php :
@extends("layouts.app")
@section("title", "Créer un post")
@section("content")
<h1>Créer un post</h1>
<!-- Le formulaire est géré par la route "posts.store" -->
<form method="POST" action="{{ route('posts.store') }}" enctype="multipart/form-data" >
<!-- Le token CSRF -->
@csrf
<p>
<label for="title" >Titre</label><br/>
<input type="text" name="title" value="{{ old('title') }}" id="title" placeholder="Le titre du post" >
<!-- Le message d'erreur pour "title" -->
@error("title")
<div>{{ $message }}</div>
@enderror
</p>
<p>
<label for="picture" >Couverture</label><br/>
<input type="file" name="picture" id="picture" >
<!-- Le message d'erreur pour "picture" -->
@error("picture")
<div>{{ $message }}</div>
@enderror
</p>
<p>
<label for="content" >Contenu</label><br/>
<textarea name="content" id="content" lang="fr" rows="10" cols="50" placeholder="Le contenu du post" >{{ old('content') }}</textarea>
<!-- Le message d'erreur pour "content" -->
@error("content")
<div>{{ $message }}</div>
@enderror
</p>
<input type="submit" name="valider" value="Valider" >
</form>
@endsection
L'action du formulaire pointe vers la route nommée « posts.store ».
La méthode ou l'action store(Request $request)
dont la route est nommée « posts.store » permet d'enregistrer une nouvelle ressource :
Méthode | URI | Action | Nom de la route |
---|---|---|---|
POST | /posts |
store | posts.store |
Nous allons procéder de la manière suivante pour enregistrer un nouveau Post à partir de données qui proviennent du formulaire de la route « posts.create » :
Implémentions ce processus :
public function store(Request $request) {
// 1. La validation
$this->validate($request, [
'title' => 'bail|required|string|max:255',
"picture" => 'bail|required|image|max:1024',
"content" => 'bail|required',
]);
// 2. On upload l'image dans "/storage/app/public/posts"
$chemin_image = $request->picture->store("posts");
// 3. On enregistre les informations du Post
Post::create([
"title" => $request->title,
"picture" => $chemin_image,
"content" => $request->content,
]);
// 4. On retourne vers tous les posts : route("posts.index")
return redirect(route("posts.index"));
}
Notez-bien
1. Pour ne pas tomber sur l'exception MassAssignmentException, nous devons indiquer les propriétés $fillable
du modèle /app/Http/Models/Post.php :
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
use HasFactory;
protected $fillable = [ "title", "picture", "content" ];
}
2. Pour uploader l'image dans le répertoire /storage/app/public/posts, nous devons configurer le driver du système de fichiers en « public » au fichier .env :
FILESYSTEM_DRIVER=public
Puis créer un lien symbolique /public/storage/ connecté à /storage/app/public/ en exécutant la commande artisan suivante :
php artisan storage:link
La méthode ou l'action show(Post $post)
dont la route est nommée « posts.show» permet d'afficher une ressource spécifiée :
Méthode | URI | Action | Nom de la route |
---|---|---|---|
GET | /posts/{post} |
show | posts.show |
Pour afficher un Post enregistré, nous le récupérons à partir de son identifiant puis le transmettons à la vue /resources/views/posts/show.blade.php :
public function show(Post $post) {
return view("posts.show", compact("post"));
}
Nous pouvons présenter un Post de la manière suivante sur la vue /resources/views/posts/show.blade.php :
@extends("layouts.app")
@section("title", $post->title)
@section("content")
<h1>{{ $post->title }}</h1>
<img src="{{ asset('storage/'.$post->picture) }}" alt="Image de couverture" style="max-width: 300px;">
<div>{{ $post->content }}</div>
<p><a href="{{ route('posts.index') }}" title="Retourner aux articles" >Retourner aux posts</a></p>
@endsection
La méthode ou l'action edit(Post $post)
dont la route est nommée « posts.edit » permet d'afficher le formulaire où éditer (modifier) une ressource spécifiée :
Méthode | URI | Action | Nom de la route |
---|---|---|---|
GET | /posts/{post}/edit |
edit | posts.edit |
Pour éditer un Post, nous le récupérons à partir de son identifiant puis le transmettons à la vue /resources/views/posts/edit.blade.php :
public function edit(Post $post) {
return view("posts.edit", compact("post"));
}
Comme nous utilisons la vue edit.blade.php pour les actions create()
et edit()
, adaptons-la. Si un Post $post
est transmis à la vue :
<input type="hidden" name="_method" value="PUT">
$post
La vue /resources/views/posts/edit.blade.php adaptée aux méthodes create()
et edit()
:
@extends("layouts.app")
@section("title", "Editer un post")
@section("content")
<h1>Editer un post</h1>
<!-- Si nous avons un Post $post -->
@if (isset($post))
<!-- Le formulaire est géré par la route "posts.update" -->
<form method="POST" action="{{ route('posts.update', $post) }}" enctype="multipart/form-data" >
<!-- <input type="hidden" name="_method" value="PUT"> -->
@method('PUT')
@else
<!-- Le formulaire est géré par la route "posts.store" -->
<form method="POST" action="{{ route('posts.store') }}" enctype="multipart/form-data" >
@endif
<!-- Le token CSRF -->
@csrf
<p>
<label for="title" >Titre</label><br/>
<!-- S'il y a un $post->title, on complète la valeur de l'input -->
<input type="text" name="title" value="{{ isset($post->title) ? $post->title : old('title') }}" id="title" placeholder="Le titre du post" >
<!-- Le message d'erreur pour "title" -->
@error("title")
<div>{{ $message }}</div>
@enderror
</p>
<!-- S'il y a une image $post->picture, on l'affiche -->
@if(isset($post->picture))
<p>
<span>Couverture actuelle</span><br/>
<img src="{{ asset('storage/'.$post->picture) }}" alt="image de couverture actuelle" style="max-height: 200px;" >
</p>
@endif
<p>
<label for="picture" >Couverture</label><br/>
<input type="file" name="picture" id="picture" >
<!-- Le message d'erreur pour "picture" -->
@error("picture")
<div>{{ $message }}</div>
@enderror
</p>
<p>
<label for="content" >Contenu</label><br/>
<!-- S'il y a un $post->content, on complète la valeur du textarea -->
<textarea name="content" id="content" lang="fr" rows="10" cols="50" placeholder="Le contenu du post" >{{ isset($post->content) ? $post->content : old('content') }}</textarea>
<!-- Le message d'erreur pour "content" -->
@error("content")
<div>{{ $message }}</div>
@enderror
</p>
<input type="submit" name="valider" value="Valider" >
</form>
@endsection
La méthode ou l'action update(Request $request, Post $post)
dont la route est nommée « posts.update » permet de mettre à jour une ressource spécifiée :
Méthode | URI | Action | Nom de la route |
---|---|---|---|
PUT/PATCH | /posts/{post} |
update | posts.update |
Nous allons procéder de la manière suivante pour mettre à jour un Post avec les données qui proviennent du formulaire de la route « posts.edit » :
Implémentions ce processus :
public function update(Request $request, Post $post) {
// 1. La validation
// Les règles de validation pour "title" et "content"
$rules = [
'title' => 'bail|required|string|max:255',
"content" => 'bail|required',
];
// Si une nouvelle image est envoyée
if ($request->has("picture")) {
// On ajoute la règle de validation pour "picture"
$rules["picture"] = 'bail|required|image|max:1024';
}
$this->validate($request, $rules);
// 2. On upload l'image dans "/storage/app/public/posts"
if ($request->has("picture")) {
//On supprime l'ancienne image
Storage::delete($post->picture);
$chemin_image = $request->picture->store("posts");
}
// 3. On met à jour les informations du Post
$post->update([
"title" => $request->title,
"picture" => isset($chemin_image) ? $chemin_image : $post->picture,
"content" => $request->content
]);
// 4. On affiche le Post modifié : route("posts.show")
return redirect(route("posts.show", $post));
}
La méthode ou l'action destroy(Post $post)
dont la route est nommée « posts.destroy » permet de supprimer une ressource spécifiée :
Méthode | URI | Action | Nom de la route |
---|---|---|---|
DELETE | /posts/{post} |
destroy | posts.destroy |
Pour supprimer un Post, nous commençons par supprimer son image de couverture du répertoire /storage/app/public/posts, ensuite ses informations dans la table « posts » puis on retourne (redirection) à la route « posts.index » :
public function destroy(Post $post) {
// On supprime l'image existant
Storage::delete($post->picture);
// On les informations du $post de la table "posts"
$post->delete();
// Redirection route "posts.index"
return redirect(route('posts.index'));
}
Pour supprimer l'image, nous avons fait appel à la méthode delete($post->picture)
de la classe Storage
que nous devons importer :
<?php
use Illuminate\Support\Facades\Storage; // <= importer Storage
class PostController extends Controller
{
// ...
}
Assemblons tout, voici le code complet du contrôleur app/Http/Controllers/PostController.php :
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class PostController extends Controller
{
// Afficher tous les Post
public function index() {
//On récupère tous les Post
$posts = Post::latest()->get();
// On transmet les Post à la vue "/resources/views/posts/index.blade.php"
return view("posts.index", compact("posts"));
}
// Créer un nouveau Post
public function create() {
// On retourne la vue "/resources/views/posts/edit.blade.php"
return view("posts.edit");
}
// Enregistrer un nouveau Post
public function store(Request $request) {
// 1. La validation
$this->validate($request, [
'title' => 'bail|required|string|max:255',
"picture" => 'bail|required|image|max:1024',
"content" => 'bail|required',
]);
// 2. On upload l'image dans "/storage/app/public/posts"
$chemin_image = $request->picture->store("posts");
// 3. On enregistre les informations du Post
Post::create([
"title" => $request->title,
"picture" => $chemin_image,
"content" => $request->content,
]);
// 4. On retourne vers tous les posts : route("posts.index")
return redirect(route("posts.index"));
}
// Afficher un Post
public function show(Post $post) {
return view("posts.show", compact("post"));
}
// Editer un Post enregistré
public function edit(Post $post) {
return view("posts.edit", compact("post"));
}
// Mettre à jour un Post
public function update(Request $request, Post $post) {
// 1. La validation
// Les règles de validation pour "title" et "content"
$rules = [
'title' => 'bail|required|string|max:255',
"content" => 'bail|required',
];
// Si une nouvelle image est envoyée
if ($request->has("picture")) {
// On ajoute la règle de validation pour "picture"
$rules["picture"] = 'bail|required|image|max:1024';
}
$this->validate($request, $rules);
// 2. On upload l'image dans "/storage/app/public/posts"
if ($request->has("picture")) {
//On supprime l'ancienne image
Storage::delete($post->picture);
$chemin_image = $request->picture->store("posts");
}
// 3. On met à jour les informations du Post
$post->update([
"title" => $request->title,
"picture" => isset($chemin_image) ? $chemin_image : $post->picture,
"content" => $request->content
]);
// 4. On affiche le Post modifié : route("posts.show")
return redirect(route("posts.show", $post));
}
// Supprimer un Post
public function destroy(Post $post) {
// On supprime l'image existant
Storage::delete($post->picture);
// On les informations du $post de la table "posts"
$post->delete();
// Redirection route "posts.index"
return redirect(route('posts.index'));
}
}
Nous avons terminé l'implémentation du CRUD. Voici un récapitulatif de fichiers édités :
Pour toute question ou problème que vous pouvez rencontrer, laissez un commentaire. Portez-vous bien ! 😉
Cette publication vous a plu ?
Partagez-la avec vos ami(e)s sur les réseaux sociaux.
Wilo Ahadi, l'auteur
Passionné de l'informatique, je suis spécialiste en techniques des systèmes et réseaux, développeur web et mobile, infographiste et designer, ... J'aime partager mon expérience en formant sur la plateforme Akili School
Voir profil
Commentaires