Retour au cours

backend / php-laravel

Migrations et seeders

Leçon 121 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi les migrations versionnent le schéma de base de données comme le code
  • Écrire les méthodes symétriques up() et down() d'une migration
  • Déclarer une clé étrangère avec une règle de suppression en cascade
  • Générer des données de test réalistes avec les factories, y compris des "states" personnalisés
  • Distinguer les commandes migrate, migrate:rollback et migrate:fresh, et savoir laquelle est dangereuse

Dans quel contexte ?

Une équipe de trois développeurs travaille sur le même projet Laravel, chacun sur sa propre base de données locale. Sans migrations, chacun devrait recréer manuellement les tables à chaque changement de schéma, avec un risque constant de désynchronisation. Les migrations résolvent ce problème en transformant le schéma de base de données en fichiers versionnés dans Git, appliqués dans le même ordre chez tout le monde.

D'abord, une migration, c'est du code qui décrit un changement de schéma

Chaque fichier de migration porte un timestamp dans son nom (2024_01_15_000000_create_produits_table.php), ce qui garantit un ordre d'exécution cohérent. À l'intérieur, la méthode up() décrit le changement à appliquer (créer une table, ajouter une colonne), tandis que down() décrit précisément comment l'annuler.

Cette symétrie n'est pas décorative : si une migration doit être annulée en développement (migrate:rollback), c'est down() qui s'exécute, et un down() incomplet ou incorrect peut laisser la base dans un état incohérent.

Prérequis

Cette leçon suppose d'avoir compris les relations Eloquent (leçon précédente), puisque les clés étrangères déclarées ici correspondent directement à ces relations.

Ensuite, une clé étrangère avec une règle claire de suppression

$table->foreignId('categorie_id')->constrained('categories')->cascadeOnDelete(); déclare une clé étrangère typée ET précise ce qui doit arriver aux produits si leur catégorie est supprimée : ici, ils seraient supprimés en cascade. D'autres options existent (nullOnDelete(), restrictOnDelete()) selon la règle métier voulue.

Méthode de suppressionComportement si la ligne parente est supprimée
cascadeOnDelete()Supprime aussi les lignes enfants
nullOnDelete()Met la clé étrangère à null sur les lignes enfants
restrictOnDelete()Empêche la suppression tant que des enfants existent

Piège fréquent

Oublier de réfléchir à cette règle de suppression avant la mise en production peut mener à des lignes orphelines (référence vers une catégorie qui n'existe plus) ou, à l'inverse, à des suppressions en cascade non désirées qui effacent bien plus de données que prévu.

Il reste à générer des données de test réalistes

Écrire manuellement des dizaines de produits de test pour chaque session de développement est fastidieux. Une factory (ProduitFactory) génère des données réalistes via une librairie de faux-semblants ($this->faker), et peut définir des "states" réutilisables comme epuise() pour représenter des cas particuliers du métier.

Astuce

Produit::factory(20)->for($categorie)->create() crée 20 produits tous rattachés à la même catégorie en une seule expression, en s'appuyant directement sur la relation Eloquent définie en leçon précédente.

Enfin, une commande à ne surtout jamais lancer en production

migrate:fresh supprime absolument toutes les tables avant de rejouer toutes les migrations depuis zéro — extrêmement pratique en développement pour repartir d'une base propre, mais catastrophique en production où elle détruirait définitivement toutes les données réelles.

Piège dangereux

Il n'existe aucune confirmation interactive avant l'exécution de migrate:fresh : la commande s'exécute immédiatement. Ne jamais la faire figurer dans un script de déploiement automatisé destiné à la production.

Maintenant que ton schéma de base de données est en place et peuplé de données de test, la prochaine leçon revient sur un sujet déjà effleuré en contrôleur : comment valider rigoureusement des données entrantes grâce aux Form Requests dédiés.

Commandes & code

Migrations et seeders

php
<?php
// database/migrations/2024_01_15_000000_create_produits_table.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('produits', function (Blueprint $table) {
            $table->id();                                          // bigint auto-increment
            $table->string('nom', 120);
            $table->text('description')->nullable();
            $table->decimal('prix', 10, 2);
            $table->unsignedInteger('stock')->default(0);
            $table->boolean('est_disponible')->default(true);
            $table->json('metadonnees')->nullable();
            $table->foreignId('categorie_id')                      // clé étrangère typée
                ->constrained('categories')
                ->cascadeOnDelete();
            $table->timestamps();                                  // created_at / updated_at
            $table->softDeletes();                                 // deleted_at (suppression logique)

            $table->index(['categorie_id', 'est_disponible']);      // index composite
            $table->unique('sku');
        });
    }

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

// Migration d'altération d'une table existante
return new class extends Migration {
    public function up(): void {
        Schema::table('produits', function (Blueprint $table) {
            $table->string('sku', 40)->after('nom');
            $table->index('sku');
        });
    }

    public function down(): void {
        Schema::table('produits', function (Blueprint $table) {
            $table->dropIndex(['sku']);
            $table->dropColumn('sku');
        });
    }
};
php
<?php
// database/factories/ProduitFactory.php : génère des données réalistes pour les tests/seeds
namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class ProduitFactory extends Factory {
    public function definition(): array {
        return [
            'nom' => $this->faker->words(3, true),
            'prix' => $this->faker->randomFloat(2, 5, 500),
            'stock' => $this->faker->numberBetween(0, 200),
            'sku' => strtoupper($this->faker->bothify('???-####')),
            'categorie_id' => \App\Models\Categorie::factory(),
        ];
    }

    // State personnalisé réutilisable : Produit::factory()->epuise()->create()
    public function epuise(): static {
        return $this->state(fn (array $attributs) => ['stock' => 0, 'est_disponible' => false]);
    }
}

// database/seeders/DatabaseSeeder.php
namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\{Categorie, Produit};

class DatabaseSeeder extends Seeder {
    public function run(): void {
        Categorie::factory(5)->create()->each(function ($categorie) {
            Produit::factory(20)->for($categorie)->create();
        });

        Produit::factory(10)->epuise()->create();
    }
}
bash
php artisan make:migration create_produits_table
php artisan migrate                 # applique les migrations en attente
php artisan migrate:rollback        # annule le dernier batch de migrations
php artisan migrate:fresh --seed    # DROP toutes les tables, remigre, puis seed (dev uniquement !)
php artisan db:seed --class=DatabaseSeeder

Résumé

  • Chaque migration a un up() (appliquer) et un down() (annuler) symétriques.
  • foreignId()->constrained()->cascadeOnDelete() déclare une FK avec règle de suppression en cascade.
  • Les factories génèrent des données de test réalistes, réutilisables via des "states" (->epuise()).
  • migrate:fresh détruit les données : jamais en production.

Exercices pratiques

1 disponible
1

Mission : eviter une catastrophe de suppression en cascade

Objectif : Corriger une migration dont la regle de suppression de cle etrangere est mal choisie, et generer des donnees de test realistes.

Contexte

La migration de la table produits declare $table->foreignId('categorie_id')->constrained('categories')->cascadeOnDelete();. Un administrateur supprime par erreur une categorie de test nommee "Demo", pensant qu'elle ne contenait rien d'important : 340 produits reels, mal classes dans cette categorie par un import defaillant, disparaissent instantanement avec elle. L'equipe veut aussi generer un jeu de donnees de test avec des produits en rupture de stock, sans les creer un par un a la main.

Résoudre l’exercice →