f813c1732e
Volet A — relation Category <-> CategoryType passee de ManyToOne a ManyToMany (jonction category_category_type). Au moins un type obligatoire (Assert\Count), unicite du nom desormais GLOBALE parmi les actifs (uq_category_name_active). Migration avec backfill + drop de l'ancienne colonne. Contrat Shared CategoryInterface : getCategoryTypeCode() -> getCategoryTypeCodes(): array ; RG-2.10 fournisseurs (Supplier / SupplierAddress / fixtures) revalident « contient FOURNISSEUR ». Provider/Repository : filtre type via sous-requete EXISTS (sans tronquer la collection embarquee), eager-load anti-N+1. Volet B — bouton « Filtres » sur la liste des categories (recherche nom + types multi en OR), sur le modele du Repertoire Clients ; etat local, jamais persiste en URL. Filtres back ?name= et ?typeId[]= sur la collection. Front : multi-select MalioSelectCheckbox, useCategoryForm en categoryTypeIds[], colonne « Types », i18n. ColumnCommentsCatalog + makefile test-db-setup alignes sur le nouvel index partiel. Tests Catalog/Commercial adaptes + CategoryFilterTest.
150 lines
6.8 KiB
PHP
150 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace DoctrineMigrations;
|
|
|
|
use Doctrine\DBAL\Schema\Schema;
|
|
use Doctrine\Migrations\AbstractMigration;
|
|
|
|
/**
|
|
* Catalog — Category multi-types : passage de la relation Category -> CategoryType
|
|
* de ManyToOne a ManyToMany.
|
|
*
|
|
* Ordre critique :
|
|
* 1. Creation de la table de jonction `category_category_type` (FK category ON
|
|
* DELETE CASCADE, FK category_type ON DELETE RESTRICT — conserve le garde-fou
|
|
* « on ne supprime pas un type encore reference »).
|
|
* 2. Backfill : chaque categorie existante recoit une ligne de jonction vers son
|
|
* ancien `category_type_id` (avant de dropper la colonne).
|
|
* 3. Drop de l'index unique (LOWER(name), category_type_id), de l'index FK et de
|
|
* la colonne `category.category_type_id` (Postgres drope la FK dependante).
|
|
* 4. Nouvel index unique GLOBAL sur le nom : LOWER(name) WHERE deleted_at IS NULL
|
|
* (l'unicite n'est plus liee au type — RG-1.07 reformulee).
|
|
*
|
|
* Sur base fraiche, les categories seedees CLIENT (Distributeur/Courtier/Secteur/
|
|
* Autre) et FOURNISSEUR (Negociant/Cooperative/...) n'ont aucun nom en collision
|
|
* -> l'index unique global passe sans conflit.
|
|
*
|
|
* Migration placee au namespace racine `DoctrineMigrations` (regle ABSOLUE n°11) :
|
|
* Doctrine Migrations 3.x trie par FQCN puis version ; le namespace racine garantit
|
|
* l'ordre par timestamp apres les migrations d'init des tables.
|
|
*/
|
|
final class Version20260608120000 extends AbstractMigration
|
|
{
|
|
public function getDescription(): string
|
|
{
|
|
return 'Catalog : Category <-> CategoryType en ManyToMany (jonction category_category_type), unicite du nom globalisee.';
|
|
}
|
|
|
|
public function up(Schema $schema): void
|
|
{
|
|
// 1. Table de jonction.
|
|
$this->addSql(<<<'SQL'
|
|
CREATE TABLE category_category_type (
|
|
category_id INT NOT NULL,
|
|
category_type_id INT NOT NULL,
|
|
PRIMARY KEY (category_id, category_type_id),
|
|
CONSTRAINT fk_category_category_type_category
|
|
FOREIGN KEY (category_id) REFERENCES category (id) ON DELETE CASCADE,
|
|
CONSTRAINT fk_category_category_type_type
|
|
FOREIGN KEY (category_type_id) REFERENCES category_type (id) ON DELETE RESTRICT
|
|
)
|
|
SQL);
|
|
$this->addSql('CREATE INDEX idx_cat_cat_type_type ON category_category_type (category_type_id)');
|
|
|
|
$this->comment('category_category_type', '_table', 'Jointure M2M category <-> category_type (Catalog) — types portes par la categorie, au moins un obligatoire (RG-1.05).');
|
|
$this->comment('category_category_type', 'category_id', 'FK -> category.id, ON DELETE CASCADE — categorie portant le type.');
|
|
$this->comment('category_category_type', 'category_type_id', 'FK -> category_type.id, ON DELETE RESTRICT — type rattache (un type ne peut etre supprime tant qu il reste reference).');
|
|
|
|
// 2. Backfill depuis l'ancienne colonne ManyToOne (chaque categorie -> 1 type).
|
|
$this->addSql(<<<'SQL'
|
|
INSERT INTO category_category_type (category_id, category_type_id)
|
|
SELECT id, category_type_id FROM category
|
|
SQL);
|
|
|
|
// 3. Suppression de l'ancien modele : index unique par type, index FK, colonne.
|
|
$this->addSql('DROP INDEX uq_category_name_type_active');
|
|
$this->addSql('DROP INDEX idx_category_type_id');
|
|
// DROP COLUMN drope automatiquement la FK fk_category_type qui en depend.
|
|
$this->addSql('ALTER TABLE category DROP COLUMN category_type_id');
|
|
|
|
// 4. Unicite du nom desormais GLOBALE parmi les actifs (RG-1.07 reformulee).
|
|
$this->addSql(<<<'SQL'
|
|
CREATE UNIQUE INDEX uq_category_name_active
|
|
ON category (LOWER(name))
|
|
WHERE deleted_at IS NULL
|
|
SQL);
|
|
|
|
// Realignement de la doc SQL de `category` (le type n'est plus une colonne).
|
|
$this->comment('category', '_table', 'Categories — referentiel multi-types via la jonction category_category_type, soft-delete via deleted_at, unicite LOWER(name) GLOBALE parmi les actifs (uq_category_name_active).');
|
|
$this->comment('category', 'name', 'Libelle de la categorie (≤ 120 caracteres) — unique GLOBALEMENT parmi les actifs (RG-1.07, uq_category_name_active).');
|
|
}
|
|
|
|
public function down(Schema $schema): void
|
|
{
|
|
// Restauration best-effort de l'ancien modele ManyToOne (1 type par categorie).
|
|
$this->addSql('DROP INDEX IF EXISTS uq_category_name_active');
|
|
|
|
$this->addSql('ALTER TABLE category ADD COLUMN category_type_id INT DEFAULT NULL');
|
|
|
|
// Reprend le premier type de chaque categorie (l'ordre des types perdus
|
|
// au-dela du premier est best-effort : le modele cible n'en gardait qu'un).
|
|
$this->addSql(<<<'SQL'
|
|
UPDATE category c
|
|
SET category_type_id = (
|
|
SELECT cct.category_type_id
|
|
FROM category_category_type cct
|
|
WHERE cct.category_id = c.id
|
|
ORDER BY cct.category_type_id ASC
|
|
LIMIT 1
|
|
)
|
|
SQL);
|
|
|
|
// Categories sans aucun type (theorique) : on les rattache a defaut au
|
|
// premier type existant pour pouvoir reposer le NOT NULL.
|
|
$this->addSql(<<<'SQL'
|
|
UPDATE category
|
|
SET category_type_id = (SELECT id FROM category_type ORDER BY id ASC LIMIT 1)
|
|
WHERE category_type_id IS NULL
|
|
SQL);
|
|
|
|
$this->addSql('ALTER TABLE category ALTER COLUMN category_type_id SET NOT NULL');
|
|
$this->addSql(<<<'SQL'
|
|
ALTER TABLE category
|
|
ADD CONSTRAINT fk_category_type
|
|
FOREIGN KEY (category_type_id) REFERENCES category_type (id) ON DELETE RESTRICT
|
|
SQL);
|
|
$this->addSql('CREATE INDEX idx_category_type_id ON category (category_type_id)');
|
|
$this->addSql(<<<'SQL'
|
|
CREATE UNIQUE INDEX uq_category_name_type_active
|
|
ON category (LOWER(name), category_type_id)
|
|
WHERE deleted_at IS NULL
|
|
SQL);
|
|
|
|
$this->addSql('DROP TABLE category_category_type');
|
|
}
|
|
|
|
/**
|
|
* Emet un `COMMENT ON TABLE` (colonne speciale `_table`) ou `COMMENT ON COLUMN`
|
|
* en dollar-quoting Postgres ($_$...$_$) pour eviter tout echappement.
|
|
*/
|
|
private function comment(string $table, string $column, string $description): void
|
|
{
|
|
$quotedTable = '"'.str_replace('"', '""', $table).'"';
|
|
|
|
if ('_table' === $column) {
|
|
$this->addSql(sprintf('COMMENT ON TABLE %s IS $_$%s$_$', $quotedTable, $description));
|
|
|
|
return;
|
|
}
|
|
|
|
$this->addSql(sprintf(
|
|
'COMMENT ON COLUMN %s.%s IS $_$%s$_$',
|
|
$quotedTable,
|
|
'"'.str_replace('"', '""', $column).'"',
|
|
$description,
|
|
));
|
|
}
|
|
}
|