ce DROP s'executerait avant le CREATE TABLE client * (Version20260601000000). Le namespace racine garantit l'ordre par timestamp. */ final class Version20260603120000 extends AbstractMigration { /** Colonnes de contact inline supprimees du `client`. */ private const array INLINE_CONTACT_COLUMNS = [ 'first_name', 'last_name', 'phone_primary', 'phone_secondary', 'email', ]; public function getDescription(): string { return 'M1 : suppression du contact inline du Client (backfill vers client_contact puis DROP des 5 colonnes).'; } public function up(Schema $schema): void { // 1. Backfill : tout client SANS contact recoit une ligne client_contact // (position 0) reprenant ses champs inline. phone_primary / email du // client sont NOT NULL -> toujours une donnee a reporter. Le CHECK // chk_client_contact_name (first_name OU last_name) est garanti par le // fallback company_name si jamais les deux noms etaient null (cas qui ne // devrait pas exister, RG-1.01 ayant ete appliquee a l'ecriture). // created_at/updated_at NOT NULL -> NOW() ; created_by/updated_by null // (backfill hors contexte HTTP, libelle « Systeme » cote front). $this->addSql(<<<'SQL' INSERT INTO client_contact ( client_id, first_name, last_name, phone_primary, phone_secondary, email, position, created_at, updated_at ) SELECT c.id, c.first_name, CASE WHEN c.first_name IS NULL AND c.last_name IS NULL THEN c.company_name ELSE c.last_name END, c.phone_primary, c.phone_secondary, c.email, 0, NOW(), NOW() FROM client c WHERE NOT EXISTS ( SELECT 1 FROM client_contact cc WHERE cc.client_id = c.id ) SQL); // 2. DROP des 5 colonnes inline (rien a documenter : suppression). $this->addSql(<<<'SQL' ALTER TABLE client DROP COLUMN first_name, DROP COLUMN last_name, DROP COLUMN phone_primary, DROP COLUMN phone_secondary, DROP COLUMN email SQL); } public function down(Schema $schema): void { // Best-effort : on RECREE les 5 colonnes (en NULLABLE — l'etat NOT NULL // d'origine de phone_primary/email ne peut etre restaure sur une table // peuplee sans risque) et on retro-alimente depuis le contact principal // (position minimale) de chaque client. La donnee n'est pas garantie // identique a l'origine : ce down() sert au rollback technique, pas a une // restauration fidele. $this->addSql('ALTER TABLE client ADD COLUMN first_name VARCHAR(120) DEFAULT NULL'); $this->addSql('ALTER TABLE client ADD COLUMN last_name VARCHAR(120) DEFAULT NULL'); $this->addSql('ALTER TABLE client ADD COLUMN phone_primary VARCHAR(20) DEFAULT NULL'); $this->addSql('ALTER TABLE client ADD COLUMN phone_secondary VARCHAR(20) DEFAULT NULL'); $this->addSql('ALTER TABLE client ADD COLUMN email VARCHAR(180) DEFAULT NULL'); // Retro-alimentation depuis le contact de position la plus basse. $this->addSql(<<<'SQL' UPDATE client c SET first_name = cc.first_name, last_name = cc.last_name, phone_primary = cc.phone_primary, phone_secondary = cc.phone_secondary, email = cc.email FROM ( SELECT DISTINCT ON (client_id) client_id, first_name, last_name, phone_primary, phone_secondary, email FROM client_contact ORDER BY client_id, position ASC, id ASC ) cc WHERE cc.client_id = c.id SQL); // Re-pose des commentaires d'origine (regle ABSOLUE n°12) — dollar-quoting // Postgres pour eviter tout echappement d apostrophe. $this->addSql('COMMENT ON COLUMN client.first_name IS $_$Prenom du contact principal (capitalise serveur, RG-1.19). first_name OU last_name obligatoire (RG-1.01).$_$'); $this->addSql('COMMENT ON COLUMN client.last_name IS $_$Nom du contact principal (capitalise serveur, RG-1.19). first_name OU last_name obligatoire (RG-1.01).$_$'); $this->addSql('COMMENT ON COLUMN client.phone_primary IS $_$Telephone principal — stocke en chiffres uniquement (RG-1.20). Obligatoire.$_$'); $this->addSql('COMMENT ON COLUMN client.phone_secondary IS $_$Telephone secondaire optionnel — chiffres uniquement (RG-1.20).$_$'); $this->addSql('COMMENT ON COLUMN client.email IS $_$Email principal (lowercase serveur, RG-1.21). NON unique (RG-1.17 supprimee, Q4).$_$'); } }