Laravel : Créer le CRUD d'un blog

Mis à jour il y a 3 ans

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.

Wilo Ahadi

Auteur

Wilo A.

Technologies

Laravel, PHP, HTML
Voir aussi Un tutoriel pour afficher les messages d’erreur de validation avec Laravel, sur un formulaire stylisé avec Bootstrap 5 En savoir plus

Introduction au CRUD

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 :

  • « Create » : Créer une donnée
  • « Read » ou « Retrieve » : Lire ou retrouver une donnée
  • « Update » : Mettre à jour une donnée
  • « Delete » ou Destroy » : Supprimer une donnée

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.

Base de données, migration et modèle du CRUD

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 » :

  • Un identifiant : $post->id
  • Un titre : $post->title
  • Une image de couverture : $post->picture
  • Un contenu : $post->content
  • La date de création et de mise à jour : $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 :

  • /app/Models/Post.php : Le modèle qui représente la table d'articles « posts »
  • /database/migrations/…_create_posts_table.php : La migration où décrire le schéma de la table « posts »

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

Le contrôleur du CRUD

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 :

  • « --resource » permet d'intégrer les méthodes du CRUD dans le contrôleur : index(), create(), store(), show(), edit(), update() et destroy()
  • « --model=Post » permet d'importer et injecter le modèle Post dans les méthodes 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.

Les 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

Les opérations du CRUD

Revenons sur les méthodes du contrôleur /app/Http/Controllers/PostController.php pour décrire les actions des routes :

1. L'action « index »

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>

2. L'action « create »

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

3. L'action « 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 » :

  1. Valider les informations envoyées (Voir validation Laravel)
  2. Uploader l'image de couverture
  3. Enregistrer les informations du Post dans la table « posts »
  4. Retourner vers la liste de Post : la route « posts.index »

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

4. L'action « show »

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

5. L'action « edit »

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 :

  • L'action du formulaire se gère par la route nommée « posts.update »
  • On ajoute un champ <input type="hidden" name="_method" value="PUT">
  • On complète les valeurs des inputs du formulaire (« title » et « content ») avec les données existantes du Post $post
  • On affiche l'image de couverture existant

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

6. L'action « update »

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 » :

  1. On valide les informations envoyées
  2. Si une nouvelle image est envoyée, on supprime l'ancienne image puis on upload la nouvelle
  3. On met à jour les informations du Post spécifié
  4. On se redirige vers la route « posts.show » pour afficher le post modifié 

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));
}

7. L'action « destroy »

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 :

  • /app/Models/Post.php : Le modèle qui représente un article (post) de la table « posts »
  • /database/migrations/..._create_posts_table.php : La migration où décrire le schéma de la table « posts »
  • /routes/web.php : Le fichier où définir les routes du CRUD
  • /app/Http/Controllers/PostController.php : Le contrôleur pour pour décrire les actions du CRUD : index, create, store, show, edit, update et destroy
  • /resources/views/layouts/app.blade.php : La vue principale (template Blade) dont héritent les autres vues
  • /resources/views/posts/index.blade.php : La vue où afficher tous les posts
  • /resources/views/posts/edit.blade.php : La vue où créer et éditer un post
  • /resources/views/posts/show.blade.php : La vue pour afficher un post
  • /.env : Le fichier où modifier les configurations de l'application

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

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