diff --git a/migrations/Version20260312171810.php b/migrations/Version20260312171810.php new file mode 100644 index 0000000..b1b0a0c --- /dev/null +++ b/migrations/Version20260312171810.php @@ -0,0 +1,47 @@ +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'); + } +} diff --git a/migrations/Version20260312190000.php b/migrations/Version20260312190000.php new file mode 100644 index 0000000..56c2297 --- /dev/null +++ b/migrations/Version20260312190000.php @@ -0,0 +1,248 @@ +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'); + } +} diff --git a/src/Entity/Piece.php b/src/Entity/Piece.php index e8ca2dd..d73017d 100644 --- a/src/Entity/Piece.php +++ b/src/Entity/Piece.php @@ -109,6 +109,15 @@ class Piece #[Groups(['piece:read'])] private Collection $customFieldValues; + /** + * @var Collection + */ + #[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 */ @@ -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 + */ + 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; + } } diff --git a/src/Entity/Product.php b/src/Entity/Product.php index 5e0acc9..0699548 100644 --- a/src/Entity/Product.php +++ b/src/Entity/Product.php @@ -106,6 +106,12 @@ class Product #[ORM\OneToMany(mappedBy: 'product', targetEntity: Composant::class)] private Collection $composants; + /** + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Piece::class, mappedBy: 'products')] + private Collection $linkedPieces; + /** * @var Collection */ @@ -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 + */ + public function getLinkedPieces(): Collection + { + return $this->linkedPieces; + } }