1074 lines
36 KiB
Markdown
1074 lines
36 KiB
Markdown
# 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
|
||
<?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 `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
|
||
<?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**
|
||
|
||
```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<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:
|
||
```php
|
||
/**
|
||
* @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:
|
||
```php
|
||
/**
|
||
* @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:
|
||
```php
|
||
/**
|
||
* @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:
|
||
```php
|
||
/**
|
||
* @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:
|
||
```php
|
||
/**
|
||
* @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:
|
||
```php
|
||
/**
|
||
* @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:
|
||
```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"
|
||
```
|