pas de `COMMENT ON COLUMN` (regle ABSOLUE n°12) : * la migration ne fait que des INSERT de donnees de reference. * * Namespace racine `DoctrineMigrations` (regle ABSOLUE n°11) et NON modulaire : * garantit l'ordre par timestamp avant les migrations modulaires sur base vide. * * Idempotence : `INSERT ... ON CONFLICT (code) DO NOTHING` pour le type, * `INSERT ... SELECT ... WHERE NOT EXISTS` pour chaque categorie et chaque ligne * de jonction (aligne sur le pattern PRESTATAIRE / Version20260612080000). En prod * la table `category` est vide (aucune fixture metier) ; en dev/test le purger * Doctrine vide `category` / `category_type` avant les fixtures qui reproduisent le * meme etat final (CategoryTypeFixtures / CategoryFixtures etendus a ADRESSE). */ final class Version20260625100000 extends AbstractMigration { /** * Categories de demonstration du type ADRESSE : nom => code stable. Le code est * la cle metier (slug MAJUSCULE du nom, miroir du CategoryCodeGenerator) et reste * unique parmi les actifs (uq_category_code). Le nom est unique GLOBALEMENT parmi * les actifs (uq_category_name_active) : aucune collision avec les categories * deja seedees (CLIENT / FOURNISSEUR / PRESTATAIRE). */ private const array ADDRESS_CATEGORIES = [ 'Siège' => 'SIEGE', 'Contact issues' => 'CONTACT_ISSUES', 'Facturation' => 'FACTURATION', 'Livraison' => 'LIVRAISON', 'Approvisionnement' => 'APPROVISIONNEMENT', 'Méthaniseur' => 'METHANISEUR', ]; public function getDescription(): string { return 'Taxonomie ADRESSE : cree le CategoryType ADRESSE + seed des categories adresse (Siege, Contact issues, Facturation, Livraison, Approvisionnement, Methaniseur).'; } public function up(Schema $schema): void { // 1. Type ADRESSE (idempotent via l'index unique uq_category_type_code). $this->addSql(<<<'SQL' INSERT INTO category_type (code, label) VALUES ('ADRESSE', 'Adresse') ON CONFLICT (code) DO NOTHING SQL); foreach (self::ADDRESS_CATEGORIES as $name => $code) { // 2a. Categorie sous ADRESSE (si le code est libre parmi les actifs). // created_at/updated_at NOT NULL -> NOW() ; le blame reste null // (seed hors contexte HTTP, libelle « Systeme » cote front). $this->addSql(<<<'SQL' INSERT INTO category (name, code, created_at, updated_at) SELECT :name, :code, NOW(), NOW() WHERE NOT EXISTS ( SELECT 1 FROM category c WHERE c.code = :code AND c.deleted_at IS NULL ) SQL, ['name' => $name, 'code' => $code]); // 2b. Jonction M2M categorie <-> type ADRESSE (modele courant). $this->addSql(<<<'SQL' INSERT INTO category_category_type (category_id, category_type_id) SELECT c.id, ct.id FROM category c CROSS JOIN category_type ct WHERE c.code = :code AND c.deleted_at IS NULL AND ct.code = 'ADRESSE' AND NOT EXISTS ( SELECT 1 FROM category_category_type cct WHERE cct.category_id = c.id AND cct.category_type_id = ct.id ) SQL, ['code' => $code]); } } public function down(Schema $schema): void { // Best-effort : on retire d'abord les categories seedees (par code) — la FK // category_category_type est ON DELETE CASCADE cote category, donc les lignes // de jonction partent avec —, puis le type s'il n'est plus reference. $this->addSql( 'DELETE FROM category WHERE code IN (:codes) ' .'AND id IN (SELECT category_id FROM category_category_type cct ' ."JOIN category_type ct ON ct.id = cct.category_type_id WHERE ct.code = 'ADRESSE')", ['codes' => array_values(self::ADDRESS_CATEGORIES)], ['codes' => ArrayParameterType::STRING], ); $this->addSql(<<<'SQL' DELETE FROM category_type WHERE code = 'ADRESSE' AND NOT EXISTS ( SELECT 1 FROM category_category_type cct WHERE cct.category_type_id = category_type.id ) SQL); } }