# Supplier References Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Allow storing a supplier reference (`supplierReference`) per (item, constructeur) pair by converting ManyToMany join tables to proper Link entities. **Architecture:** Replace the 4 simple join tables (`_MachineConstructeurs`, `_PieceConstructeurs`, `_ComposantConstructeurs`, `_ProductConstructeurs`) with Doctrine entities following the existing `*Link` pattern. Each entity holds `supplierReference` (string, nullable). Update all consumers: audit subscribers, version service, MCP tools, structure controller, category conversion service. **Tech Stack:** Symfony 8 / Doctrine ORM / API Platform / PHP 8.4 / PostgreSQL --- ## File Structure ### New files - `src/Entity/MachineConstructeurLink.php` — pivot entity machine↔constructeur - `src/Entity/PieceConstructeurLink.php` — pivot entity piece↔constructeur - `src/Entity/ComposantConstructeurLink.php` — pivot entity composant↔constructeur - `src/Entity/ProductConstructeurLink.php` — pivot entity product↔constructeur - `src/Repository/MachineConstructeurLinkRepository.php` - `src/Repository/PieceConstructeurLinkRepository.php` - `src/Repository/ComposantConstructeurLinkRepository.php` - `src/Repository/ProductConstructeurLinkRepository.php` - `migrations/VersionXXX.php` — migration (auto-generated, then adjusted) ### Modified files - `src/Entity/Machine.php` — replace ManyToMany with OneToMany to Link - `src/Entity/Piece.php` — replace ManyToMany with OneToMany to Link - `src/Entity/Composant.php` — replace ManyToMany with OneToMany to Link - `src/Entity/Product.php` — replace ManyToMany with OneToMany to Link - `src/Entity/Constructeur.php` — replace 4 ManyToMany with 4 OneToMany to Links - `src/Controller/MachineStructureController.php` — update clone + normalize - `src/Service/EntityVersionService.php` — update snapshot/restore/diff for constructeur links - `src/Service/ModelTypeCategoryConversionService.php` — update table names in raw SQL - `src/EventSubscriber/AbstractAuditSubscriber.php` — remove ManyToMany collection tracking for constructeurs - `src/EventSubscriber/MachineAuditSubscriber.php` — update snapshotEntity - `src/EventSubscriber/PieceAuditSubscriber.php` — update snapshotEntity - `src/EventSubscriber/ComposantAuditSubscriber.php` — update snapshotEntity - `src/EventSubscriber/ProductAuditSubscriber.php` — update snapshotEntity - `src/Mcp/Tool/Machine/CreateMachineTool.php` — use Links instead of addConstructeur - `src/Mcp/Tool/Machine/UpdateMachineTool.php` — use Links - `src/Mcp/Tool/Machine/GetMachineTool.php` — read from Links - `src/Mcp/Tool/Piece/CreatePieceTool.php` — use Links - `src/Mcp/Tool/Piece/UpdatePieceTool.php` — use Links - `src/Mcp/Tool/Piece/GetPieceTool.php` — read from Links - `src/Mcp/Tool/Composant/CreateComposantTool.php` — use Links - `src/Mcp/Tool/Composant/UpdateComposantTool.php` — use Links - `src/Mcp/Tool/Composant/GetComposantTool.php` — read from Links - `src/Mcp/Tool/Product/CreateProductTool.php` — use Links - `src/Mcp/Tool/Product/UpdateProductTool.php` — use Links - `src/Mcp/Tool/Product/GetProductTool.php` — read from Links - `tests/AbstractApiTestCase.php` — add `createConstructeurLink()` factory helpers --- ### Task 1: Create the 4 Link entities + repositories **Files:** - Create: `src/Entity/MachineConstructeurLink.php` - Create: `src/Entity/PieceConstructeurLink.php` - Create: `src/Entity/ComposantConstructeurLink.php` - Create: `src/Entity/ProductConstructeurLink.php` - Create: `src/Repository/MachineConstructeurLinkRepository.php` - Create: `src/Repository/PieceConstructeurLinkRepository.php` - Create: `src/Repository/ComposantConstructeurLinkRepository.php` - Create: `src/Repository/ProductConstructeurLinkRepository.php` - [ ] **Step 1: Create `MachineConstructeurLink` entity** ```php createdAt = new DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable(); } public function getMachine(): Machine { return $this->machine; } public function setMachine(Machine $machine): static { $this->machine = $machine; return $this; } public function getConstructeur(): Constructeur { return $this->constructeur; } public function setConstructeur(Constructeur $constructeur): static { $this->constructeur = $constructeur; return $this; } public function getSupplierReference(): ?string { return $this->supplierReference; } public function setSupplierReference(?string $supplierReference): static { $this->supplierReference = $supplierReference; return $this; } } ``` - [ ] **Step 2: Create `PieceConstructeurLink` entity** Same structure as `MachineConstructeurLink`, replacing: - Class name: `PieceConstructeurLink` - Repository: `PieceConstructeurLinkRepository` - Table: `piece_constructeur_links` - Unique constraint: `uniq_piece_constructeur` on `['pieceid', 'constructeurid']` - ManyToOne: `Piece` (inversedBy: `constructeurLinks`, joinColumn: `pieceId`) - Constructeur inversedBy: `pieceLinks` - Description: `'Liaisons pièce–fournisseur. Chaque liaison peut porter une référence fournisseur spécifique.'` - Property + getter/setter: `piece` / `getPiece()` / `setPiece(Piece $piece)` - [ ] **Step 3: Create `ComposantConstructeurLink` entity** Same structure, replacing: - Class name: `ComposantConstructeurLink` - Repository: `ComposantConstructeurLinkRepository` - Table: `composant_constructeur_links` - Unique constraint: `uniq_composant_constructeur` on `['composantid', 'constructeurid']` - ManyToOne: `Composant` (inversedBy: `constructeurLinks`, joinColumn: `composantId`) - Constructeur inversedBy: `composantLinks` - Description: `'Liaisons composant–fournisseur. Chaque liaison peut porter une référence fournisseur spécifique.'` - Property + getter/setter: `composant` / `getComposant()` / `setComposant(Composant $composant)` - [ ] **Step 4: Create `ProductConstructeurLink` entity** Same structure, replacing: - Class name: `ProductConstructeurLink` - Repository: `ProductConstructeurLinkRepository` - Table: `product_constructeur_links` - Unique constraint: `uniq_product_constructeur` on `['productid', 'constructeurid']` - ManyToOne: `Product` (inversedBy: `constructeurLinks`, joinColumn: `productId`) - Constructeur inversedBy: `productLinks` - Description: `'Liaisons produit–fournisseur. Chaque liaison peut porter une référence fournisseur spécifique.'` - Property + getter/setter: `product` / `getProduct()` / `setProduct(Product $product)` - [ ] **Step 5: Create the 4 repositories** Each repository follows the `MachinePieceLinkRepository` pattern: ```php */ class MachineConstructeurLinkRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, MachineConstructeurLink::class); } } ``` Repeat for `PieceConstructeurLinkRepository`, `ComposantConstructeurLinkRepository`, `ProductConstructeurLinkRepository` (changing entity class). - [ ] **Step 6: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 7: Commit** ```bash git add src/Entity/*ConstructeurLink.php src/Repository/*ConstructeurLinkRepository.php git commit -m "feat(constructeur) : add 4 ConstructeurLink pivot entities with supplierReference" ``` --- ### Task 2: Update existing entities (Machine, Piece, Composant, Product, Constructeur) **Files:** - Modify: `src/Entity/Machine.php` - Modify: `src/Entity/Piece.php` - Modify: `src/Entity/Composant.php` - Modify: `src/Entity/Product.php` - Modify: `src/Entity/Constructeur.php` - [ ] **Step 1: Update `Machine.php`** Replace the ManyToMany property and methods with OneToMany: Remove: ```php /** * @var Collection */ #[ORM\ManyToMany(targetEntity: Constructeur::class, inversedBy: 'machines')] #[ORM\JoinTable( name: '_MachineConstructeurs', joinColumns: [new ORM\JoinColumn(name: 'A', referencedColumnName: 'id', onDelete: 'CASCADE')], inverseJoinColumns: [new ORM\InverseJoinColumn(name: 'B', referencedColumnName: 'id', onDelete: 'CASCADE')] )] private Collection $constructeurs; ``` Add: ```php /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'machine', targetEntity: MachineConstructeurLink::class, cascade: ['remove'])] private Collection $constructeurLinks; ``` In `__construct()`, replace `$this->constructeurs = new ArrayCollection();` with `$this->constructeurLinks = new ArrayCollection();` Replace methods `getConstructeurs()`, `addConstructeur()`, `removeConstructeur()` with: ```php /** * @return Collection */ public function getConstructeurLinks(): Collection { return $this->constructeurLinks; } ``` - [ ] **Step 2: Update `Piece.php`** Same pattern as Machine. Replace ManyToMany `constructeurs` property (with `#[Groups(['piece:read'])]`) with: ```php /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'piece', targetEntity: PieceConstructeurLink::class, cascade: ['remove'])] #[Groups(['piece:read'])] private Collection $constructeurLinks; ``` In `__construct()`: `$this->constructeurLinks = new ArrayCollection();` Replace getter/add/remove with `getConstructeurLinks(): Collection`. - [ ] **Step 3: Update `Composant.php`** Same pattern. Replace ManyToMany (with `#[Groups(['composant:read'])]`) with: ```php /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'composant', targetEntity: ComposantConstructeurLink::class, cascade: ['remove'])] #[Groups(['composant:read'])] private Collection $constructeurLinks; ``` - [ ] **Step 4: Update `Product.php`** Same pattern. Replace ManyToMany (with `#[Groups(['product:read'])]`) with: ```php /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'product', targetEntity: ProductConstructeurLink::class, cascade: ['remove'])] #[Groups(['product:read'])] private Collection $constructeurLinks; ``` - [ ] **Step 5: Update `Constructeur.php`** Replace the 4 ManyToMany properties and their initializations in `__construct()`: Remove all 4 ManyToMany properties (`machines`, `composants`, `pieces`, `products`) and replace with: ```php /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: MachineConstructeurLink::class, cascade: ['remove'])] private Collection $machineLinks; /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: ComposantConstructeurLink::class, cascade: ['remove'])] private Collection $composantLinks; /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: PieceConstructeurLink::class, cascade: ['remove'])] private Collection $pieceLinks; /** * @var Collection */ #[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: ProductConstructeurLink::class, cascade: ['remove'])] private Collection $productLinks; ``` In `__construct()`, replace the 4 `ArrayCollection` inits with: ```php $this->machineLinks = new ArrayCollection(); $this->composantLinks = new ArrayCollection(); $this->pieceLinks = new ArrayCollection(); $this->productLinks = new ArrayCollection(); ``` - [ ] **Step 6: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 7: Commit** ```bash git add src/Entity/Machine.php src/Entity/Piece.php src/Entity/Composant.php src/Entity/Product.php src/Entity/Constructeur.php git commit -m "refactor(constructeur) : replace ManyToMany with OneToMany to ConstructeurLink entities" ``` --- ### Task 3: Generate and adjust migration **Files:** - Create: `migrations/VersionXXX.php` (auto-generated) - [ ] **Step 1: Generate migration diff** Run inside docker: ```bash docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:diff ``` - [ ] **Step 2: Edit the generated migration** The auto-generated migration will try to drop the old tables and create new ones. We need to add data migration in between. Edit the migration `up()` method to follow this order: 1. Create the 4 new tables (keep the auto-generated CREATE TABLE statements) 2. **Add data migration** — insert from old join tables into new Link tables: ```sql -- Machine constructeur links INSERT INTO machine_constructeur_links (id, machineid, constructeurid, supplierreference, createdat, updatedat) SELECT 'cl' || encode(gen_random_bytes(12), 'hex'), a, b, NULL, NOW(), NOW() FROM "_machineconstructeurs"; -- Piece constructeur links INSERT INTO piece_constructeur_links (id, pieceid, constructeurid, supplierreference, createdat, updatedat) SELECT 'cl' || encode(gen_random_bytes(12), 'hex'), a, b, NULL, NOW(), NOW() FROM "_piececonstructeurs"; -- Composant constructeur links INSERT INTO composant_constructeur_links (id, composantid, constructeurid, supplierreference, createdat, updatedat) SELECT 'cl' || encode(gen_random_bytes(12), 'hex'), a, b, NULL, NOW(), NOW() FROM "_composantconstructeurs"; -- Product constructeur links INSERT INTO product_constructeur_links (id, productid, constructeurid, supplierreference, createdat, updatedat) SELECT 'cl' || encode(gen_random_bytes(12), 'hex'), a, b, NULL, NOW(), NOW() FROM "_productconstructeurs"; ``` 3. Drop the 4 old join tables (keep the auto-generated DROP TABLE statements) For `down()`: reverse — recreate old tables, migrate data back (without supplierReference), drop new tables. - [ ] **Step 3: Run the migration** ```bash docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate ``` Expected: migration passes without errors. - [ ] **Step 4: Verify data** ```bash docker exec -u www-data php-inventory-apache php bin/console doctrine:query:sql "SELECT COUNT(*) FROM machine_constructeur_links" docker exec -u www-data php-inventory-apache php bin/console doctrine:query:sql "SELECT COUNT(*) FROM piece_constructeur_links" docker exec -u www-data php-inventory-apache php bin/console doctrine:query:sql "SELECT COUNT(*) FROM composant_constructeur_links" docker exec -u www-data php-inventory-apache php bin/console doctrine:query:sql "SELECT COUNT(*) FROM product_constructeur_links" ``` Expected: counts match the old join tables. - [ ] **Step 5: Commit** ```bash git add migrations/ git commit -m "feat(constructeur) : add migration for ConstructeurLink tables with data migration" ``` --- ### Task 4: Update MachineStructureController **Files:** - Modify: `src/Controller/MachineStructureController.php:141-144` (clone) - Modify: `src/Controller/MachineStructureController.php:589` (normalize in structure response) - Modify: `src/Controller/MachineStructureController.php:814-824` (normalizeConstructeurs) - [ ] **Step 1: Update clone logic (lines ~141-144)** Replace: ```php // Copy constructeurs foreach ($source->getConstructeurs() as $constructeur) { $newMachine->getConstructeurs()->add($constructeur); } ``` With: ```php // Copy constructeur links foreach ($source->getConstructeurLinks() as $link) { $newLink = new MachineConstructeurLink(); $newLink->setMachine($newMachine); $newLink->setConstructeur($link->getConstructeur()); $newLink->setSupplierReference($link->getSupplierReference()); $this->entityManager->persist($newLink); } ``` Add `use App\Entity\MachineConstructeurLink;` at top of file. - [ ] **Step 2: Update structure response (line ~589)** Replace: ```php 'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()), ``` With: ```php 'constructeurs' => $this->normalizeConstructeurLinks($machine->getConstructeurLinks()), ``` - [ ] **Step 3: Update normalizeConstructeurs → normalizeConstructeurLinks (lines ~814-824)** Replace the method: ```php private function normalizeConstructeurs(Collection $constructeurs): array { $items = []; foreach ($constructeurs as $constructeur) { $items[] = [ 'id' => $constructeur->getId(), 'name' => $constructeur->getName(), 'email' => $constructeur->getEmail(), 'phone' => $constructeur->getPhone(), ]; } return $items; } ``` With: ```php private function normalizeConstructeurLinks(Collection $constructeurLinks): array { $items = []; foreach ($constructeurLinks as $link) { $items[] = [ 'id' => $link->getId(), 'constructeur' => [ 'id' => $link->getConstructeur()->getId(), 'name' => $link->getConstructeur()->getName(), 'email' => $link->getConstructeur()->getEmail(), 'phone' => $link->getConstructeur()->getPhone(), ], 'supplierReference' => $link->getSupplierReference(), ]; } return $items; } ``` - [ ] **Step 4: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 5: Commit** ```bash git add src/Controller/MachineStructureController.php git commit -m "refactor(machines) : update structure controller to use ConstructeurLinks" ``` --- ### Task 5: Update audit subscribers **Files:** - Modify: `src/EventSubscriber/AbstractAuditSubscriber.php:437-458` - Modify: `src/EventSubscriber/MachineAuditSubscriber.php:107` - Modify: `src/EventSubscriber/PieceAuditSubscriber.php:66` - Modify: `src/EventSubscriber/ComposantAuditSubscriber.php:89` - Modify: `src/EventSubscriber/ProductAuditSubscriber.php:53` - [ ] **Step 1: Update `AbstractAuditSubscriber.php`** The ManyToMany collection tracking for `constructeurs` is no longer needed since constructeur links are now separate entities tracked by their own lifecycle. In `collectCollectionUpdate()` (line ~438), the check for `'constructeurs' !== $fieldName` can be removed or the method can be simplified. Since `constructeurLinks` is a OneToMany (inverse side), Doctrine won't fire collection update events for it — the Link entities themselves are tracked as inserts/updates/deletes. If `hasCollectionTracking()` was ONLY used for constructeurs, set it to return `false` in all subscribers and remove the `collectCollectionUpdate` method body. If other collections are tracked, keep the infrastructure but remove the constructeurs-specific logic. Check: search for any other `fieldName` checks in `collectCollectionUpdate`. If `'constructeurs'` is the only one, simplify by removing the collection tracking entirely. - [ ] **Step 2: Update snapshot methods in all 4 audit subscribers** Replace `'constructeurIds' => $this->normalizeCollection($entity->getConstructeurs())` with: ```php 'constructeurIds' => array_map( fn ($link) => [ 'id' => $link->getConstructeur()->getId(), 'name' => $link->getConstructeur()->getName(), 'supplierReference' => $link->getSupplierReference(), ], $entity->getConstructeurLinks()->toArray(), ), ``` Apply this change in: - `MachineAuditSubscriber.php` (line ~107) - `PieceAuditSubscriber.php` (line ~66) - `ComposantAuditSubscriber.php` (line ~89) - `ProductAuditSubscriber.php` (line ~53) - [ ] **Step 3: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 4: Commit** ```bash git add src/EventSubscriber/ git commit -m "refactor(audit) : update audit subscribers to use ConstructeurLinks" ``` --- ### Task 6: Update EntityVersionService **Files:** - Modify: `src/Service/EntityVersionService.php:255-281` (checkIntegrity) - Modify: `src/Service/EntityVersionService.php:395-410` (buildRestoreDiff) - Modify: `src/Service/EntityVersionService.php:544-575` (restoreConstructeurs) - [ ] **Step 1: Update `checkIntegrity()` (lines ~255-281)** No structural change needed — the integrity check already works with `constructeurIds` from the snapshot and checks if the Constructeur entities still exist. The format is compatible. - [ ] **Step 2: Update `buildRestoreDiff()` (lines ~395-410)** Replace: ```php $currentConstructeurIds = []; if (method_exists($entity, 'getConstructeurs')) { foreach ($entity->getConstructeurs() as $c) { $currentConstructeurIds[] = $c->getId(); } } ``` With: ```php $currentConstructeurIds = []; if (method_exists($entity, 'getConstructeurLinks')) { foreach ($entity->getConstructeurLinks() as $link) { $currentConstructeurIds[] = $link->getConstructeur()->getId(); } } ``` - [ ] **Step 3: Update `restoreConstructeurs()` (lines ~544-575)** Replace the entire method. Instead of add/remove on a ManyToMany collection, we now create/delete Link entities: ```php private function restoreConstructeurs(object $entity, array $snapshot): void { if (!method_exists($entity, 'getConstructeurLinks')) { return; } $targetIds = []; $targetRefs = []; foreach ($snapshot['constructeurIds'] ?? [] as $entry) { $id = is_array($entry) ? ($entry['id'] ?? null) : $entry; if ($id) { $targetIds[] = $id; $targetRefs[$id] = is_array($entry) ? ($entry['supplierReference'] ?? null) : null; } } // Remove current links not in snapshot foreach ($entity->getConstructeurLinks()->toArray() as $link) { $cId = $link->getConstructeur()->getId(); if (!in_array($cId, $targetIds, true)) { $this->em->remove($link); } else { // Update supplierReference if present in snapshot if (isset($targetRefs[$cId])) { $link->setSupplierReference($targetRefs[$cId]); } } } // Add missing constructeur links from snapshot $currentIds = array_map( fn ($link) => $link->getConstructeur()->getId(), $entity->getConstructeurLinks()->toArray(), ); foreach ($targetIds as $id) { if (!in_array($id, $currentIds, true)) { $constructeur = $this->constructeurs->find($id); if (null !== $constructeur) { $linkClass = $this->getConstructeurLinkClass($entity); $link = new $linkClass(); $link->{'set' . $this->getEntityShortName($entity)}($entity); $link->setConstructeur($constructeur); $link->setSupplierReference($targetRefs[$id] ?? null); $this->em->persist($link); } } } } private function getConstructeurLinkClass(object $entity): string { return match (true) { $entity instanceof Machine => MachineConstructeurLink::class, $entity instanceof Piece => PieceConstructeurLink::class, $entity instanceof Composant => ComposantConstructeurLink::class, $entity instanceof Product => ProductConstructeurLink::class, default => throw new \LogicException('Unsupported entity type'), }; } private function getEntityShortName(object $entity): string { return match (true) { $entity instanceof Machine => 'Machine', $entity instanceof Piece => 'Piece', $entity instanceof Composant => 'Composant', $entity instanceof Product => 'Product', default => throw new \LogicException('Unsupported entity type'), }; } ``` Add the necessary use statements at the top. - [ ] **Step 4: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 5: Commit** ```bash git add src/Service/EntityVersionService.php git commit -m "refactor(versioning) : update EntityVersionService to use ConstructeurLinks" ``` --- ### Task 7: Update ModelTypeCategoryConversionService **Files:** - Modify: `src/Service/ModelTypeCategoryConversionService.php:287-299` (piece→composant) - Modify: `src/Service/ModelTypeCategoryConversionService.php:355-367` (composant→piece) - [ ] **Step 1: Update piece→composant conversion (lines ~287-299)** Replace the raw SQL that references `_piececonstructeurs` and `_composantconstructeurs`: ```php // 2. Transfer constructeur link associations $this->connection->executeStatement( 'INSERT INTO composant_constructeur_links (id, composantid, constructeurid, supplierreference, createdat, updatedat) SELECT \'cl\' || encode(gen_random_bytes(12), \'hex\'), pcl.pieceid, pcl.constructeurid, pcl.supplierreference, pcl.createdat, pcl.updatedat FROM piece_constructeur_links pcl WHERE pcl.pieceid IN (SELECT id FROM composants WHERE typecomposantid = :id)', ['id' => $modelTypeId], ); $this->connection->executeStatement( 'DELETE FROM piece_constructeur_links WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)', ['id' => $modelTypeId], ); ``` - [ ] **Step 2: Update composant→piece conversion (lines ~355-367)** ```php // 2. Transfer constructeur link associations $this->connection->executeStatement( 'INSERT INTO piece_constructeur_links (id, pieceid, constructeurid, supplierreference, createdat, updatedat) SELECT \'cl\' || encode(gen_random_bytes(12), \'hex\'), ccl.composantid, ccl.constructeurid, ccl.supplierreference, ccl.createdat, ccl.updatedat FROM composant_constructeur_links ccl WHERE ccl.composantid IN (SELECT id FROM pieces WHERE typepieceid = :id)', ['id' => $modelTypeId], ); $this->connection->executeStatement( 'DELETE FROM composant_constructeur_links WHERE composantid IN (SELECT id FROM composants WHERE typecomposantid = :id)', ['id' => $modelTypeId], ); ``` - [ ] **Step 3: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 4: Commit** ```bash git add src/Service/ModelTypeCategoryConversionService.php git commit -m "refactor(conversion) : update category conversion to use ConstructeurLink tables" ``` --- ### Task 8: Update MCP Tools **Files:** - Modify: `src/Mcp/Tool/Machine/CreateMachineTool.php` - Modify: `src/Mcp/Tool/Machine/UpdateMachineTool.php` - Modify: `src/Mcp/Tool/Machine/GetMachineTool.php` - Modify: `src/Mcp/Tool/Piece/CreatePieceTool.php` - Modify: `src/Mcp/Tool/Piece/UpdatePieceTool.php` - Modify: `src/Mcp/Tool/Piece/GetPieceTool.php` - Modify: `src/Mcp/Tool/Composant/CreateComposantTool.php` - Modify: `src/Mcp/Tool/Composant/UpdateComposantTool.php` - Modify: `src/Mcp/Tool/Composant/GetComposantTool.php` - Modify: `src/Mcp/Tool/Product/CreateProductTool.php` - Modify: `src/Mcp/Tool/Product/UpdateProductTool.php` - Modify: `src/Mcp/Tool/Product/GetProductTool.php` - [ ] **Step 1: Update `CreateMachineTool.php`** Replace the constructeur-adding loop (lines ~59-65): ```php foreach ($constructeurIds as $cId) { $c = $this->constructeurs->find($cId); if (!$c) { $this->mcpError('not_found', "Constructeur not found: {$cId}"); } $machine->addConstructeur($c); } ``` With: ```php foreach ($constructeurIds as $cId) { $c = $this->constructeurs->find($cId); if (!$c) { $this->mcpError('not_found', "Constructeur not found: {$cId}"); } $link = new MachineConstructeurLink(); $link->setMachine($machine); $link->setConstructeur($c); $this->em->persist($link); } ``` Add `use App\Entity\MachineConstructeurLink;`. - [ ] **Step 2: Update `UpdateMachineTool.php`** Replace (lines ~69-80): ```php if (null !== $constructeurIds) { foreach ($machine->getConstructeurs()->toArray() as $existing) { $machine->removeConstructeur($existing); } foreach ($constructeurIds as $cId) { $c = $this->constructeurs->find($cId); if (!$c) { $this->mcpError('not_found', "Constructeur not found: {$cId}"); } $machine->addConstructeur($c); } } ``` With: ```php if (null !== $constructeurIds) { foreach ($machine->getConstructeurLinks()->toArray() as $existing) { $this->em->remove($existing); } foreach ($constructeurIds as $cId) { $c = $this->constructeurs->find($cId); if (!$c) { $this->mcpError('not_found', "Constructeur not found: {$cId}"); } $link = new MachineConstructeurLink(); $link->setMachine($machine); $link->setConstructeur($c); $this->em->persist($link); } } ``` Add `use App\Entity\MachineConstructeurLink;`. - [ ] **Step 3: Update `GetMachineTool.php`** Replace (lines ~32-38): ```php $constructeurs = []; foreach ($machine->getConstructeurs() as $c) { $constructeurs[] = [ 'id' => $c->getId(), 'name' => $c->getName(), ]; } ``` With: ```php $constructeurs = []; foreach ($machine->getConstructeurLinks() as $link) { $constructeurs[] = [ 'id' => $link->getConstructeur()->getId(), 'name' => $link->getConstructeur()->getName(), 'supplierReference' => $link->getSupplierReference(), ]; } ``` - [ ] **Step 4: Repeat for Piece tools** Apply the same patterns to `CreatePieceTool.php`, `UpdatePieceTool.php`, `GetPieceTool.php` — replacing `Machine` references with `Piece` and using `PieceConstructeurLink`. - [ ] **Step 5: Repeat for Composant tools** Apply to `CreateComposantTool.php`, `UpdateComposantTool.php`, `GetComposantTool.php` — using `ComposantConstructeurLink`. - [ ] **Step 6: Repeat for Product tools** Apply to `CreateProductTool.php`, `UpdateProductTool.php`, `GetProductTool.php` — using `ProductConstructeurLink`. - [ ] **Step 7: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 8: Commit** ```bash git add src/Mcp/Tool/ git commit -m "refactor(mcp) : update MCP tools to use ConstructeurLinks" ``` --- ### Task 9: Update test helpers + fix existing tests **Files:** - Modify: `tests/AbstractApiTestCase.php` - Modify: `tests/Mcp/Tool/Machine/MachinesCrudToolTest.php` - Modify: `tests/Mcp/Tool/Piece/PiecesCrudToolTest.php` - Modify: `tests/Mcp/Tool/Composant/ComposantsCrudToolTest.php` - Modify: `tests/Mcp/Tool/Product/ProductsCrudToolTest.php` - [ ] **Step 1: Add factory helpers in `AbstractApiTestCase.php`** Add after the existing `createConstructeur()` method: ```php protected function createMachineConstructeurLink(Machine $machine, Constructeur $constructeur, ?string $supplierReference = null): MachineConstructeurLink { $link = new MachineConstructeurLink(); $link->setMachine($machine); $link->setConstructeur($constructeur); $link->setSupplierReference($supplierReference); $this->getEntityManager()->persist($link); $this->getEntityManager()->flush(); return $link; } protected function createPieceConstructeurLink(Piece $piece, Constructeur $constructeur, ?string $supplierReference = null): PieceConstructeurLink { $link = new PieceConstructeurLink(); $link->setPiece($piece); $link->setConstructeur($constructeur); $link->setSupplierReference($supplierReference); $this->getEntityManager()->persist($link); $this->getEntityManager()->flush(); return $link; } protected function createComposantConstructeurLink(Composant $composant, Constructeur $constructeur, ?string $supplierReference = null): ComposantConstructeurLink { $link = new ComposantConstructeurLink(); $link->setComposant($composant); $link->setConstructeur($constructeur); $link->setSupplierReference($supplierReference); $this->getEntityManager()->persist($link); $this->getEntityManager()->flush(); return $link; } protected function createProductConstructeurLink(Product $product, Constructeur $constructeur, ?string $supplierReference = null): ProductConstructeurLink { $link = new ProductConstructeurLink(); $link->setProduct($product); $link->setConstructeur($constructeur); $link->setSupplierReference($supplierReference); $this->getEntityManager()->persist($link); $this->getEntityManager()->flush(); return $link; } ``` Add the use statements for all 4 Link entities. - [ ] **Step 2: Update MCP test files** In each test file, replace `$entity->addConstructeur($constructeur)` with the factory helper. For example in `MachinesCrudToolTest.php` (line ~34): Replace: ```php $machine->addConstructeur($constructeur); $this->getEntityManager()->flush(); ``` With: ```php $this->createMachineConstructeurLink($machine, $constructeur); ``` Update assertions to match the new response format (with `supplierReference` field). Apply the same changes to `PiecesCrudToolTest.php`, `ComposantsCrudToolTest.php`, `ProductsCrudToolTest.php`. - [ ] **Step 3: Run php-cs-fixer** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 4: Commit** ```bash git add tests/ git commit -m "test(constructeur) : update test helpers and MCP tests for ConstructeurLinks" ``` --- ### Task 10: Run full test suite + fix any remaining issues - [ ] **Step 1: Run the full test suite** ```bash make test ``` Expected: all tests pass. If any test fails due to references to the old `getConstructeurs()` / `addConstructeur()` / `removeConstructeur()` methods, fix them to use `getConstructeurLinks()` + Link entities. - [ ] **Step 2: Fix any failures** Look for remaining usages of the old API and update them. - [ ] **Step 3: Run php-cs-fixer one final time** Run: `make php-cs-fixer-allow-risky` - [ ] **Step 4: Final commit if needed** ```bash git add -A git commit -m "fix(constructeur) : fix remaining references after ConstructeurLink migration" ``` --- ### Task 11: Update fixtures dump - [ ] **Step 1: Dump updated fixtures** If the dev database has been migrated, dump the new state: ```bash make fixtures-dump ``` - [ ] **Step 2: Commit** ```bash git add fixtures/ git commit -m "chore : update fixtures dump after ConstructeurLink migration" ```