Laravel Résumé
Models Migration Relation
Introduction Installation Projet:Structure Strucutre,model,migration Migration,Models,Relation Artisan CLI
Les Relations
BelongsTo HasOne HasMany BelongsToMany HasManyThrough
Exemples des Relations
Relations:oneToMany,ManyToMany... Relations:Exemples
Exercices
Exercice 1 Exercice 2
Controllers Views Routes
Routes,Controller,Model,view
Les Routes
Définir:Routes Routes avec Paramètres Routes nommées Groupes de routes
Les Controllers
Les Controllers Les Contrôleurs de Ressources
Les Vues
Vues et Blade Templates Blade Layouts et Sections Sous-vues Composants et Slots Contrôles de flux
MVC :CRUD
CRUD: Produit CRUD: Etudiant CRUD: Car CRUD,Recherche: Book
Validation
Exemple :Projets
ORM:Eloquent
Exemple :Transport
Api:Laravel +React
Middleware

Seeders & Factories
Exemples :EFM

Authenfication
Queue,job,task
TP:Schools Management
Authenfication:React
Layouts
Exercices





Projet Gestion de Bibliothèque

Réalisé par : Hanae Chaiboub FS-202

les questions:
  • 1. Créer les models des tables : Book, Author, Category, Loan, User, Profile, Card
  • 2. Créer les migrations des models (7 tables + 1 table pivot book_category)
  • 3. Créer les Controllers avec les méthodes CRUD (index, create, store, show, edit, update, destroy) pour Book, Author, Category, Loan, User
  • 4. Créer les vues pour chaque Controller : Index, Create, Show, Edit
  • 5. Afficher pour chaque auteur la liste de ses livres avec catégories
  • 6. Afficher la liste des livres par catégorie sélectionnée
  • 7. Afficher les emprunts avec statut (En cours / Retourné)
  • 8. Afficher le nombre d'emprunts pour chaque utilisateur
  • 9. Afficher le nombre de livres pour chaque catégorie
  • 10. Upload d'images de couverture et fichiers PDF pour les livres

Aperçu de l'application

Models

Book model


<?php

namespace App\Models; // Dossier où se trouve le modèle

use Illuminate\Database\Eloquent\Model; // Classe de base pour tous les modèles Eloquent

class Book extends Model
{
    // $fillable : liste des champs que l'on peut remplir via un formulaire (mass assignment)
    protected $fillable = ['titre', 'author_id', 'image', 'fichier_pdf'];

    // Relation : Un livre appartient à UN seul auteur (Many-to-One)
    // Clé étrangère : author_id dans la table books
    public function author()
    {
        return $this->belongsTo(Author::class);
    }

    // Relation : Un livre peut avoir PLUSIEURS catégories (Many-to-Many)
    // Utilise la table pivot 'book_category' pour stocker les associations
    public function categories()
    {
        return $this->belongsToMany(Category::class);
    }

    // Relation : Un livre peut avoir PLUSIEURS emprunts (One-to-Many)
    public function loans()
    {
        return $this->hasMany(Loan::class);
    }

    // Méthode personnalisée : vérifie si le livre est disponible
    // Retourne true si aucun emprunt n'a une date_retour NULL (= pas encore retourné)
    public function isAvailable()
    {
        return !$this->loans()->whereNull('date_retour')->exists();
    }
}

Author model


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    // Seul le champ 'nom_complet' peut être rempli via un formulaire
    protected $fillable = ['nom_complet'];

    // Relation : Un auteur peut avoir PLUSIEURS livres (One-to-Many)
    // Laravel cherche automatiquement la clé étrangère 'author_id' dans la table books
    public function books()
    {
        return $this->hasMany(Book::class);
    }
}

Category model


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Category extends Model
{
    protected $fillable = ['libelle']; // 'libelle' = nom de la catégorie

    // Relation Many-to-Many : Une catégorie peut contenir PLUSIEURS livres
    // et un livre peut appartenir à PLUSIEURS catégories
    // Table pivot utilisée : book_category
    public function books()
    {
        return $this->belongsToMany(Book::class);
    }
}

Loan model


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

// Modèle Loan (Emprunt) : représente un emprunt de livre par un utilisateur
class Loan extends Model
{
    // Champs remplissables : qui a emprunté, quel livre, quand, et date de retour
    // date_retour est nullable : NULL = emprunt en cours, une date = livre retourné
    protected $fillable = ['user_id', 'book_id', 'date_emprunt', 'date_retour'];

    // Relation : Un emprunt appartient à UN utilisateur (Many-to-One)
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    // Relation : Un emprunt concerne UN livre (Many-to-One)
    public function book()
    {
        return $this->belongsTo(Book::class);
    }
}

User model


<?php

namespace App\Models;

// On étend Authenticatable au lieu de Model car User gère l'authentification
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    protected $fillable = ['name', 'email', 'password'];
    // $hidden : ces champs ne seront JAMAIS visibles dans les réponses JSON (sécurité)
    protected $hidden = ['password', 'remember_token'];

    // Relation One-to-One : Un utilisateur a UN seul profil
    public function profile()
    {
        return $this->hasOne(Profile::class);
    }

    // Relation hasOneThrough : accéder à la carte DIRECTEMENT via le profil
    // User -> Profile -> Card (raccourci sans passer par profile manuellement)
    public function card()
    {
        return $this->hasOneThrough(Card::class, Profile::class);
    }

    // Relation One-to-Many : Un utilisateur peut faire PLUSIEURS emprunts
    public function loans()
    {
        return $this->hasMany(Loan::class);
    }
}

Profile model


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

// Modèle Profile : informations complémentaires d'un utilisateur
class Profile extends Model
{
    protected $fillable = ['user_id', 'adresse', 'telephone'];

    // Relation inverse : Ce profil appartient à UN utilisateur
    public function user()
    {
        return $this->belongsTo(User::class);
    }

    // Relation One-to-One : Un profil a UNE seule carte de bibliothèque
    public function card()
    {
        return $this->hasOne(Card::class);
    }
}

Card model


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

// Modèle Card : carte de bibliothèque liée au profil
class Card extends Model
{
    protected $fillable = ['profile_id', 'numero_carte'];

    // Relation inverse : Cette carte appartient à UN profil
    public function profile()
    {
        return $this->belongsTo(Profile::class);
    }
}

Migrations

Authors migration


<?php

// Importation des classes nécessaires pour créer une migration
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

// Classe anonyme qui étend Migration
return new class extends Migration
{
    // up() : exécutée quand on lance 'php artisan migrate'
    public function up(): void
    {
        Schema::create('authors', function (Blueprint $table) {
            $table->id();                    // Colonne ID auto-incrémentée (clé primaire)
            $table->string('nom_complet');   // Nom complet de l'auteur (VARCHAR)
            $table->timestamps();            // Crée created_at et updated_at automatiquement
        });
    }

    // down() : exécutée quand on lance 'php artisan migrate:rollback'
    public function down(): void
    {
        Schema::dropIfExists('authors'); // Supprime la table si elle existe
    }
};

Categories migration


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->id();
            $table->string('libelle');  // Nom de la catégorie
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('categories');
    }
};

Books migration


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('books', function (Blueprint $table) {
            $table->id();
            $table->string('titre');  // Titre du livre
            // foreignId() crée une clé étrangère vers la table 'authors'
            // constrained() ajoute automatiquement la contrainte de clé étrangère
            // onDelete('cascade') : si l'auteur est supprimé, ses livres aussi
            $table->foreignId('author_id')->constrained()->onDelete('cascade');
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('books');
    }
};

Books: ajout image et PDF migration


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

// Cette migration MODIFIE une table existante (Schema::table au lieu de Schema::create)
return new class extends Migration
{
    public function up(): void
    {
        Schema::table('books', function (Blueprint $table) {
            // nullable() : le champ peut être vide (pas obligatoire)
            // after() : positionne la colonne après 'author_id' dans la table
            $table->string('image')->nullable()->after('author_id');
            $table->string('fichier_pdf')->nullable()->after('image');
        });
    }

    public function down(): void
    {
        Schema::table('books', function (Blueprint $table) {
            $table->dropColumn(['image', 'fichier_pdf']); // Supprime les 2 colonnes
        });
    }
};

book_category (table pivot) migration


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

// TABLE PIVOT : nécessaire pour la relation Many-to-Many entre Book et Category
// Convention Laravel : noms des 2 tables au singulier, ordre alphabétique = book_category
return new class extends Migration
{
    public function up(): void
    {
        Schema::create('book_category', function (Blueprint $table) {
            // Deux clés étrangères : chaque ligne associe un livre à une catégorie
            $table->foreignId('book_id')->constrained()->onDelete('cascade');
            $table->foreignId('category_id')->constrained()->onDelete('cascade');
            // Clé primaire composée : empêche les doublons (même livre + même catégorie)
            $table->primary(['book_id', 'category_id']);
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('book_category');
    }
};

Profiles migration


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('profiles', function (Blueprint $table) {
            $table->id();
            // Clé étrangère vers la table users : chaque profil appartient à un seul user
            $table->foreignId('user_id')->constrained()->onDelete('cascade');
            $table->string('adresse')->nullable();   // Adresse optionnelle
            $table->string('telephone')->nullable(); // Téléphone optionnel
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('profiles');
    }
};

Cards migration


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('cards', function (Blueprint $table) {
            $table->id();
            $table->foreignId('profile_id')->constrained()->onDelete('cascade');
            $table->string('numero_carte')->unique(); // unique() : chaque carte a un numéro unique
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('cards');
    }
};

Loans migration


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('loans', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->constrained()->onDelete('cascade'); // Qui a emprunté
            $table->foreignId('book_id')->constrained()->onDelete('cascade'); // Quel livre
            $table->date('date_emprunt');          // Date de l'emprunt (obligatoire)
            $table->date('date_retour')->nullable(); // Date de retour (NULL = pas encore retourné)
            $table->timestamps();
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('loans');
    }
};

Controllers

BookController


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Book;
use App\Models\Author;
use App\Models\Category;
use Illuminate\Support\Facades\Storage; // Pour gérer les fichiers (upload/suppression)

class BookController extends Controller
{
    // INDEX : Afficher la liste de tous les livres
    public function index()
    {
        // with() = Eager Loading : charge les relations 'author' et 'categories' en même temps
        // latest() = trie par date de création décroissante (les plus récents d'abord)
        // paginate(10) = affiche 10 livres par page
        $books = Book::with(['author', 'categories'])->latest()->paginate(10);
        return view('books.index', compact('books')); // compact() envoie $books à la vue
    }

    // CREATE : Afficher le formulaire d'ajout
    public function create()
    {
        // On récupère les auteurs et catégories pour les listes déroulantes du formulaire
        $authors = Author::orderBy('nom_complet')->get();
        $categories = Category::orderBy('libelle')->get();
        return view('books.create', compact('authors', 'categories'));
    }

    // STORE : Traiter l'envoi du formulaire d'ajout
    public function store(Request $request)
    {
        // Validation des données reçues du formulaire
        // required = obligatoire, exists = doit exister dans la table, max = taille max
        $validated = $request->validate([
            'titre' => 'required|string|max:255',
            'author_id' => 'required|exists:authors,id', // L'auteur doit exister en BDD
            'categories' => 'nullable|array',
            'categories.*' => 'exists:categories,id',     // Chaque catégorie doit exister
            'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:2048', // Max 2 Mo
            'fichier_pdf' => 'nullable|mimes:pdf|max:10240', // Max 10 Mo
        ]);

        $data = [
            'titre' => $validated['titre'],
            'author_id' => $validated['author_id'],
        ];

        // Si un fichier image est envoyé, on le stocke dans storage/app/public/books/images
        if ($request->hasFile('image')) {
            $data['image'] = $request->file('image')->store('books/images', 'public');
        }
        // Si un fichier PDF est envoyé, on le stocke dans storage/app/public/books/pdfs
        if ($request->hasFile('fichier_pdf')) {
            $data['fichier_pdf'] = $request->file('fichier_pdf')->store('books/pdfs', 'public');
        }

        $book = Book::create($data); // Créer le livre en BDD

        // sync() : associe le livre aux catégories sélectionnées dans la table pivot
        if (!empty($validated['categories'])) {
            $book->categories()->sync($validated['categories']);
        }

        // Redirige vers la liste avec un message de succès
        return redirect()->route('books.index')->with('success', 'Livre ajouté avec succès.');
    }

    // SHOW : Afficher les détails d'un livre
    public function show(Book $book) // Route Model Binding : Laravel trouve le livre automatiquement par son ID
    {
        // load() charge les relations après la récupération du modèle
        $book->load(['author', 'categories', 'loans.user']);
        return view('books.show', compact('book'));
    }

    // EDIT : Afficher le formulaire de modification
    public function edit(Book $book)
    {
        $authors = Author::orderBy('nom_complet')->get();
        $categories = Category::orderBy('libelle')->get();
        $book->load('categories'); // Charge les catégories actuelles du livre
        return view('books.edit', compact('book', 'authors', 'categories'));
    }

    // UPDATE : Traiter la modification du livre
    public function update(Request $request, Book $book)
    {
        $validated = $request->validate([
            'titre' => 'required|string|max:255',
            'author_id' => 'required|exists:authors,id',
            'categories' => 'nullable|array',
            'categories.*' => 'exists:categories,id',
            'image' => 'nullable|image|mimes:jpeg,png,jpg,gif,webp|max:2048',
            'fichier_pdf' => 'nullable|mimes:pdf|max:10240',
        ]);

        $data = [
            'titre' => $validated['titre'],
            'author_id' => $validated['author_id'],
        ];

        // Si une nouvelle image est envoyée, on supprime l'ancienne avant de stocker la nouvelle
        if ($request->hasFile('image')) {
            if ($book->image) Storage::disk('public')->delete($book->image);
            $data['image'] = $request->file('image')->store('books/images', 'public');
        }
        if ($request->hasFile('fichier_pdf')) {
            if ($book->fichier_pdf) Storage::disk('public')->delete($book->fichier_pdf);
            $data['fichier_pdf'] = $request->file('fichier_pdf')->store('books/pdfs', 'public');
        }

        $book->update($data); // Met à jour le livre en BDD
        // sync() remplace toutes les catégories par celles sélectionnées
        $book->categories()->sync($validated['categories'] ?? []);

        return redirect()->route('books.index')->with('success', 'Livre mis à jour.');
    }

    // DESTROY : Supprimer un livre
    public function destroy(Book $book)
    {
        // Supprimer les fichiers associés du stockage avant de supprimer le livre
        if ($book->image) Storage::disk('public')->delete($book->image);
        if ($book->fichier_pdf) Storage::disk('public')->delete($book->fichier_pdf);
        $book->categories()->detach(); // Supprimer les associations dans la table pivot
        $book->delete();               // Supprimer le livre de la BDD
        return redirect()->route('books.index')->with('success', 'Livre supprimé.');
    }
}

AuthorController


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Author;

class AuthorController extends Controller
{
    // INDEX : Liste des auteurs avec le nombre de livres de chacun
    public function index()
    {
        // withCount('books') ajoute un attribut 'books_count' à chaque auteur
        // C'est une requête optimisée (sous-requête SQL) au lieu de charger tous les livres
        $authors = Author::withCount('books')->latest()->paginate(10);
        return view('authors.index', compact('authors'));
    }

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

    public function store(Request $request)
    {
        $request->validate(['nom_complet' => 'required|string|max:255']);
        // only() : ne récupère que le champ 'nom_complet' du formulaire (sécurité)
        Author::create($request->only('nom_complet'));
        return redirect()->route('authors.index')->with('success', 'Auteur ajouté.');
    }

    public function show(Author $author)
    {
        // Charge les livres de l'auteur AVEC leurs catégories (relation imbriquée)
        $author->load('books.categories');
        return view('authors.show', compact('author'));
    }

    public function edit(Author $author)
    {
        return view('authors.edit', compact('author'));
    }

    public function update(Request $request, Author $author)
    {
        $request->validate(['nom_complet' => 'required|string|max:255']);
        $author->update($request->only('nom_complet'));
        return redirect()->route('authors.index')->with('success', 'Auteur mis à jour.');
    }

    public function destroy(Author $author)
    {
        $author->delete(); // Supprime l'auteur (et ses livres grâce au cascade)
        return redirect()->route('authors.index')->with('success', 'Auteur supprimé.');
    }
}

CategoryController


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Category;

class CategoryController extends Controller
{
    // INDEX : Liste des catégories avec le nombre de livres dans chacune
    public function index()
    {
        // withCount('books') : ajoute books_count à chaque catégorie
        $categories = Category::withCount('books')->latest()->paginate(10);
        return view('categories.index', compact('categories'));
    }

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

    public function store(Request $request)
    {
        // unique:categories = le libellé doit être unique dans la table categories
        $request->validate(['libelle' => 'required|string|max:255|unique:categories']);
        Category::create($request->only('libelle'));
        return redirect()->route('categories.index')->with('success', 'Catégorie ajoutée.');
    }

    public function show(Category $category)
    {
        $category->load('books'); // Charge tous les livres de cette catégorie
        return view('categories.show', compact('category'));
    }

    public function edit(Category $category)
    {
        return view('categories.edit', compact('category'));
    }

    public function update(Request $request, Category $category)
    {
        // unique:categories,libelle,$id : ignore la catégorie actuelle lors de la vérification d'unicité
        $request->validate(['libelle' => 'required|string|max:255|unique:categories,libelle,'.$category->id]);
        $category->update($request->only('libelle'));
        return redirect()->route('categories.index')->with('success', 'Catégorie mise à jour.');
    }

    public function destroy(Category $category)
    {
        $category->books()->detach(); // Supprimer les liaisons dans la table pivot d'abord
        $category->delete();
        return redirect()->route('categories.index')->with('success', 'Catégorie supprimée.');
    }
}

LoanController


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Loan;
use App\Models\Book;
use App\Models\User;

class LoanController extends Controller
{
    // INDEX : Afficher tous les emprunts avec livre et utilisateur
    public function index()
    {
        // 'book.author' : charge le livre ET son auteur en une seule requête (relation imbriquée)
        $loans = Loan::with(['book.author', 'user'])->latest()->paginate(10);
        return view('loans.index', compact('loans'));
    }

    public function create()
    {
        $users = User::orderBy('name')->get();
        $books = Book::orderBy('titre')->get();
        return view('loans.create', compact('users', 'books'));
    }

    public function store(Request $request)
    {
        $request->validate([
            'user_id' => 'required|exists:users,id',
            'book_id' => 'required|exists:books,id',
            'date_emprunt' => 'required|date',
            // after_or_equal : la date de retour doit être égale ou après la date d'emprunt
            'date_retour' => 'nullable|date|after_or_equal:date_emprunt',
        ]);
        Loan::create($request->all());
        return redirect()->route('loans.index')->with('success', 'Emprunt enregistré.');
    }

    public function show(Loan $loan)
    {
        $loan->load(['book', 'user']);
        return view('loans.show', compact('loan'));
    }

    public function edit(Loan $loan)
    {
        $users = User::orderBy('name')->get();
        $books = Book::orderBy('titre')->get();
        return view('loans.edit', compact('loan', 'users', 'books'));
    }

    public function update(Request $request, Loan $loan)
    {
        $request->validate([
            'user_id' => 'required|exists:users,id',
            'book_id' => 'required|exists:books,id',
            'date_emprunt' => 'required|date',
            'date_retour' => 'nullable|date|after_or_equal:date_emprunt',
        ]);
        $loan->update($request->all());
        return redirect()->route('loans.index')->with('success', 'Emprunt mis à jour.');
    }

    public function destroy(Loan $loan)
    {
        $loan->delete();
        return redirect()->route('loans.index')->with('success', 'Emprunt supprimé.');
    }
}

UserController


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Hash; // Pour le hashage sécurisé du mot de passe

class UserController extends Controller
{
    // INDEX : Liste des utilisateurs avec nombre d'emprunts
    public function index()
    {
        // withCount('loans') : ajoute loans_count à chaque utilisateur
        $users = User::withCount('loans')->latest()->paginate(10);
        return view('users.index', compact('users'));
    }

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

    public function store(Request $request)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|email|unique:users', // Email unique dans la table users
            'password' => 'required|min:6|confirmed',  // confirmed : nécessite un champ password_confirmation
        ]);
        User::create([
            'name' => $request->name,
            'email' => $request->email,
            'password' => Hash::make($request->password), // JAMAIS stocker le mot de passe en clair !
        ]);
        return redirect()->route('users.index')->with('success', 'Utilisateur créé.');
    }

    public function show(User $user)
    {
        // Charge le profil et les emprunts avec les livres associés
        $user->load(['profile', 'loans.book']);
        return view('users.show', compact('user'));
    }

    public function edit(User $user)
    {
        return view('users.edit', compact('user'));
    }

    public function update(Request $request, User $user)
    {
        $request->validate([
            'name' => 'required|string|max:255',
            // unique:users,email,$id : ignore l'utilisateur actuel lors de la vérification
            'email' => 'required|email|unique:users,email,'.$user->id,
            'password' => 'nullable|min:6|confirmed', // nullable : le mot de passe est optionnel lors de la modification
        ]);
        $data = $request->only('name', 'email');
        // filled() : vérifie si le champ n'est pas vide (différent de has())
        if ($request->filled('password')) {
            $data['password'] = Hash::make($request->password);
        }
        $user->update($data);
        return redirect()->route('users.index')->with('success', 'Utilisateur mis à jour.');
    }

    public function destroy(User $user)
    {
        $user->delete();
        return redirect()->route('users.index')->with('success', 'Utilisateur supprimé.');
    }
}

Routes

web.php


<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\BookController;
use App\Http\Controllers\AuthorController;
use App\Http\Controllers\CategoryController;
use App\Http\Controllers\LoanController;
use App\Http\Controllers\UserController;
use App\Models\Book;
use App\Models\Author;
use App\Models\Category;
use App\Models\Loan;

// Page d'accueil : affiche les statistiques et les livres récents
Route::get('/', function () {
    return view('welcome', [
        'booksCount' => Book::count(),           // Nombre total de livres
        'authorsCount' => Author::count(),       // Nombre total d'auteurs
        'categoriesCount' => Category::count(),  // Nombre total de catégories
        // Emprunts actifs = ceux qui n'ont pas encore de date de retour
        'activeLoansCount' => Loan::whereNull('date_retour')->count(),
        // Les 6 derniers livres ajoutés avec leurs relations
        'recentBooks' => Book::with(['author','categories'])->latest()->take(6)->get(),
    ]);
});

// Route::resource() crée automatiquement les 7 routes CRUD :
// GET /books          => index   (liste)
// GET /books/create   => create  (formulaire d'ajout)
// POST /books         => store   (enregistrer)
// GET /books/{book}   => show    (détails)
// GET /books/{book}/edit => edit (formulaire de modification)
// PUT /books/{book}   => update  (mettre à jour)
// DELETE /books/{book} => destroy (supprimer)
Route::resource('books', BookController::class);
Route::resource('authors', AuthorController::class);
Route::resource('categories', CategoryController::class);
Route::resource('loans', LoanController::class);
Route::resource('users', UserController::class);

Vues

Layouts app.blade.php


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@yield('title', 'Biblio')</title>
    <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
    <style>
        /* Design System - Light Theme */
        :root {
            --bg-primary: #f8f9fc;
            --text-primary: #1e1e2e;
            --accent: #6366f1;
        }
        nav { background: linear-gradient(135deg, #4f46e5, #7c3aed); padding: 15px 30px; }
        nav a { color: #fff; text-decoration: none; margin-right: 15px; font-family: 'Inter', sans-serif; }
        nav a:hover { text-decoration: underline; }
        h1 { color: #333; font-family: 'Inter', sans-serif; }
    </style>
</head>
<body>
    @include('partials.navbar')
    @yield('content')
    @include('partials.footer')
</body>
</html>

Liste des Livres


Route::resource('books', BookController::class);
// GET /books => BookController@index

@extends('layouts.app')

@section('content')
<div class="page-container">
    <div class="page-header">
        <h1>📚 Tous les Livres</h1>
        <a href="{{ route('books.create') }}" class="btn btn-primary">+ Ajouter un Livre</a>
    </div>
    <table class="data-table">
        <thead><tr>
            <th>#</th><th>Couverture</th><th>Titre</th><th>Auteur</th>
            <th>Catégories</th><th>PDF</th><th>Statut</th><th>Actions</th>
        </tr></thead>
        <tbody>
        @foreach($books as $book)
        <tr>
            <td>{{ $book->id }}</td>
            <td>
                @if($book->image)
                <img src="{{ asset('storage/'.$book->image) }}" width="50">
                @endif
            </td>
            <td><strong>{{ $book->titre }}</strong></td>
            <td>{{ $book->author->nom_complet }}</td>
            <td>
                @foreach($book->categories as $cat)
                <span class="badge">{{ $cat->libelle }}</span>
                @endforeach
            </td>
            <td>
                @if($book->fichier_pdf)
                <a href="{{ asset('storage/'.$book->fichier_pdf) }}">📄 PDF</a>
                @endif
            </td>
            <td>{{ $book->isAvailable() ? '✅ Disponible' : '🔴 Emprunté' }}</td>
            <td>
                <a href="{{ route('books.show', $book) }}" class="btn-view">Voir</a>
                <a href="{{ route('books.edit', $book) }}" class="btn-edit">Modifier</a>
                <form action="{{ route('books.destroy', $book) }}" method="POST" style="display:inline;">
                    @csrf @method('DELETE')
                    <button type="submit" class="btn-danger">Supprimer</button>
                </form>
            </td>
        </tr>
        @endforeach
        </tbody>
    </table>
    {{ $books->links() }}
</div>
@endsection

Ajouter un Livre


// GET /books/create => BookController@create
// POST /books => BookController@store

@extends('layouts.app')

@section('content')
<div class="page-container">
    <h1>📖 Ajouter un Livre</h1>
    <form action="{{ route('books.store') }}" method="POST" enctype="multipart/form-data">
        @csrf
        <label for="titre">Titre</label>
        <input type="text" name="titre" class="form-control" required>

        <label for="author_id">Auteur</label>
        <select name="author_id" class="form-control">
            <option value="">Choisissez un auteur</option>
            @foreach($authors as $author)
            <option value="{{ $author->id }}">{{ $author->nom_complet }}</option>
            @endforeach
        </select>

        <label>Catégories</label>
        @foreach($categories as $cat)
        <label class="checkbox-label">
            <input type="checkbox" name="categories[]" value="{{ $cat->id }}">
            {{ $cat->libelle }}
        </label>
        @endforeach

        <label for="image">Image de couverture</label>
        <input type="file" name="image" accept="image/*">

        <label for="fichier_pdf">Fichier PDF</label>
        <input type="file" name="fichier_pdf" accept=".pdf">

        <button type="submit" class="btn btn-primary">Ajouter le livre</button>
    </form>
</div>
@endsection

Liste des Emprunts


Route::resource('loans', LoanController::class);
// GET /loans => LoanController@index

@extends('layouts.app')

@section('content')
<div class="page-container">
    <div class="page-header">
        <h1>📋 Liste des Emprunts</h1>
        <a href="{{ route('loans.create') }}" class="btn btn-primary">+ Nouvel Emprunt</a>
    </div>
    <table class="data-table">
        <thead><tr>
            <th>#</th><th>Livre</th><th>Utilisateur</th>
            <th>Date Emprunt</th><th>Date Retour</th><th>Statut</th><th>Actions</th>
        </tr></thead>
        <tbody>
        @foreach($loans as $loan)
        <tr>
            <td>{{ $loan->id }}</td>
            <td>{{ $loan->book->titre }}</td>
            <td>{{ $loan->user->name }}</td>
            <td>{{ $loan->date_emprunt }}</td>
            <td>{{ $loan->date_retour ?? '—' }}</td>
            <td>{{ $loan->date_retour ? '✅ Retourné' : '🔴 En cours' }}</td>
            <td>
                <a href="{{ route('loans.edit', $loan) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: green; color: white;">Modifier</a>
                <form action="{{ route('loans.destroy', $loan) }}" method="POST" style="display:inline;">
                    @csrf @method('DELETE')
                    <button type="submit" style="padding: 5px 10px; border-radius: 5px; background-color: red; color: white; border:none; cursor:pointer;">Supprimer</button>
                </form>
            </td>
        </tr>
        @endforeach
        </tbody>
    </table>
    {{ $loans->links() }}
</div>
@endsection

Ajouter un Emprunt


// GET /loans/create => LoanController@create
// POST /loans => LoanController@store

@extends('layouts.app')

@section('content')
<div class="page-container">
    <h1>📋 Ajouter un Emprunt</h1>
    <form action="{{ route('loans.store') }}" method="POST">
        @csrf
        <label for="user_id">Utilisateur</label>
        <select name="user_id" class="form-control">
            <option value="">Choisissez un utilisateur</option>
            @foreach($users as $user)
            <option value="{{ $user->id }}">{{ $user->name }}</option>
            @endforeach
        </select>

        <label for="book_id">Livre</label>
        <select name="book_id" class="form-control">
            <option value="">Choisissez un livre</option>
            @foreach($books as $book)
            <option value="{{ $book->id }}">{{ $book->titre }}</option>
            @endforeach
        </select>

        <label for="date_emprunt">Date d'emprunt</label>
        <input type="date" name="date_emprunt" class="form-control" required>

        <label for="date_retour">Date de retour (optionnel)</label>
        <input type="date" name="date_retour" class="form-control">

        <button type="submit" class="btn btn-primary">Enregistrer l'emprunt</button>
    </form>
</div>
@endsection

Liste des Auteurs (Nombre de livres par auteur)


Route::resource('authors', AuthorController::class);
// GET /authors => AuthorController@index

@extends('layouts.app')

@section('content')
<div class="page-container">
    <div class="page-header">
        <h1>✍️ Liste des Auteurs</h1>
        <a href="{{ route('authors.create') }}" class="btn btn-primary">+ Ajouter</a>
    </div>
    <table class="data-table">
        <thead><tr>
            <th>#</th><th>Nom Complet</th><th>Nombre de Livres</th><th>Actions</th>
        </tr></thead>
        <tbody>
        @foreach($authors as $author)
        <tr>
            <td>{{ $author->id }}</td>
            <td>{{ $author->nom_complet }}</td>
            <td>{{ $author->books_count }}</td>
            <td>
                <a href="{{ route('authors.show', $author) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: #3b82f6; color: white;">Voir</a>
                <a href="{{ route('authors.edit', $author) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: green; color: white;">Modifier</a>
                <form action="{{ route('authors.destroy', $author) }}" method="POST" style="display:inline;">
                    @csrf @method('DELETE')
                    <button type="submit" style="padding: 5px 10px; border-radius: 5px; background-color: red; color: white; border:none;">Supprimer</button>
                </form>
            </td>
        </tr>
        @endforeach
        </tbody>
    </table>
    {{ $authors->links() }}
</div>
@endsection

Liste des Utilisateurs (Nombre d'emprunts par utilisateur)


Route::resource('users', UserController::class);
// GET /users => UserController@index

@extends('layouts.app')

@section('content')
<div class="page-container">
    <div class="page-header">
        <h1>👥 Liste des Utilisateurs</h1>
        <a href="{{ route('users.create') }}" class="btn btn-primary">+ Ajouter</a>
    </div>
    <table class="data-table">
        <thead><tr>
            <th>#</th><th>Nom</th><th>Email</th>
            <th>Nombre d'emprunts</th><th>Inscrit le</th><th>Actions</th>
        </tr></thead>
        <tbody>
        @foreach($users as $user)
        <tr>
            <td>{{ $user->id }}</td>
            <td>{{ $user->name }}</td>
            <td>{{ $user->email }}</td>
            <td>{{ $user->loans_count }}</td>
            <td>{{ $user->created_at->format('d/m/Y') }}</td>
            <td>
                <a href="{{ route('users.show', $user) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: #3b82f6; color: white;">Voir</a>
                <a href="{{ route('users.edit', $user) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: green; color: white;">Modifier</a>
                <form action="{{ route('users.destroy', $user) }}" method="POST" style="display:inline;">
                    @csrf @method('DELETE')
                    <button type="submit" style="padding: 5px 10px; border-radius: 5px; background-color: red; color: white; border:none;">Supprimer</button>
                </form>
            </td>
        </tr>
        @endforeach
        </tbody>
    </table>
    {{ $users->links() }}
</div>
@endsection

Liste des Catégories (Nombre de livres par catégorie)


Route::resource('categories', CategoryController::class);
// GET /categories => CategoryController@index

@extends('layouts.app')

@section('content')
<div class="page-container">
    <div class="page-header">
        <h1>🏷️ Liste des Catégories</h1>
        <a href="{{ route('categories.create') }}" class="btn btn-primary">+ Ajouter</a>
    </div>
    <table class="data-table">
        <thead><tr>
            <th>#</th><th>Libellé</th><th>Nombre de Livres</th><th>Actions</th>
        </tr></thead>
        <tbody>
        @foreach($categories as $category)
        <tr>
            <td>{{ $category->id }}</td>
            <td>{{ $category->libelle }}</td>
            <td>{{ $category->books_count }}</td>
            <td>
                <a href="{{ route('categories.show', $category) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: #3b82f6; color: white;">Voir</a>
                <a href="{{ route('categories.edit', $category) }}" style="margin-right: 10px; text-decoration: none; padding: 5px 10px; border-radius: 5px; background-color: green; color: white;">Modifier</a>
                <form action="{{ route('categories.destroy', $category) }}" method="POST" style="display:inline;">
                    @csrf @method('DELETE')
                    <button type="submit" style="padding: 5px 10px; border-radius: 5px; background-color: red; color: white; border:none;">Supprimer</button>
                </form>
            </td>
        </tr>
        @endforeach
        </tbody>
    </table>
    {{ $categories->links() }}
</div>
@endsection

Page d'accueil (Statistiques)


// Route page d'accueil
Route::get('/', function () {
    return view('welcome', [
        'booksCount' => Book::count(),
        'authorsCount' => Author::count(),
        'categoriesCount' => Category::count(),
        'activeLoansCount' => Loan::whereNull('date_retour')->count(),
        'recentBooks' => Book::with(['author','categories'])->latest()->take(6)->get(),
    ]);
});

@extends('layouts.app')

@section('content')
    @include('partials.hero')    {{-- Section hero avec titre et description --}}
    @include('partials.stats')   {{-- 4 cartes statistiques : Livres, Auteurs, Catégories, Emprunts actifs --}}
    @include('partials.books')   {{-- Grille des 6 derniers livres ajoutés --}}
@endsection