Files
Inventory/src/Mcp/Tool/Machine/CloneMachineTool.php
Matthieu bd7259ed05 feat(mcp) : add Slots, Machine Links, Structure, and Clone tools
- list_slots + update_slots for composant/piece slots
- list/add/update/remove machine links (component, piece, product)
- get_machine_structure with full hierarchy
- clone_machine with all links and custom fields
- 52 MCP tests pass total

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-16 14:49:55 +01:00

224 lines
8.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Mcp\Tool\Machine;
use App\Entity\CustomField;
use App\Entity\CustomFieldValue;
use App\Entity\Machine;
use App\Entity\MachineComponentLink;
use App\Entity\MachinePieceLink;
use App\Entity\MachineProductLink;
use App\Entity\Site;
use App\Mcp\Tool\McpToolHelper;
use App\Repository\MachineComponentLinkRepository;
use App\Repository\MachinePieceLinkRepository;
use App\Repository\MachineProductLinkRepository;
use App\Repository\MachineRepository;
use Doctrine\ORM\EntityManagerInterface;
use Mcp\Capability\Attribute\McpTool;
use Symfony\Bundle\SecurityBundle\Security;
#[McpTool(
name: 'clone_machine',
description: 'Clone an existing machine with all its links (components, pieces, products), custom fields, and constructeurs. Requires ROLE_GESTIONNAIRE.',
)]
class CloneMachineTool
{
use McpToolHelper;
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly MachineRepository $machineRepository,
private readonly MachineComponentLinkRepository $machineComponentLinkRepository,
private readonly MachinePieceLinkRepository $machinePieceLinkRepository,
private readonly MachineProductLinkRepository $machineProductLinkRepository,
private readonly Security $security,
) {}
public function __invoke(
string $machineId,
string $name,
string $siteId,
string $reference = '',
): array {
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
$source = $this->machineRepository->find($machineId);
if (!$source instanceof Machine) {
$this->mcpError('not_found', "Machine not found: {$machineId}");
}
$site = $this->entityManager->getRepository(Site::class)->find($siteId);
if (!$site) {
$this->mcpError('not_found', "Site not found: {$siteId}");
}
// Create new machine
$newMachine = new Machine();
$newMachine->setName($name);
$newMachine->setSite($site);
if ('' !== $reference) {
$newMachine->setReference($reference);
}
$newMachine->setPrix($source->getPrix());
// Copy constructeurs
foreach ($source->getConstructeurs() as $constructeur) {
$newMachine->getConstructeurs()->add($constructeur);
}
$this->entityManager->persist($newMachine);
// Copy custom fields and values
$this->cloneCustomFields($source, $newMachine);
// Copy component links (preserving hierarchy with two-pass)
$componentLinkMap = $this->cloneComponentLinks($source, $newMachine);
// Copy piece links
$pieceLinkMap = $this->clonePieceLinks($source, $newMachine, $componentLinkMap);
// Copy product links
$this->cloneProductLinks($source, $newMachine, $componentLinkMap, $pieceLinkMap);
$this->entityManager->flush();
return $this->jsonResponse([
'id' => $newMachine->getId(),
'name' => $newMachine->getName(),
'reference' => $newMachine->getReference(),
'siteId' => $site->getId(),
'clonedFrom' => $source->getId(),
]);
}
private function cloneCustomFields(Machine $source, Machine $target): void
{
foreach ($source->getCustomFields() as $cf) {
$newCf = new CustomField();
$newCf->setName($cf->getName());
$newCf->setType($cf->getType());
$newCf->setRequired($cf->isRequired());
$newCf->setDefaultValue($cf->getDefaultValue());
$newCf->setOptions($cf->getOptions());
$newCf->setOrderIndex($cf->getOrderIndex());
$newCf->setMachine($target);
$this->entityManager->persist($newCf);
}
foreach ($source->getCustomFieldValues() as $cfv) {
$newValue = new CustomFieldValue();
$newValue->setMachine($target);
$newValue->setCustomField($cfv->getCustomField());
$newValue->setValue($cfv->getValue());
$this->entityManager->persist($newValue);
}
}
/**
* @return array<string, MachineComponentLink> Map of old link ID to new link
*/
private function cloneComponentLinks(Machine $source, Machine $target): array
{
$sourceLinks = $this->machineComponentLinkRepository->findBy(['machine' => $source]);
$linkMap = [];
// First pass: create all links without parent relationships
foreach ($sourceLinks as $link) {
$newLink = new MachineComponentLink();
$newLink->setMachine($target);
$newLink->setComposant($link->getComposant());
$newLink->setNameOverride($link->getNameOverride());
$newLink->setReferenceOverride($link->getReferenceOverride());
$newLink->setPrixOverride($link->getPrixOverride());
$this->entityManager->persist($newLink);
$linkMap[$link->getId()] = $newLink;
}
// Second pass: set parent relationships
foreach ($sourceLinks as $link) {
$parent = $link->getParentLink();
if ($parent && isset($linkMap[$parent->getId()])) {
$linkMap[$link->getId()]->setParentLink($linkMap[$parent->getId()]);
}
}
return $linkMap;
}
/**
* @param array<string, MachineComponentLink> $componentLinkMap
*
* @return array<string, MachinePieceLink> Map of old link ID to new link
*/
private function clonePieceLinks(Machine $source, Machine $target, array $componentLinkMap): array
{
$sourceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $source]);
$linkMap = [];
foreach ($sourceLinks as $link) {
$newLink = new MachinePieceLink();
$newLink->setMachine($target);
$newLink->setPiece($link->getPiece());
$newLink->setNameOverride($link->getNameOverride());
$newLink->setReferenceOverride($link->getReferenceOverride());
$newLink->setPrixOverride($link->getPrixOverride());
$newLink->setQuantity($link->getQuantity());
$parent = $link->getParentLink();
if ($parent && isset($componentLinkMap[$parent->getId()])) {
$newLink->setParentLink($componentLinkMap[$parent->getId()]);
}
$this->entityManager->persist($newLink);
$linkMap[$link->getId()] = $newLink;
}
return $linkMap;
}
/**
* @param array<string, MachineComponentLink> $componentLinkMap
* @param array<string, MachinePieceLink> $pieceLinkMap
*/
private function cloneProductLinks(
Machine $source,
Machine $target,
array $componentLinkMap,
array $pieceLinkMap,
): void {
$sourceLinks = $this->machineProductLinkRepository->findBy(['machine' => $source]);
$linkMap = [];
// First pass: create all links
foreach ($sourceLinks as $link) {
$newLink = new MachineProductLink();
$newLink->setMachine($target);
$newLink->setProduct($link->getProduct());
$parentComponent = $link->getParentComponentLink();
if ($parentComponent && isset($componentLinkMap[$parentComponent->getId()])) {
$newLink->setParentComponentLink($componentLinkMap[$parentComponent->getId()]);
}
$parentPiece = $link->getParentPieceLink();
if ($parentPiece && isset($pieceLinkMap[$parentPiece->getId()])) {
$newLink->setParentPieceLink($pieceLinkMap[$parentPiece->getId()]);
}
$this->entityManager->persist($newLink);
$linkMap[$link->getId()] = $newLink;
}
// Second pass: set parent product link relationships
foreach ($sourceLinks as $link) {
$parent = $link->getParentLink();
if ($parent && isset($linkMap[$parent->getId()])) {
$linkMap[$link->getId()]->setParentLink($linkMap[$parent->getId()]);
}
}
}
}