Skip to Content

Chapitre 6.4 — Migrations & fixtures

Partie 6 : Doctrine ORM — Chapitre 4/7 Version de référence : Doctrine Migrations 3 / Symfony 7.4.

⏱️ TL;DR

  • Une migration est un script versionné qui fait évoluer le schéma de la base (créer une table, ajouter une colonne). On les génère automatiquement par différence entre vos entités et la base : make:migration.
  • On applique avec doctrine:migrations:migrate, on inspecte avec migrations:status. Chaque migration a un up() (appliquer) et un down() (annuler).
  • Règle d’or : on ne modifie jamais le schéma à la main en prod — toujours via une migration versionnée et committée. C’est l’historique reproductible de votre base.
  • Les fixtures peuplent la base de données de test/démo (DoctrineFixturesBundle, make:fixtures). doctrine:fixtures:load purge puis recharge.
  • Pour des données réalistes, on combine avec Faker (ou Foundry).
  • Pour un dev Next.js : migrations ≈ prisma migrate (avec up/down explicites), fixtures ≈ prisma db seed.

🎯 Objectifs

  • Générer, lire et appliquer des migrations.
  • Comprendre pourquoi on ne touche jamais le schéma à la main.
  • Créer des fixtures avec des relations et des données réalistes.
  • Distinguer migrations (schéma) et fixtures (données).

1. Pourquoi des migrations

Vos entités décrivent le schéma voulu. La base a un schéma actuel. Une migration est le script de transition de l’un vers l’autre, versionné dans git. Ainsi, chaque environnement (votre machine, celle d’un collègue, la CI, la prod) applique la même séquence de changements → un schéma identique et reproductible.

2. Le cycle de travail

# 1. Vous modifiez/ajoutez une entité (ex. nouvelle propriété) # 2. Générer la migration par différence entités ↔ base php bin/console make:migration # 3. RELIRE le fichier généré dans migrations/ (important !) # 4. Appliquer php bin/console doctrine:migrations:migrate # Inspecter / diagnostiquer php bin/console doctrine:migrations:status php bin/console doctrine:migrations:list

Une migration générée ressemble à ceci :

// migrations/Version20260709120000.php final class Version20260709120000 extends AbstractMigration { public function getDescription(): string { return 'Ajoute la colonne seats à bookings'; } public function up(Schema $schema): void { $this->addSql('ALTER TABLE bookings ADD seats INT DEFAULT 1 NOT NULL'); } public function down(Schema $schema): void { $this->addSql('ALTER TABLE bookings DROP seats'); } }

up() applique le changement, down() le réverte. Doctrine tient une table doctrine_migration_versions qui mémorise les migrations déjà exécutées → une migration n’est jamais rejouée.

⚠️ Piège : toujours relire la migration générée avant de l’appliquer. Le diff automatique peut proposer des DROP inattendus (surtout si votre base a dérivé du mapping), ou manquer une nuance (renommage vu comme drop+add → perte de données). Le générateur est une aide, pas un oracle : vous restez responsable du SQL exécuté.

3. La règle d’or : jamais de modification manuelle en prod

Ne faites jamais un ALTER TABLE à la main sur la base de prod (ni schema:update). Pourquoi ?

  • La modification ne serait pas versionnée → les autres environnements divergent.
  • Aucune trace, aucun down(), aucun moyen de reproduire.

Le flux correct : modifier l’entité → make:migration → relire → committer → appliquer partout via migrations:migrate (dev, CI, prod). C’est ce qui rend le schéma fiable et reproductible.

⚠️ Piège : php bin/console doctrine:schema:update --force synchronise le schéma sans migration. Pratique pour un prototype jetable, dangereux en équipe/prod (aucun historique, risque de drop). Réservez-le à un tout début de projet, puis passez exclusivement aux migrations. doctrine:schema:validate (qui vérifie que mapping et base concordent) est, lui, toujours bon à lancer.

4. Les fixtures : peupler des données

Les migrations gèrent le schéma (structure). Les fixtures gèrent les données (contenu de test/démo). On installe le bundle :

composer require --dev orm-fixtures php bin/console make:fixtures BookingFixtures

Une fixture implémente load() et crée des entités :

// src/DataFixtures/AppFixtures.php declare(strict_types=1); namespace App\DataFixtures; use App\Entity\Provider; use App\Entity\Service; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; final class AppFixtures extends Fixture { public function load(ObjectManager $manager): void { $provider = (new Provider()) ->setName('Studio Zen') ->setSlug('studio-zen'); $manager->persist($provider); for ($i = 1; $i <= 5; $i++) { $service = (new Service()) ->setName("Séance de coaching #$i") ->setProvider($provider); // relation $manager->persist($service); } $manager->flush(); // un seul flush à la fin } }

On charge (⚠️ ça purge la base d’abord) :

php bin/console doctrine:fixtures:load

5. Données réalistes : Faker et références

Pour des jeux de données crédibles (noms, e-mails, dates), on utilise Faker (fakerphp/faker) :

use Faker\Factory; $faker = Factory::create('fr_FR'); for ($i = 0; $i < 50; $i++) { $booking = (new Booking()) ->setCustomerEmail($faker->email()) ->setStart(\DateTimeImmutable::createFromMutable($faker->dateTimeBetween('now', '+30 days'))) ->setSeats($faker->numberBetween(1, 4)); $manager->persist($booking); }

Quand une fixture a besoin d’une entité créée par une autre fixture, on utilise les références ($this->addReference('provider-zen', $provider) puis $this->getReference(...)) et l’ordre via DependentFixtureInterface.

💡 Pour un dev Next.js : fixtures ≈ prisma db seed, Faker ≈ @faker-js/faker. Le concept est identique : un script idempotent qui remplit la base de données de démo/test. Une alternative moderne, Foundry (zenstruck/foundry), offre une API de « factories » très proche de ce que vous connaissez côté JS (BookingFactory::createMany(50)).

6. Migrations vs fixtures : ne pas confondre

MigrationsFixtures
Gèrentle schéma (structure)les données (contenu)
Versionnées en gitoui (historique du schéma)oui (le code des fixtures)
En productionoui, obligatoiresnon (démo/dev/test seulement)
Commandemigrations:migratefixtures:load (purge !)

⚠️ Piège : mettre des données métier (ex. la liste des pays, des rôles) dans des fixtures alors qu’elles doivent exister en prod. Les fixtures ne sont pas chargées en prod. Pour des données de référence nécessaires en prod, insérez-les dans une migration (INSERT dans up()) ou une commande d’initialisation dédiée.

✏️ Exercices

Exercice 1. Vous ajoutez une propriété phone (string, nullable) à Provider. Quelles commandes exécutez-vous, et quelle étape ne faut-il pas sauter ?

✅ Solution

  1. Ajouter la propriété + son mapping #[ORM\Column(length: 20, nullable: true)] dans l’entité.
  2. php bin/console make:migration.
  3. Relire le fichier de migration généré (étape à ne pas sauter — vérifier qu’il fait bien un ADD phone et rien d’inattendu).
  4. php bin/console doctrine:migrations:migrate.
  5. Committer la migration.

Exercice 2. Un collègue a fait ALTER TABLE à la main sur la base de dev pour tester vite. Quel problème cela pose-t-il, et que fallait-il faire ?

✅ Solution

Le changement n’est pas versionné : sa base diverge de celle des autres (et de la prod), sans historique ni down(). Au prochain make:migration, le diff sera faussé. Il fallait modifier l’entité puis générer une migration (make:migration), la relire et l’appliquer (migrations:migrate) — reproductible et partagé. La base ne se modifie que par migration.

Exercice 3. Où placer la liste des rôles de référence qui doit exister en production : fixtures ou migration ? Pourquoi ?

✅ Solution

Dans une migration (ou une commande d’initialisation), car les fixtures ne sont pas exécutées en production. Les fixtures servent aux données de démo/test. Pour des données de référence requises en prod, un INSERT dans le up() d’une migration (ou bin/console app:init) garantit leur présence partout.

🧠 Quiz de révision

1. Qu’est-ce qu’une migration et comment la génère-t-on ?

Un script versionné qui fait évoluer le schéma, généré par différence entités ↔ base avec make:migration, avec un up() (appliquer) et un down() (annuler).

2. Pourquoi ne jamais modifier le schéma à la main en prod ?

Parce que ce ne serait pas versionné ni reproductible : les environnements divergeraient, sans historique ni possibilité d’annuler. Toujours passer par une migration committée.

3. Que font les fixtures, et que se passe-t-il au chargement ?

Elles peuplent la base de données de test/démo. doctrine:fixtures:load purge la base puis recharge — à ne pas lancer en prod.

4. Migrations ou fixtures pour le schéma ? Pour les données de démo ?

Migrations pour le schéma ; fixtures pour les données de démo/test.

5. Faut-il relire une migration auto-générée avant de l’appliquer ?

Oui : le diff peut proposer des DROP inattendus ou interpréter un renommage comme drop+add (perte de données). On reste responsable du SQL exécuté.


Prochain chapitre : 6.5 — Cycle de vie & événements — réagir à la persistance (timestamps, événements) proprement.