Introduction à Laravel InertiaJS ReactJS

--- vues
Banner image for Introduction à Laravel InertiaJS ReactJS

Ce guide complet vous accompagnera dans la construction d'une application web moderne en utilisant Laravel comme backend, Inertia.js comme pont, et React pour le frontend. Nous créerons un système complet de gestion de produits à partir de zéro.

Installation et Configuration de Laravel#

Commençons par configurer un nouveau projet Laravel. Il existe plusieurs façons de créer une application Laravel :

laravel-installation
# Install global laravel installer
composer global require laravel/installer

# Create a new Laravel project
laravel new introduction

# Alternative: using composer
composer create-project laravel/laravel introduction

# Run the development server
php artisan serve
npm run dev

# Run both with single command (using concurrently, open composer.json)
composer dev

L'installateur Laravel fournit diverses options incluant Docker Sail et des kits de démarrage comme Breeze avec des templates Blade ou Livewire.

Configuration Docker avec PostgreSQL#

Pour un environnement de développement cohérent, configurons Docker avec PostgreSQL. Créez un fichier docker-compose.yml à la racine de votre projet :

docker-compose.yml
services:
  db:
    image: postgres:17
    container_name: advantryx_introduction_db
    restart: unless-stopped
    environment:
      POSTGRES_DB: advantryx_introduction
      POSTGRES_USER: laravel
      POSTGRES_PASSWORD: password
      POSTGRES_ROOT_PASSWORD: admin
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - advantryx_introduction

networks:
  advantryx_introduction:
    driver: bridge

volumes:
  postgres_data:
    driver: local

Configuration de la Base de Données#

Mettez à jour votre fichier .env avec la configuration de base de données PostgreSQL :

.env
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=advantryx_introduction
DB_USERNAME=laravel
DB_PASSWORD=password

Démarrez votre conteneur PostgreSQL :

commandes-docker
# Démarrer le conteneur PostgreSQL
docker-compose up -d

# Vérifier si le conteneur fonctionne
docker-compose ps

# Voir les logs du conteneur
docker-compose logs db

# Arrêter le conteneur quand terminé
docker-compose down

Bases de Laravel et Création de Produits avec Routes et Controllers#

Comprendre les Concepts Clés de Laravel#

Avant de plonger dans le code, comprenons les composants clés de Laravel :

Les Models fournissent une interface puissante et agréable pour interagir avec les tables de votre base de données.

Les Migrations vous permettent de créer et modifier facilement les tables de votre base de données. Elles garantissent que la même structure de base de données existe partout où votre application s'exécute.

Les Controllers sont responsables du traitement des requêtes faites à votre application et du retour d'une réponse.

Création de Produits avec Routes et Controllers#

Créons un model Product avec migration, controller et routes de ressources :

create-product-model
php artisan make:model Product -mcr

Cette commande crée :

  • Model : app/Models/Product.php
  • Migration : database/migrations/xxxx_create_products_table.php
  • Controller : app/Http/Controllers/ProductController.php
  • Routes de ressources (nous les ajouterons manuellement)

Maintenant configurons les routes :

routes/web.php
Route::resource('products', ProductController::class)
    ->only(['index', 'store']);

// Alternative individual routes:
// Route::get('/products', [ProductController::class, 'index'])->name('products.index');
// Route::post('/products', [ProductController::class, 'store'])->name('products.store');

Vous pouvez vérifier vos routes en utilisant ces commandes :

route-list
php artisan route:list
php artisan route:list --path=products

Mettons à jour le ProductController pour retourner une réponse basique :

app/Http/Controllers/ProductController.php
<?php
 ...
namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;
+ use Illuminate\Http\Response;

class ProductController extends Controller
{
    /**
     * Display a listing of the resource.
     */
-    public function index()
+    public function index(): Response
    {
-        //
+        return response('Hello, World!');
    }
 ...
}

Maintenant intégrons Inertia.js pour rendre les composants React :

app/Http/Controllers/ProductController.php
<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
+ use Inertia\Inertia;
+ use Inertia\Response;

class ProductController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(): Response
    {
-        return response('Hello, World!');
+        return Inertia::render('Products/Index');
    }
}

Configuration Frontend avec Inertia.js et React#

Maintenant que nous avons configuré notre backend, créons le composant React frontend qui gérera la création de produits.

Création du Composant Index des Produits#

Créons le composant React qui sera rendu par notre controller Inertia :

resources/js/Pages/Products/Index.tsx
import AppLayout from '@/layouts/app-layout';
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useForm, Head } from '@inertiajs/react';
import { FormEventHandler } from 'react';

export default function Index() {
    const { data, setData, post, processing, reset, errors } = useForm({
        name: '',
    });

    const submit: FormEventHandler = (e) => {
        e.preventDefault();
        post(route('products.store'), { onSuccess: () => reset() });
    };

    return (
        <AppLayout>
            <Head title="Products" />

            <div className="mx-auto max-w-2xl p-4 sm:p-6 lg:p-8">
                <form onSubmit={submit}>
                    <Input
                        value={data.name}
                        placeholder="Name of the product"
                        onChange={(e) => setData('name', e.target.value)}
                    />
                    <InputError message={errors.name} className="mt-2" />
                    <Button className="mt-4" disabled={processing}>
                        Create
                    </Button>
                </form>
            </div>
        </AppLayout>
    );
}

Ce composant démontre plusieurs concepts clés d'Inertia.js et React :

  • Hook useForm : Gère l'état du formulaire, la soumission et les erreurs de validation
  • Composant Head : Définit le titre de la page
  • Helper route() : Génère des URLs pour les routes nommées
  • Gestion de formulaire : Empêche la soumission par défaut et utilise la méthode post d'Inertia

Ajout de Navigation#

Pour rendre la page des produits accessible, ajoutons-la à la navigation de la sidebar :

resources/js/components/app-sidebar.tsx
// resources/js/components/app-sidebar.tsx
const mainNavItems: NavItem[] = [
+    {
+        title: 'Products',
+        href: '/products',
+        icon: Package,
+    },
];

Cela ajoute un lien "Products" à la sidebar de votre application avec une icône Package.

Relations de Base de Données et Eloquent#

Maintenant configurons les relations de base de données appropriées et implémentons la fonctionnalité store pour nos produits.

Configuration de la Migration de Base de Données#

D'abord, mettons à jour notre migration products pour inclure les champs nécessaires et les relations de clés étrangères :

products-migration
<?php
 ...
return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('products', function (Blueprint $table) {
            $table->id();
+            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
+            $table->string('name');
            $table->timestamps();
        });
    }
 ...
};

Implémentation de la Méthode Store#

Mettons à jour notre ProductController pour gérer la création de produits avec une validation appropriée :

store-method
<?php

namespace App\Http\Controllers;

use App\Models\Product;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;

class ProductController extends Controller
{
 ...
    /**
     * Store a newly created resource in storage.
     */
-    public function store(Request $request)
+    public function store(Request $request): RedirectResponse
    {
-        //
+        $validated = $request->validate([
+            'name' => 'required|string|max:255',
+        ]);
+
+        $request->user()->products()->create($validated);
+
+        return redirect(route('products.index'));
    }
 ...
}

Configuration des Relations Eloquent#

Maintenant définissons les relations entre les models User et Product. Consultez la documentation des Relations Eloquent pour plus de détails.

Relation du Model User#

user-model-relationship
<?php
 ...
+ use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
 ...
+    public function products(): HasMany
+    {
+        return $this->hasMany(Product::class);
+    }
}

Configuration du Model Product#

product-model
<?php

class Product extends Model
{
    protected $fillable = ['name'];
    // protected $guarded = []; // if you want to allow all fields
}

Exécution des Migrations#

migration-commands
php artisan migrate
php artisan migrate:rollback
Astuce de Développement

Pendant le développement, vous pouvez ajouter de nouveaux champs de base de données sans créer un nouveau fichier de migration en modifiant la migration existante avant de l'exécuter.

Vous pouvez tester vos models en utilisant Laravel Tinker :

tinker-testing
php artisan tinker
App\Models\Product::all();

Affichage des Produits#

Maintenant que nous avons configuré nos relations de base de données, implémentons la fonctionnalité pour afficher les produits avec leurs informations utilisateur associées.

Mise à Jour du Controller pour Récupérer les Produits#

Modifions notre ProductController pour récupérer les produits avec leurs données utilisateur liées :

display-products-controller
<?php
 ...
class ProductController extends Controller
{
    /**
     * Display a listing of the resource.
     */
    public function index(): Response
    {
        return Inertia::render('Products/Index', [
-            //
+            'products' => Product::with('user:id,name')->latest()->get(),
        ]);
    }
 ...
}

Ajout de la Relation du Model Product#

Nous devons ajouter la relation belongsTo dans notre model Product :

product-belongs-to-user
<?php
+ use Illuminate\Database\Eloquent\Relations\BelongsTo;
 ...
class Product extends Model
{
+    public function user(): BelongsTo
+    {
+        return $this->belongsTo(User::class);
+    }
}

Création du Composant Product#

Créons un composant Product réutilisable pour afficher les produits individuels :

product-component
// resources/js/components/Product.tsx
import { User } from 'lucide-react';

export interface ProductProps {
    id: number;
    name: string;
    user: {
        name: string;
    };
    created_at: string;
}

export default function Product(product: ProductProps) {
    return (
        <div className="flex space-x-2 p-6">
            <User className="h-6 w-6 -scale-x-100 text-gray-600" />
            <div className="flex-1">
                <div className="flex items-center justify-between">
                    <div>
                        <span className="text-gray-800">{product.user.name}</span>
                        <small className="ml-2 text-sm text-gray-600">{new Date(product.created_at).toLocaleString()}</small>
                    </div>
                </div>
                <p className="mt-4 text-lg text-gray-900">{product.name}</p>
            </div>
        </div>
    );
}

Mise à Jour de la Page Index#

Maintenant mettons à jour notre page Index des Produits pour afficher la liste des produits :

updated-index-page
// resources/js/pages/Products/Index.tsx
import InputError from '@/components/input-error';
import Product, { ProductProps } from '@/components/Product';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import AppLayout from '@/layouts/app-layout';
import { Head, useForm } from '@inertiajs/react';
import { FormEventHandler } from 'react';

export default function Index({ products }: { products: ProductProps[] }) {
    const { data, setData, post, processing, reset, errors } = useForm({
        name: '',
    });

    const submit: FormEventHandler = (e) => {
        e.preventDefault();
        post(route('products.store'), { onSuccess: () => reset() });
    };

    return (
        <AppLayout>
            <Head title="Products" />

            <div className="mx-auto max-w-2xl p-4 sm:p-6 lg:p-8">
                <form onSubmit={submit}>
                    <Input value={data.name} placeholder="Name of the product" onChange={(e) => setData('name', e.target.value)} />
                    <InputError message={errors.name} className="mt-2" />
                    <Button className="mt-4" disabled={processing}>
                        Create
                    </Button>
                </form>
            </div>
            <div className="mt-6 divide-y rounded-lg bg-white shadow-sm">
                {products.map((product) => (
                    <Product key={product.id} {...product} />
                ))}
            </div>
        </AppLayout>
    );
}

Problèmes de Requêtes N+1 et Solutions#

L'un des problèmes de performance les plus courants dans les applications Laravel est le problème de requête N+1. Comprenons ce que c'est et comment le résoudre.

Comprendre le Problème N+1#

n-plus-one-problem
class ProductController extends Controller
{
/**
    * Display a listing of the resource.
    */
public function index()
{
    $products = Product::latest()->get();

    // This will trigger N+1 queries when we access user relationship
    // For each product, Laravel will make a separate query to fetch the user
    // If you have 100 products, it will make 1 query for products + 100 queries for users = 101 queries
    $products->each(function ($product) {
        $product->user;
    });

    return Inertia::render('Products/Index', [
        'products' => $products,
    ]);

    // With eager loading
    // This makes only 2 queries: 1 for products + 1 for all related users
    // return Inertia::render('Products/Index', [
    //     'products' => Product::with('user:id,name')->latest()->get(),
    // ]);
}

Prévenir le Lazy Loading en Développement#

Ajoutez ceci à votre AppServiceProvider pour détecter les problèmes N+1 pendant le développement :

prevent-lazy-loading
# app/Providers/AppServiceProvider.php
public function boot(): void
{
    // Detect N+1 query problems in development
    // This will throw an exception when lazy loading occurs
    if (app()->environment('local')) {
        Model::preventLazyLoading();
    }
}

Amélioration de l'Affichage des Dates#

Installons et utilisons dayjs pour un meilleur formatage des dates :

install-dayjs
npm install dayjs

dayjs-usage
// resources/js/components/Product.tsx
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';

dayjs.extend(relativeTime);

...
<small className="ml-2 text-sm text-gray-600">{dayjs(product.created_at).fromNow()}</small>
...

Filtrage des Produits Spécifiques à l'Utilisateur#

Vous pouvez également filtrer les produits pour afficher seulement ceux appartenant à l'utilisateur authentifié :

user-specific-products
# get only user products
class ProductController extends Controller
{
    public function index()
    {
        return Inertia::render('Products/Index', [
            'products' => Auth::user()->products()->with('user:id,name')->latest()->get(),
        ]);
    }
}

Édition des Produits#

Maintenant implémentons la capacité d'éditer les produits avec une autorisation appropriée pour s'assurer que les utilisateurs ne peuvent éditer que leurs propres produits.

Ajout de la Route Update#

D'abord, ajoutons la route update à nos routes de ressources :

add-update-route
<?php
 ...
Route::resource('products', ProductController::class)
-    ->only(['index', 'store'])
+    ->only(['index', 'store', 'update'])
    ->middleware(['auth', 'verified']);
 ...

Composant Product Amélioré avec Édition#

Mettons à jour notre composant Product pour inclure la fonctionnalité d'édition :

enhanced-product-component
// resources/js/components/Product.tsx
import React, { useState } from 'react';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { MoreHorizontal, User } from 'lucide-react';
import { useForm, usePage } from '@inertiajs/react';

import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import {
   DropdownMenu,
   DropdownMenuContent,
   DropdownMenuItem,
   DropdownMenuTrigger
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { type SharedData } from '@/types';

dayjs.extend(relativeTime);

export interface ProductProps {
   id: number;
   name: string;
   user: {
       id: number;
       name: string;
   };
   created_at: string;
   updated_at: string;
}

export default function Product(product: ProductProps) {
   const { auth } = usePage<SharedData>().props;
   const [editing, setEditing] = useState(false);

   const { data, setData, patch, clearErrors, reset, errors } = useForm({
       name: product.name,
   });

   const submit = (e: React.FormEvent) => {
       e.preventDefault();
       patch(route('products.update', product.id), {
           onSuccess: () => setEditing(false)
       });
   };

   return (
       <div className="flex space-x-2 p-6">
           <User className="h-6 w-6 -scale-x-100 text-gray-600" />
           <div className="flex-1">
               <div className="flex items-center justify-between">
                   <div>
                       <span className="text-gray-800">{product.user.name}</span>
                       <small className="ml-2 text-sm text-gray-600">{dayjs(product.created_at).fromNow()}</small>
                       {product.created_at !== product.updated_at && (
                           <small className="text-sm text-gray-600"> &middot; edited</small>
                       )}
                   </div>
                   {product.user.id === auth.user.id && (
                       <DropdownMenu>
                           <DropdownMenuTrigger asChild>
                               <Button variant="ghost" size="icon" className="h-8 w-8">
                                   <MoreHorizontal className="h-4 w-4 text-gray-400" />
                               </Button>
                           </DropdownMenuTrigger>
                           <DropdownMenuContent align="end">
                               <DropdownMenuItem onClick={() => setEditing(true)}>
                                   Edit
                               </DropdownMenuItem>
                           </DropdownMenuContent>
                       </DropdownMenu>
                   )}
               </div>
               {editing ? (
                   <form onSubmit={submit}>
                       <Input
                           value={data.name}
                           onChange={e => setData('name', e.target.value)}
                           className="mt-4"
                       />
                       <InputError message={errors.name} className="mt-2" />
                       <div className="space-x-2">
                           <Button type="submit" className="mt-4">Save</Button>
                           <Button
                               type="button"
                               variant="ghost"
                               className="mt-4"
                               onClick={() => {
                                   setEditing(false);
                                   reset();
                                   clearErrors();
                               }}
                           >
                               Cancel
                           </Button>
                       </div>
                   </form>
               ) : (
                   <p className="mt-4 text-lg text-gray-900">{product.name}</p>
               )}
           </div>
       </div>
   );
}

Implémentation de la Méthode Update du Controller#

Maintenant implémentons la méthode update dans notre ProductController :

update-controller-method
<?php
class ProductController extends Controller
{
/**
 * Update the specified resource in storage.
 */
    public function update(Request $request, Product $product): RedirectResponse
    {
        Gate::authorize('update', $product);
        // if ($product->user_id !== $request->user()->id) {
        //     abort(403, 'Unauthorized action.');
        // }

        $validated = $request->validate([
            'name' => 'required|string|max:255',
        ]);

        $product->update($validated);

        return redirect(route('products.index'));
    }
}

Autorisation avec les Policies#

Pour une autorisation appropriée, créons une policy pour contrôler qui peut mettre à jour les produits :

create-policy
php artisan make:policy ProductPolicy --model=Product
product-policy
# app/Policies/ProductPolicy.php
class ProductPolicy
{
    /**
     * Determine whether the user can update the model.
    */
    public function update(User $user, Product $product): bool
    {
        return $product->user_id === $user->id;
        // return $product->user()->is($user);
    }
}

Exemple de Test de Sécurité#

Test de Sécurité

Voici un exemple de comment vous pouvez tester l'autorisation sans protection Gate appropriée. Cela démontre pourquoi l'autorisation est cruciale.

Étapes pour tester :

  1. Ouvrez l'onglet Network dans le navigateur
  2. Éditez un produit que l'utilisateur possède
  3. Copiez le X-XSRF-TOKEN des Request Headers
  4. Exécutez cette commande console :
security-test
fetch('/products/11', {
    // Change to a product ID you don't own
    method: 'PATCH',
    headers: {
        'Content-Type': 'application/json',
        'X-XSRF-TOKEN':
            'eyJpdiI6IjRFTy9Ma3Q0Ylc0NUlHRXJMRVcwUVE9PSIsInZhbHVlIjoiR1JoVHVpZithdlFqclArdHExcHdybC83VzJPVzRqbFd3eTJjRFZCUVJmNEhTUENvZXlISW1LTXBIZTdWRU9qaFBKL3VuTWFFOW1saFhYZDRBRnlsSzBBM2dEVGluckpjeUdCU0htZUpHd1FFUFVWT2hiL3JFV0U0anNodzJBZ1YiLCJtYWMiOiI1NjRkYTJhYmFiOWQyOTNmYjQzOGYzZTYyN2IzMjYxZDU0MTc0ZDZjYTcwMGRjMGM3M2JhZWZmODViYjlkNDA4IiwidGFnIjoiIn0=',
        'X-Inertia': 'true',
        'X-Requested-With': 'XMLHttpRequest',
    },
    body: JSON.stringify({
        name: 'HACKED PRODUCT!',
    }),
});

Suppression des Produits#

Finalement, implémentons la fonctionnalité de suppression avec une autorisation appropriée.

Ajout de la Route Destroy#

add-destroy-route
Route::resource('products', ProductController::class)
-    ->only(['index', 'store', 'update'])
+    ->only(['index', 'store', 'update', 'destroy'])
    ->middleware(['auth', 'verified']);

Implémentation de la Méthode Destroy#

destroy-method
- public function destroy(Product $product)
+ public function destroy(Product $product): RedirectResponse
{
-    //
+    Gate::authorize('delete', $product);

+    $product->delete();

+    return redirect(route('products.index'));
}

Ajout de la Policy Delete#

delete-policy
public function delete(User $user, Product $product): bool
{
    return $this->update($user, $product);
}

Ajout du Bouton Delete au Composant#

delete-button
// resources/js/components/Product.tsx
- const { data, setData, patch, clearErrors, reset, errors} = useForm({
-     name: product.name,
- });
+ const { data, setData, patch, clearErrors, reset, errors, delete: destroy } = useForm({
+     name: product.name,
+ });

...
+ <DropdownMenuItem onClick={() => destroy(route('products.destroy', product.id))}>Delete</DropdownMenuItem>

Conclusion#

Vous avez maintenant une application Laravel + Inertia.js + React complète avec :

  • ✅ Création de produits avec validation
  • ✅ Liste de produits avec relations
  • ✅ Édition de produits avec autorisation
  • ✅ Suppression de produits avec sécurité
  • ✅ Optimisation des requêtes N+1
  • ✅ Relations de base de données appropriées
  • ✅ Authentification et autorisation utilisateur

Cette fondation vous fournit tous les patterns essentiels pour construire des applications web modernes en utilisant cette stack puissante.

Bonus : Laravel Factory et Seeder pour les Produits#

Les factories et seeders Laravel fournissent un moyen puissant de générer des données de test pour votre application. C'est particulièrement utile pendant les phases de développement et de test.

Création de Product Factory#

Générez une factory pour le model Product :

create-factory
php artisan make:factory ProductFactory
Note Importante

Assurez-vous que votre model Product utilise le trait HasFactory pour activer la fonctionnalité factory.

app/Models/Product.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Product extends Model
{
    use HasFactory;

    protected $fillable = ['name'];

    public function user(): BelongsTo
    {
        return $this->belongsTo(User::class);
    }
}

Mettez à jour la ProductFactory pour définir comment les faux produits doivent être générés :

database/factories/ProductFactory.php
<?php

namespace Database\Factories;

use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;

class ProductFactory extends Factory
{
    public function definition(): array
    {
        return [
            'user_id' => User::inRandomOrder()->first()?->id ?? User::factory(),
            'name' => fake()->words(3, true),
        ];
    }
}

Création de Product Seeder#

Générez un seeder pour peupler votre base de données avec des produits d'exemple :

create-seeder
php artisan make:seeder ProductSeeder

Mettez à jour le ProductSeeder :

database/seeders/ProductSeeder.php
<?php

namespace Database\Seeders;

use App\Models\Product;
use Illuminate\Database\Seeder;

class ProductSeeder extends Seeder
{
    public function run(): void
    {
        Product::factory(50)->create();
    }
}

Exécution des Seeders#

Ajoutez le ProductSeeder à votre DatabaseSeeder :

database/seeders/DatabaseSeeder.php
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    public function run(): void
    {
        $this->call([
            ProductSeeder::class,
        ]);
    }
}

Exécutez les seeders pour peupler votre base de données :

run-seeders
# Run all seeders
php artisan db:seed

# Run specific seeder
php artisan db:seed --class=ProductSeeder

# Fresh migration with seeding
php artisan migrate:fresh --seed

Utilisation des Factories dans Tinker#

Vous pouvez également utiliser les factories directement dans Tinker pour des tests rapides :

tinker-factories
php artisan tinker

# Create a single product
Product::factory()->create();

# Create 5 products
Product::factory()->count(5)->create();

# Create products for a specific user
Product::factory()->count(3)->create(['user_id' => 1]);
Astuce de Développement

Les factories et seeders sont essentiels pour maintenir des données de test cohérentes à travers différents environnements et membres d'équipe.

Bonus : Laravel Middleware pour l'Autorisation#

Le middleware Laravel fournit un mécanisme pratique pour filtrer les requêtes HTTP entrant dans votre application. Au lieu de placer la logique d'autorisation directement dans les controllers, le middleware offre une approche plus propre et plus réutilisable.

Comprendre le Middleware#

Le middleware agit comme un pont entre une requête et une réponse. C'est comme une série de couches que votre requête doit traverser avant d'atteindre la logique principale de votre application. Chaque middleware peut examiner la requête, la modifier, ou même la terminer entièrement.

Création de Middleware Personnalisé#

Créons un middleware pour s'assurer que les utilisateurs ne peuvent accéder qu'à leurs propres produits :

create-middleware
php artisan make:middleware EnsureProductOwnership

Cela crée un nouveau fichier middleware à app/Http/Middleware/EnsureProductOwnership.php :

app/Http/Middleware/EnsureProductOwnership.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureProductOwnership
{
    /**
     * Handle an incoming request.
     */
    public function handle(Request $request, Closure $next): Response
    {
        $product = $request->route('product');

        if ($product && $product->user_id !== auth()->id()) {
            abort(403, 'Unauthorized access to product.');
        }

        return $next($request);
    }
}

Enregistrement du Middleware dans Laravel 12#

Dans Laravel 12, l'enregistrement du middleware a été simplifié. Enregistrez votre middleware dans bootstrap/app.php :

bootstrap/app.php
<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'product.owner' => \App\Http\Middleware\EnsureProductOwnership::class,
        ]);
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

Application du Middleware aux Routes#

Maintenant vous pouvez appliquer le middleware à vos routes de plusieurs façons :

Option 1 : Routes Individuelles#

routes/web.php
Route::resource('products', ProductController::class)
    ->only(['index', 'store', 'update', 'destroy'])
    ->middleware(['auth', 'verified']);

Route::middleware('product.owner')->group(function () {
    Route::patch('/products/{product}', [ProductController::class, 'update'])->name('products.update');
    Route::delete('/products/{product}', [ProductController::class, 'destroy'])->name('products.destroy');
});

Option 2 : Groupes de Routes#

routes/web.php
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/products', [ProductController::class, 'index'])->name('products.index');
    Route::post('/products', [ProductController::class, 'store'])->name('products.store');

    Route::middleware('product.owner')->group(function () {
        Route::patch('/products/{product}', [ProductController::class, 'update'])->name('products.update');
        Route::delete('/products/{product}', [ProductController::class, 'destroy'])->name('products.destroy');
    });
});

Option 3 : Constructeur de Controller#

app/Http/Controllers/ProductController.php
<?php

class ProductController extends Controller
{
    public function __construct()
    {
        $this->middleware('product.owner')->only(['update', 'destroy']);
    }

    // ... rest of controller methods
}

Simplification des Méthodes de Controller#

Avec le middleware gérant l'autorisation, vous pouvez supprimer la logique d'autorisation de vos méthodes de controller :

Middleware Global#

Pour le middleware qui devrait s'exécuter sur chaque requête, ajoutez-le à la stack de middleware global :

global-middleware
// bootstrap/app.php
->withMiddleware(function (Middleware $middleware) {
    $middleware->append(\App\Http\Middleware\LogRequests::class);
})

Avantages de l'Approche Middleware#

  1. Séparation des Préoccupations : La logique d'autorisation est séparée de la logique métier
  2. Réutilisabilité : Le même middleware peut être appliqué à plusieurs routes
  3. Cohérence : Assure que l'autorisation est toujours vérifiée
  4. Sécurité : Plus difficile d'oublier les vérifications d'autorisation
  5. Maintenabilité : Logique d'autorisation centralisée
  6. Performance : Peut terminer les requêtes tôt sans atteindre les controllers
Bonne Pratique

Utilisez le middleware pour les préoccupations transversales comme l'authentification, l'autorisation, la journalisation et la limitation de débit. Gardez vos controllers concentrés sur la logique métier.

Bonus : Laravel Pail - Surveillance des Logs en Temps Réel#

Laravel Pail vous permet de surveiller les logs de votre application en temps réel directement depuis la console. Il fonctionne avec n'importe quel driver de log et fournit des options de filtrage.

Installation et Utilisation#

pail-setup
# Install Laravel Pail
composer require laravel/pail

# Basic usage
php artisan pail

# Verbose output (avoid truncation)
php artisan pail -v

# Maximum verbosity with stack traces
php artisan pail -vv

Exemple avec Logging#

Ajoutez la journalisation à votre ProductController :

ProductController.php
public function index(): Response
{
    $products = Product::with('user:id,name')->latest()->get();

    Log::info('Products index accessed', [
        'user_id' => auth()->id(),
        'products_count' => $products->count(),
        'products' => $products->toArray()
    ]);

    return Inertia::render('Products/Index', [
        'products' => $products,
    ]);
}

Maintenant surveillez vos logs en temps réel :

pail-monitoring
# Monitor all logs with verbose output
php artisan pail -v

# Filter by specific content
php artisan pail --filter="Products index accessed"