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 : * avec plusieurs migrations_paths, Doctrine Migrations 3.x trie par FQCN * alphabetique -> une migration `App\Module\...` passerait avant les * `DoctrineMigrations\...` sur base vide, donc avant la creation des tables * `category` / `category_type` / `category_category_type`. Le namespace racine * garantit l'ordre par timestamp. * * 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 ERP-84 / Version20260605120000). 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 PRESTATAIRE). */ final class Version20260612080000 extends AbstractMigration { /** * Categories de demonstration du type PRESTATAIRE : 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, * partage avec les codes CLIENT / FOURNISSEUR — aucune collision ici). Le nom * est unique GLOBALEMENT parmi les actifs (uq_category_name_active) : les * libelles ci-dessous n'entrent en collision avec aucune categorie seedee. */ private const array PROVIDER_CATEGORIES = [ 'Maintenance industrielle' => 'MAINTENANCE_INDUSTRIELLE', 'Nettoyage' => 'NETTOYAGE', 'Transport' => 'TRANSPORT', ]; public function getDescription(): string { return 'M3 1.1 : cree le CategoryType PRESTATAIRE + seed des categories prestataires (Maintenance industrielle, Nettoyage, Transport).'; } public function up(Schema $schema): void { // 1. Type PRESTATAIRE (idempotent via l'index unique uq_category_type_code). $this->addSql(<<<'SQL' INSERT INTO category_type (code, label) VALUES ('PRESTATAIRE', 'Prestataire') ON CONFLICT (code) DO NOTHING SQL); foreach (self::PROVIDER_CATEGORIES as $name => $code) { // 2a. Categorie sous PRESTATAIRE (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 PRESTATAIRE (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 = 'PRESTATAIRE' 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 = 'PRESTATAIRE')", ['codes' => array_values(self::PROVIDER_CATEGORIES)], ['codes' => ArrayParameterType::STRING], ); $this->addSql(<<<'SQL' DELETE FROM category_type WHERE code = 'PRESTATAIRE' AND NOT EXISTS ( SELECT 1 FROM category_category_type cct WHERE cct.category_type_id = category_type.id ) SQL); } }