feat(composant) : create composant slot tables and migrate data from structure JSON

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-12 18:20:31 +01:00
parent c01b71fe06
commit 5194543d16
4 changed files with 344 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260312171810 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create piece_products join table and migrate data from Piece.productIds JSON column';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE IF NOT EXISTS piece_products (piece_id VARCHAR(36) NOT NULL, product_id VARCHAR(36) NOT NULL, PRIMARY KEY (piece_id, product_id))');
$this->addSql('CREATE INDEX IF NOT EXISTS IDX_87C835B5C40FCFA8 ON piece_products (piece_id)');
$this->addSql('CREATE INDEX IF NOT EXISTS IDX_87C835B54584665A ON piece_products (product_id)');
$this->addSql('ALTER TABLE piece_products DROP CONSTRAINT IF EXISTS FK_87C835B5C40FCFA8');
$this->addSql('ALTER TABLE piece_products ADD CONSTRAINT FK_87C835B5C40FCFA8 FOREIGN KEY (piece_id) REFERENCES pieces (id) ON DELETE CASCADE NOT DEFERRABLE');
$this->addSql('ALTER TABLE piece_products DROP CONSTRAINT IF EXISTS FK_87C835B54584665A');
$this->addSql('ALTER TABLE piece_products ADD CONSTRAINT FK_87C835B54584665A FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE NOT DEFERRABLE');
// Migrate Piece.productIds JSON array → piece_products join table
$this->addSql(<<<'SQL'
INSERT INTO piece_products (piece_id, product_id)
SELECT DISTINCT p.id, pid.value
FROM pieces p,
LATERAL jsonb_array_elements_text(p.productids::jsonb) AS pid(value)
WHERE p.productids IS NOT NULL
AND p.productids::jsonb != '[]'::jsonb
AND jsonb_array_length(p.productids::jsonb) > 0
AND EXISTS (SELECT 1 FROM products pr WHERE pr.id = pid.value)
AND NOT EXISTS (SELECT 1 FROM piece_products pp WHERE pp.piece_id = p.id AND pp.product_id = pid.value)
SQL);
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE piece_products DROP CONSTRAINT IF EXISTS FK_87C835B5C40FCFA8');
$this->addSql('ALTER TABLE piece_products DROP CONSTRAINT IF EXISTS FK_87C835B54584665A');
$this->addSql('DROP TABLE IF EXISTS piece_products');
}
}

View File

@@ -0,0 +1,248 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Create composant slot tables and migrate existing JSON data from composant.structure.
*/
final class Version20260312190000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Create composant_piece_slots, composant_subcomponent_slots, composant_product_slots tables and migrate data from composant.structure JSON';
}
public function up(Schema $schema): void
{
// ── Table creation (idempotent) ──────────────────────────────────────
$this->addSql(<<<'SQL'
CREATE TABLE IF NOT EXISTS composant_piece_slots (
id VARCHAR(36) NOT NULL,
"composantid" VARCHAR(36) NOT NULL,
"typepieceid" VARCHAR(36) DEFAULT NULL,
"selectedpieceid" VARCHAR(36) DEFAULT NULL,
quantity INT NOT NULL DEFAULT 1,
position INT NOT NULL DEFAULT 0,
"createdat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
"updatedat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY (id)
)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE IF NOT EXISTS composant_subcomponent_slots (
id VARCHAR(36) NOT NULL,
"composantid" VARCHAR(36) NOT NULL,
alias VARCHAR(255) DEFAULT NULL,
"familycode" VARCHAR(255) DEFAULT NULL,
"typecomposantid" VARCHAR(36) DEFAULT NULL,
"selectedcomposantid" VARCHAR(36) DEFAULT NULL,
position INT NOT NULL DEFAULT 0,
"createdat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
"updatedat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY (id)
)
SQL);
$this->addSql(<<<'SQL'
CREATE TABLE IF NOT EXISTS composant_product_slots (
id VARCHAR(36) NOT NULL,
"composantid" VARCHAR(36) NOT NULL,
"typeproductid" VARCHAR(36) DEFAULT NULL,
"selectedproductid" VARCHAR(36) DEFAULT NULL,
"familycode" VARCHAR(255) DEFAULT NULL,
position INT NOT NULL DEFAULT 0,
"createdat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
"updatedat" TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL,
PRIMARY KEY (id)
)
SQL);
// ── Indexes (idempotent) ─────────────────────────────────────────────
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_piece_slot_composant ON composant_piece_slots("composantid")');
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_piece_slot_piece ON composant_piece_slots("selectedpieceid")');
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_piece_slot_type ON composant_piece_slots("typepieceid")');
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_sub_slot_composant ON composant_subcomponent_slots("composantid")');
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_sub_slot_typecomp ON composant_subcomponent_slots("typecomposantid")');
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_sub_slot_selected ON composant_subcomponent_slots("selectedcomposantid")');
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_prod_slot_composant ON composant_product_slots("composantid")');
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_prod_slot_type ON composant_product_slots("typeproductid")');
$this->addSql('CREATE INDEX IF NOT EXISTS idx_comp_prod_slot_selected ON composant_product_slots("selectedproductid")');
// ── Foreign keys (idempotent via DO $$ block) ────────────────────────
// composant_piece_slots FKs
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_piece_slot_composant') THEN
ALTER TABLE composant_piece_slots
ADD CONSTRAINT fk_comp_piece_slot_composant
FOREIGN KEY ("composantid") REFERENCES composants (id) ON DELETE CASCADE;
END IF;
END $$
SQL);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_piece_slot_type') THEN
ALTER TABLE composant_piece_slots
ADD CONSTRAINT fk_comp_piece_slot_type
FOREIGN KEY ("typepieceid") REFERENCES model_types (id) ON DELETE SET NULL;
END IF;
END $$
SQL);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_piece_slot_piece') THEN
ALTER TABLE composant_piece_slots
ADD CONSTRAINT fk_comp_piece_slot_piece
FOREIGN KEY ("selectedpieceid") REFERENCES pieces (id) ON DELETE SET NULL;
END IF;
END $$
SQL);
// composant_subcomponent_slots FKs
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_sub_slot_composant') THEN
ALTER TABLE composant_subcomponent_slots
ADD CONSTRAINT fk_comp_sub_slot_composant
FOREIGN KEY ("composantid") REFERENCES composants (id) ON DELETE CASCADE;
END IF;
END $$
SQL);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_sub_slot_typecomp') THEN
ALTER TABLE composant_subcomponent_slots
ADD CONSTRAINT fk_comp_sub_slot_typecomp
FOREIGN KEY ("typecomposantid") REFERENCES model_types (id) ON DELETE SET NULL;
END IF;
END $$
SQL);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_sub_slot_selected') THEN
ALTER TABLE composant_subcomponent_slots
ADD CONSTRAINT fk_comp_sub_slot_selected
FOREIGN KEY ("selectedcomposantid") REFERENCES composants (id) ON DELETE SET NULL;
END IF;
END $$
SQL);
// composant_product_slots FKs
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_prod_slot_composant') THEN
ALTER TABLE composant_product_slots
ADD CONSTRAINT fk_comp_prod_slot_composant
FOREIGN KEY ("composantid") REFERENCES composants (id) ON DELETE CASCADE;
END IF;
END $$
SQL);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_prod_slot_type') THEN
ALTER TABLE composant_product_slots
ADD CONSTRAINT fk_comp_prod_slot_type
FOREIGN KEY ("typeproductid") REFERENCES model_types (id) ON DELETE SET NULL;
END IF;
END $$
SQL);
$this->addSql(<<<'SQL'
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_comp_prod_slot_selected') THEN
ALTER TABLE composant_product_slots
ADD CONSTRAINT fk_comp_prod_slot_selected
FOREIGN KEY ("selectedproductid") REFERENCES products (id) ON DELETE SET NULL;
END IF;
END $$
SQL);
// ── Data migration: composant.structure.pieces → composant_piece_slots ──
$this->addSql(<<<'SQL'
INSERT INTO composant_piece_slots (id, "composantid", "typepieceid", "selectedpieceid", quantity, position, "createdat", "updatedat")
SELECT
'cl' || encode(gen_random_bytes(12), 'hex'),
c.id,
NULLIF(piece->'definition'->>'typePieceId', ''),
NULLIF(piece->>'selectedPieceId', ''),
1,
(ordinality - 1)::int,
NOW(), NOW()
FROM composants c,
LATERAL jsonb_array_elements(c.structure::jsonb->'pieces') WITH ORDINALITY AS t(piece, ordinality)
WHERE c.structure IS NOT NULL
AND (c.structure::jsonb->'pieces') IS NOT NULL
AND jsonb_array_length(c.structure::jsonb->'pieces') > 0
AND NOT EXISTS (SELECT 1 FROM composant_piece_slots cps WHERE cps."composantid" = c.id)
AND (NULLIF(piece->'definition'->>'typePieceId', '') IS NULL OR EXISTS (SELECT 1 FROM model_types mt WHERE mt.id = piece->'definition'->>'typePieceId'))
AND (NULLIF(piece->>'selectedPieceId', '') IS NULL OR EXISTS (SELECT 1 FROM pieces p WHERE p.id = piece->>'selectedPieceId'))
SQL);
// ── Data migration: composant.structure.subcomponents → composant_subcomponent_slots ──
$this->addSql(<<<'SQL'
INSERT INTO composant_subcomponent_slots (id, "composantid", alias, "familycode", "typecomposantid", "selectedcomposantid", position, "createdat", "updatedat")
SELECT
'cl' || encode(gen_random_bytes(12), 'hex'),
c.id,
COALESCE(sub->'definition'->>'alias', ''),
COALESCE(sub->'definition'->>'familyCode', ''),
NULLIF(sub->'definition'->>'typeComposantId', ''),
NULLIF(sub->>'selectedComponentId', ''),
(ordinality - 1)::int,
NOW(), NOW()
FROM composants c,
LATERAL jsonb_array_elements(c.structure::jsonb->'subcomponents') WITH ORDINALITY AS t(sub, ordinality)
WHERE c.structure IS NOT NULL
AND (c.structure::jsonb->'subcomponents') IS NOT NULL
AND jsonb_array_length(c.structure::jsonb->'subcomponents') > 0
AND NOT EXISTS (SELECT 1 FROM composant_subcomponent_slots css WHERE css."composantid" = c.id)
AND (NULLIF(sub->'definition'->>'typeComposantId', '') IS NULL OR EXISTS (SELECT 1 FROM model_types mt WHERE mt.id = sub->'definition'->>'typeComposantId'))
AND (NULLIF(sub->>'selectedComponentId', '') IS NULL OR EXISTS (SELECT 1 FROM composants sc WHERE sc.id = sub->>'selectedComponentId'))
SQL);
// ── Data migration: composant.structure.products → composant_product_slots ──
$this->addSql(<<<'SQL'
INSERT INTO composant_product_slots (id, "composantid", "typeproductid", "selectedproductid", "familycode", position, "createdat", "updatedat")
SELECT
'cl' || encode(gen_random_bytes(12), 'hex'),
c.id,
NULLIF(prod->'definition'->>'typeProductId', ''),
NULLIF(prod->>'selectedProductId', ''),
prod->'definition'->>'familyCode',
(ordinality - 1)::int,
NOW(), NOW()
FROM composants c,
LATERAL jsonb_array_elements(c.structure::jsonb->'products') WITH ORDINALITY AS t(prod, ordinality)
WHERE c.structure IS NOT NULL
AND (c.structure::jsonb->'products') IS NOT NULL
AND jsonb_array_length(c.structure::jsonb->'products') > 0
AND NOT EXISTS (SELECT 1 FROM composant_product_slots cps WHERE cps."composantid" = c.id)
AND (NULLIF(prod->'definition'->>'typeProductId', '') IS NULL OR EXISTS (SELECT 1 FROM model_types mt WHERE mt.id = prod->'definition'->>'typeProductId'))
AND (NULLIF(prod->>'selectedProductId', '') IS NULL OR EXISTS (SELECT 1 FROM products p WHERE p.id = prod->>'selectedProductId'))
SQL);
}
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE IF EXISTS composant_product_slots');
$this->addSql('DROP TABLE IF EXISTS composant_subcomponent_slots');
$this->addSql('DROP TABLE IF EXISTS composant_piece_slots');
}
}

View File

@@ -109,6 +109,15 @@ class Piece
#[Groups(['piece:read'])]
private Collection $customFieldValues;
/**
* @var Collection<int, Product>
*/
#[ORM\ManyToMany(targetEntity: Product::class, inversedBy: 'linkedPieces')]
#[ORM\JoinTable(name: 'piece_products')]
#[ORM\JoinColumn(name: 'piece_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
#[ORM\InverseJoinColumn(name: 'product_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
private Collection $products;
/**
* @var Collection<int, MachinePieceLink>
*/
@@ -130,6 +139,7 @@ class Piece
$this->constructeurs = new ArrayCollection();
$this->documents = new ArrayCollection();
$this->customFieldValues = new ArrayCollection();
$this->products = new ArrayCollection();
$this->machineLinks = new ArrayCollection();
}
@@ -298,4 +308,28 @@ class Piece
{
return $this->customFieldValues;
}
/**
* @return Collection<int, Product>
*/
public function getProducts(): Collection
{
return $this->products;
}
public function addProduct(Product $product): static
{
if (!$this->products->contains($product)) {
$this->products->add($product);
}
return $this;
}
public function removeProduct(Product $product): static
{
$this->products->removeElement($product);
return $this;
}
}

View File

@@ -106,6 +106,12 @@ class Product
#[ORM\OneToMany(mappedBy: 'product', targetEntity: Composant::class)]
private Collection $composants;
/**
* @var Collection<int, Piece>
*/
#[ORM\ManyToMany(targetEntity: Piece::class, mappedBy: 'products')]
private Collection $linkedPieces;
/**
* @var Collection<int, MachineProductLink>
*/
@@ -129,6 +135,7 @@ class Product
$this->customFieldValues = new ArrayCollection();
$this->pieces = new ArrayCollection();
$this->composants = new ArrayCollection();
$this->linkedPieces = new ArrayCollection();
$this->machineLinks = new ArrayCollection();
}
@@ -219,4 +226,12 @@ class Product
{
return $this->customFieldValues;
}
/**
* @return Collection<int, Piece>
*/
public function getLinkedPieces(): Collection
{
return $this->linkedPieces;
}
}