36 KiB
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↔constructeursrc/Entity/PieceConstructeurLink.php— pivot entity piece↔constructeursrc/Entity/ComposantConstructeurLink.php— pivot entity composant↔constructeursrc/Entity/ProductConstructeurLink.php— pivot entity product↔constructeursrc/Repository/MachineConstructeurLinkRepository.phpsrc/Repository/PieceConstructeurLinkRepository.phpsrc/Repository/ComposantConstructeurLinkRepository.phpsrc/Repository/ProductConstructeurLinkRepository.phpmigrations/VersionXXX.php— migration (auto-generated, then adjusted)
Modified files
src/Entity/Machine.php— replace ManyToMany with OneToMany to Linksrc/Entity/Piece.php— replace ManyToMany with OneToMany to Linksrc/Entity/Composant.php— replace ManyToMany with OneToMany to Linksrc/Entity/Product.php— replace ManyToMany with OneToMany to Linksrc/Entity/Constructeur.php— replace 4 ManyToMany with 4 OneToMany to Linkssrc/Controller/MachineStructureController.php— update clone + normalizesrc/Service/EntityVersionService.php— update snapshot/restore/diff for constructeur linkssrc/Service/ModelTypeCategoryConversionService.php— update table names in raw SQLsrc/EventSubscriber/AbstractAuditSubscriber.php— remove ManyToMany collection tracking for constructeurssrc/EventSubscriber/MachineAuditSubscriber.php— update snapshotEntitysrc/EventSubscriber/PieceAuditSubscriber.php— update snapshotEntitysrc/EventSubscriber/ComposantAuditSubscriber.php— update snapshotEntitysrc/EventSubscriber/ProductAuditSubscriber.php— update snapshotEntitysrc/Mcp/Tool/Machine/CreateMachineTool.php— use Links instead of addConstructeursrc/Mcp/Tool/Machine/UpdateMachineTool.php— use Linkssrc/Mcp/Tool/Machine/GetMachineTool.php— read from Linkssrc/Mcp/Tool/Piece/CreatePieceTool.php— use Linkssrc/Mcp/Tool/Piece/UpdatePieceTool.php— use Linkssrc/Mcp/Tool/Piece/GetPieceTool.php— read from Linkssrc/Mcp/Tool/Composant/CreateComposantTool.php— use Linkssrc/Mcp/Tool/Composant/UpdateComposantTool.php— use Linkssrc/Mcp/Tool/Composant/GetComposantTool.php— read from Linkssrc/Mcp/Tool/Product/CreateProductTool.php— use Linkssrc/Mcp/Tool/Product/UpdateProductTool.php— use Linkssrc/Mcp/Tool/Product/GetProductTool.php— read from Linkstests/AbstractApiTestCase.php— addcreateConstructeurLink()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
MachineConstructeurLinkentity
<?php
declare(strict_types=1);
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use App\Entity\Trait\CuidEntityTrait;
use App\Repository\MachineConstructeurLinkRepository;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: MachineConstructeurLinkRepository::class)]
#[ORM\Table(name: 'machine_constructeur_links')]
#[ORM\UniqueConstraint(name: 'uniq_machine_constructeur', columns: ['machineid', 'constructeurid'])]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
description: 'Liaisons machine–fournisseur. Chaque liaison peut porter une référence fournisseur spécifique.',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
new Post(security: "is_granted('ROLE_GESTIONNAIRE')"),
new Put(security: "is_granted('ROLE_GESTIONNAIRE')"),
new Patch(security: "is_granted('ROLE_GESTIONNAIRE')"),
new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
]
)]
class MachineConstructeurLink
{
use CuidEntityTrait;
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'constructeurLinks')]
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private Machine $machine;
#[ORM\ManyToOne(targetEntity: Constructeur::class, inversedBy: 'machineLinks')]
#[ORM\JoinColumn(name: 'constructeurId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private Constructeur $constructeur;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true, name: 'supplierReference')]
private ?string $supplierReference = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private DateTimeImmutable $updatedAt;
public function __construct()
{
$this->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
PieceConstructeurLinkentity
Same structure as MachineConstructeurLink, replacing:
-
Class name:
PieceConstructeurLink -
Repository:
PieceConstructeurLinkRepository -
Table:
piece_constructeur_links -
Unique constraint:
uniq_piece_constructeuron['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
ComposantConstructeurLinkentity
Same structure, replacing:
-
Class name:
ComposantConstructeurLink -
Repository:
ComposantConstructeurLinkRepository -
Table:
composant_constructeur_links -
Unique constraint:
uniq_composant_constructeuron['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
ProductConstructeurLinkentity
Same structure, replacing:
-
Class name:
ProductConstructeurLink -
Repository:
ProductConstructeurLinkRepository -
Table:
product_constructeur_links -
Unique constraint:
uniq_product_constructeuron['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
declare(strict_types=1);
namespace App\Repository;
use App\Entity\MachineConstructeurLink;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<MachineConstructeurLink>
*/
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
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:
/**
* @var Collection<int, Constructeur>
*/
#[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:
/**
* @var Collection<int, MachineConstructeurLink>
*/
#[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:
/**
* @return Collection<int, MachineConstructeurLink>
*/
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:
/**
* @var Collection<int, PieceConstructeurLink>
*/
#[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:
/**
* @var Collection<int, ComposantConstructeurLink>
*/
#[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:
/**
* @var Collection<int, ProductConstructeurLink>
*/
#[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:
/**
* @var Collection<int, MachineConstructeurLink>
*/
#[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: MachineConstructeurLink::class, cascade: ['remove'])]
private Collection $machineLinks;
/**
* @var Collection<int, ComposantConstructeurLink>
*/
#[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: ComposantConstructeurLink::class, cascade: ['remove'])]
private Collection $composantLinks;
/**
* @var Collection<int, PieceConstructeurLink>
*/
#[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: PieceConstructeurLink::class, cascade: ['remove'])]
private Collection $pieceLinks;
/**
* @var Collection<int, ProductConstructeurLink>
*/
#[ORM\OneToMany(mappedBy: 'constructeur', targetEntity: ProductConstructeurLink::class, cascade: ['remove'])]
private Collection $productLinks;
In __construct(), replace the 4 ArrayCollection inits with:
$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
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:
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:
- Create the 4 new tables (keep the auto-generated CREATE TABLE statements)
- Add data migration — insert from old join tables into new Link tables:
-- 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";
- 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
docker exec -u www-data php-inventory-apache php bin/console doctrine:migrations:migrate
Expected: migration passes without errors.
- Step 4: Verify data
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
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:
// Copy constructeurs
foreach ($source->getConstructeurs() as $constructeur) {
$newMachine->getConstructeurs()->add($constructeur);
}
With:
// 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:
'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()),
With:
'constructeurs' => $this->normalizeConstructeurLinks($machine->getConstructeurLinks()),
- Step 3: Update normalizeConstructeurs → normalizeConstructeurLinks (lines ~814-824)
Replace the method:
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:
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
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:
'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
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:
$currentConstructeurIds = [];
if (method_exists($entity, 'getConstructeurs')) {
foreach ($entity->getConstructeurs() as $c) {
$currentConstructeurIds[] = $c->getId();
}
}
With:
$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:
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
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:
// 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)
// 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
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):
foreach ($constructeurIds as $cId) {
$c = $this->constructeurs->find($cId);
if (!$c) {
$this->mcpError('not_found', "Constructeur not found: {$cId}");
}
$machine->addConstructeur($c);
}
With:
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):
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:
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):
$constructeurs = [];
foreach ($machine->getConstructeurs() as $c) {
$constructeurs[] = [
'id' => $c->getId(),
'name' => $c->getName(),
];
}
With:
$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
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:
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:
$machine->addConstructeur($constructeur);
$this->getEntityManager()->flush();
With:
$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
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
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
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:
make fixtures-dump
- Step 2: Commit
git add fixtures/
git commit -m "chore : update fixtures dump after ConstructeurLink migration"