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>
This commit is contained in:
162
src/Mcp/Tool/Machine/AddMachineLinksTool.php
Normal file
162
src/Mcp/Tool/Machine/AddMachineLinksTool.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\Machine;
|
||||
|
||||
use App\Entity\MachineComponentLink;
|
||||
use App\Entity\MachinePieceLink;
|
||||
use App\Entity\MachineProductLink;
|
||||
use App\Mcp\Tool\McpToolHelper;
|
||||
use App\Repository\ComposantRepository;
|
||||
use App\Repository\MachineComponentLinkRepository;
|
||||
use App\Repository\MachinePieceLinkRepository;
|
||||
use App\Repository\MachineRepository;
|
||||
use App\Repository\PieceRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
#[McpTool(
|
||||
name: 'add_machine_links',
|
||||
description: 'Add one or more links (composant, piece, product) to a machine. Each link specifies a type, entityId, and optional parentLinkId / overrides. Requires ROLE_GESTIONNAIRE.',
|
||||
)]
|
||||
class AddMachineLinksTool
|
||||
{
|
||||
use McpToolHelper;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly Security $security,
|
||||
private readonly MachineRepository $machines,
|
||||
private readonly ComposantRepository $composants,
|
||||
private readonly PieceRepository $pieces,
|
||||
private readonly ProductRepository $products,
|
||||
private readonly MachineComponentLinkRepository $componentLinks,
|
||||
private readonly MachinePieceLinkRepository $pieceLinks,
|
||||
) {}
|
||||
|
||||
public function __invoke(string $machineId, array $links): array
|
||||
{
|
||||
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
|
||||
|
||||
$machine = $this->machines->find($machineId);
|
||||
if (null === $machine) {
|
||||
$this->mcpError('NotFound', "Machine {$machineId} not found.");
|
||||
}
|
||||
|
||||
$created = [];
|
||||
|
||||
foreach ($links as $linkData) {
|
||||
$type = $linkData['type'] ?? '';
|
||||
$entityId = $linkData['entityId'] ?? '';
|
||||
|
||||
switch ($type) {
|
||||
case 'composant':
|
||||
$composant = $this->composants->find($entityId);
|
||||
if (null === $composant) {
|
||||
$this->mcpError('NotFound', "Composant {$entityId} not found.");
|
||||
}
|
||||
|
||||
$link = new MachineComponentLink();
|
||||
$link->setMachine($machine);
|
||||
$link->setComposant($composant);
|
||||
|
||||
if (!empty($linkData['parentLinkId'])) {
|
||||
$parent = $this->componentLinks->find($linkData['parentLinkId']);
|
||||
if (null !== $parent) {
|
||||
$link->setParentLink($parent);
|
||||
}
|
||||
}
|
||||
if (isset($linkData['nameOverride'])) {
|
||||
$link->setNameOverride($linkData['nameOverride']);
|
||||
}
|
||||
if (isset($linkData['referenceOverride'])) {
|
||||
$link->setReferenceOverride($linkData['referenceOverride']);
|
||||
}
|
||||
if (isset($linkData['prixOverride'])) {
|
||||
$link->setPrixOverride($linkData['prixOverride']);
|
||||
}
|
||||
|
||||
$this->em->persist($link);
|
||||
$created[] = ['id' => $link->getId(), 'type' => 'composant', 'entityId' => $entityId];
|
||||
|
||||
break;
|
||||
|
||||
case 'piece':
|
||||
$piece = $this->pieces->find($entityId);
|
||||
if (null === $piece) {
|
||||
$this->mcpError('NotFound', "Piece {$entityId} not found.");
|
||||
}
|
||||
|
||||
$link = new MachinePieceLink();
|
||||
$link->setMachine($machine);
|
||||
$link->setPiece($piece);
|
||||
$link->setQuantity((int) ($linkData['quantity'] ?? 1));
|
||||
|
||||
if (!empty($linkData['parentLinkId'])) {
|
||||
$parent = $this->componentLinks->find($linkData['parentLinkId']);
|
||||
if (null !== $parent) {
|
||||
$link->setParentLink($parent);
|
||||
}
|
||||
}
|
||||
if (isset($linkData['nameOverride'])) {
|
||||
$link->setNameOverride($linkData['nameOverride']);
|
||||
}
|
||||
if (isset($linkData['referenceOverride'])) {
|
||||
$link->setReferenceOverride($linkData['referenceOverride']);
|
||||
}
|
||||
if (isset($linkData['prixOverride'])) {
|
||||
$link->setPrixOverride($linkData['prixOverride']);
|
||||
}
|
||||
|
||||
$this->em->persist($link);
|
||||
$created[] = ['id' => $link->getId(), 'type' => 'piece', 'entityId' => $entityId];
|
||||
|
||||
break;
|
||||
|
||||
case 'product':
|
||||
$product = $this->products->find($entityId);
|
||||
if (null === $product) {
|
||||
$this->mcpError('NotFound', "Product {$entityId} not found.");
|
||||
}
|
||||
|
||||
$link = new MachineProductLink();
|
||||
$link->setMachine($machine);
|
||||
$link->setProduct($product);
|
||||
|
||||
if (!empty($linkData['parentLinkId'])) {
|
||||
$parentProduct = $this->em->getRepository(MachineProductLink::class)->find($linkData['parentLinkId']);
|
||||
if (null !== $parentProduct) {
|
||||
$link->setParentLink($parentProduct);
|
||||
}
|
||||
}
|
||||
if (!empty($linkData['parentComponentLinkId'])) {
|
||||
$parentComp = $this->componentLinks->find($linkData['parentComponentLinkId']);
|
||||
if (null !== $parentComp) {
|
||||
$link->setParentComponentLink($parentComp);
|
||||
}
|
||||
}
|
||||
if (!empty($linkData['parentPieceLinkId'])) {
|
||||
$parentPiece = $this->pieceLinks->find($linkData['parentPieceLinkId']);
|
||||
if (null !== $parentPiece) {
|
||||
$link->setParentPieceLink($parentPiece);
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->persist($link);
|
||||
$created[] = ['id' => $link->getId(), 'type' => 'product', 'entityId' => $entityId];
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
$this->mcpError('Validation', "Unknown link type '{$type}'. Expected composant, piece, or product.");
|
||||
}
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $this->jsonResponse(['created' => $created]);
|
||||
}
|
||||
}
|
||||
223
src/Mcp/Tool/Machine/CloneMachineTool.php
Normal file
223
src/Mcp/Tool/Machine/CloneMachineTool.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?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()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
src/Mcp/Tool/Machine/ListMachineLinksTool.php
Normal file
70
src/Mcp/Tool/Machine/ListMachineLinksTool.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\Machine;
|
||||
|
||||
use App\Mcp\Tool\McpToolHelper;
|
||||
use App\Repository\MachineComponentLinkRepository;
|
||||
use App\Repository\MachinePieceLinkRepository;
|
||||
use App\Repository\MachineProductLinkRepository;
|
||||
use App\Repository\MachineRepository;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
|
||||
#[McpTool(
|
||||
name: 'list_machine_links',
|
||||
description: 'List all links (component, piece, product) for a given machine, grouped by type.',
|
||||
)]
|
||||
class ListMachineLinksTool
|
||||
{
|
||||
use McpToolHelper;
|
||||
|
||||
public function __construct(
|
||||
private readonly MachineRepository $machines,
|
||||
private readonly MachineComponentLinkRepository $componentLinks,
|
||||
private readonly MachinePieceLinkRepository $pieceLinks,
|
||||
private readonly MachineProductLinkRepository $productLinks,
|
||||
) {}
|
||||
|
||||
public function __invoke(string $machineId): array
|
||||
{
|
||||
$machine = $this->machines->find($machineId);
|
||||
if (null === $machine) {
|
||||
$this->mcpError('NotFound', "Machine {$machineId} not found.");
|
||||
}
|
||||
|
||||
$compRows = $this->componentLinks->createQueryBuilder('cl')
|
||||
->select('cl.id', 'IDENTITY(cl.composant) AS entityId', 'IDENTITY(cl.parentLink) AS parentLinkId', 'cl.nameOverride', 'cl.referenceOverride', 'cl.prixOverride')
|
||||
->where('cl.machine = :machine')
|
||||
->setParameter('machine', $machine)
|
||||
->orderBy('cl.id', 'ASC')
|
||||
->getQuery()
|
||||
->getArrayResult()
|
||||
;
|
||||
|
||||
$pieceRows = $this->pieceLinks->createQueryBuilder('pl')
|
||||
->select('pl.id', 'IDENTITY(pl.piece) AS entityId', 'IDENTITY(pl.parentLink) AS parentLinkId', 'pl.nameOverride', 'pl.referenceOverride', 'pl.prixOverride', 'pl.quantity')
|
||||
->where('pl.machine = :machine')
|
||||
->setParameter('machine', $machine)
|
||||
->orderBy('pl.id', 'ASC')
|
||||
->getQuery()
|
||||
->getArrayResult()
|
||||
;
|
||||
|
||||
$productRows = $this->productLinks->createQueryBuilder('prl')
|
||||
->select('prl.id', 'IDENTITY(prl.product) AS entityId', 'IDENTITY(prl.parentLink) AS parentLinkId', 'IDENTITY(prl.parentComponentLink) AS parentComponentLinkId', 'IDENTITY(prl.parentPieceLink) AS parentPieceLinkId')
|
||||
->where('prl.machine = :machine')
|
||||
->setParameter('machine', $machine)
|
||||
->orderBy('prl.id', 'ASC')
|
||||
->getQuery()
|
||||
->getArrayResult()
|
||||
;
|
||||
|
||||
return $this->jsonResponse([
|
||||
'machineId' => $machineId,
|
||||
'componentLinks' => $compRows,
|
||||
'pieceLinks' => $pieceRows,
|
||||
'productLinks' => $productRows,
|
||||
]);
|
||||
}
|
||||
}
|
||||
469
src/Mcp/Tool/Machine/MachineStructureTool.php
Normal file
469
src/Mcp/Tool/Machine/MachineStructureTool.php
Normal file
@@ -0,0 +1,469 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\Machine;
|
||||
|
||||
use App\Entity\Composant;
|
||||
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\ModelType;
|
||||
use App\Entity\Piece;
|
||||
use App\Entity\Product;
|
||||
use App\Mcp\Tool\McpToolHelper;
|
||||
use App\Repository\MachineComponentLinkRepository;
|
||||
use App\Repository\MachinePieceLinkRepository;
|
||||
use App\Repository\MachineProductLinkRepository;
|
||||
use App\Repository\MachineRepository;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
|
||||
#[McpTool(
|
||||
name: 'get_machine_structure',
|
||||
description: 'Get the full machine hierarchy: machine info, component links (with composant details, slots, overrides), piece links (with piece details, quantity, overrides), and product links.',
|
||||
)]
|
||||
class MachineStructureTool
|
||||
{
|
||||
use McpToolHelper;
|
||||
|
||||
public function __construct(
|
||||
private readonly MachineRepository $machineRepository,
|
||||
private readonly MachineComponentLinkRepository $machineComponentLinkRepository,
|
||||
private readonly MachinePieceLinkRepository $machinePieceLinkRepository,
|
||||
private readonly MachineProductLinkRepository $machineProductLinkRepository,
|
||||
) {}
|
||||
|
||||
public function __invoke(string $machineId): array
|
||||
{
|
||||
$machine = $this->machineRepository->find($machineId);
|
||||
|
||||
if (!$machine instanceof Machine) {
|
||||
$this->mcpError('not_found', "Machine not found: {$machineId}");
|
||||
}
|
||||
|
||||
$componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $machine]);
|
||||
$pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $machine]);
|
||||
$productLinks = $this->machineProductLinkRepository->findBy(['machine' => $machine]);
|
||||
|
||||
return $this->jsonResponse($this->normalizeStructureResponse(
|
||||
$machine,
|
||||
$componentLinks,
|
||||
$pieceLinks,
|
||||
$productLinks,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MachineComponentLink[] $componentLinks
|
||||
* @param MachinePieceLink[] $pieceLinks
|
||||
* @param MachineProductLink[] $productLinks
|
||||
*/
|
||||
private function normalizeStructureResponse(
|
||||
Machine $machine,
|
||||
array $componentLinks,
|
||||
array $pieceLinks,
|
||||
array $productLinks,
|
||||
): array {
|
||||
$normalizedComponentLinks = $this->normalizeComponentLinks($componentLinks);
|
||||
$componentIndex = $this->indexById($normalizedComponentLinks);
|
||||
$normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks);
|
||||
|
||||
$childIds = [];
|
||||
foreach ($normalizedComponentLinks as $link) {
|
||||
$parentId = $link['parentComponentLinkId'] ?? null;
|
||||
if ($parentId && isset($componentIndex[$parentId])) {
|
||||
$componentIndex[$parentId]['childLinks'][] = $link;
|
||||
$childIds[$link['id']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
|
||||
|
||||
$rootComponents = array_filter(
|
||||
$componentIndex,
|
||||
static fn (array $link) => !isset($childIds[$link['id']]),
|
||||
);
|
||||
|
||||
return [
|
||||
'machine' => $this->normalizeMachine($machine),
|
||||
'componentLinks' => array_values($rootComponents),
|
||||
'pieceLinks' => $normalizedPieceLinks,
|
||||
'productLinks' => $this->normalizeProductLinks($productLinks),
|
||||
];
|
||||
}
|
||||
|
||||
private function attachPiecesToComponents(array &$componentIndex, array $pieceLinks): void
|
||||
{
|
||||
foreach ($pieceLinks as $pieceLink) {
|
||||
$parentId = $pieceLink['parentComponentLinkId'] ?? null;
|
||||
if ($parentId && isset($componentIndex[$parentId])) {
|
||||
$componentIndex[$parentId]['pieceLinks'][] = $pieceLink;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($componentIndex as &$component) {
|
||||
if (!empty($component['childLinks'])) {
|
||||
$this->attachPiecesToChildComponents($component['childLinks'], $pieceLinks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function attachPiecesToChildComponents(array &$childLinks, array $pieceLinks): void
|
||||
{
|
||||
foreach ($childLinks as &$child) {
|
||||
$childId = $child['id'] ?? null;
|
||||
if ($childId) {
|
||||
foreach ($pieceLinks as $pieceLink) {
|
||||
if (($pieceLink['parentComponentLinkId'] ?? null) === $childId) {
|
||||
$child['pieceLinks'][] = $pieceLink;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($child['childLinks'])) {
|
||||
$this->attachPiecesToChildComponents($child['childLinks'], $pieceLinks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeMachine(Machine $machine): array
|
||||
{
|
||||
$site = $machine->getSite();
|
||||
|
||||
return [
|
||||
'id' => $machine->getId(),
|
||||
'name' => $machine->getName(),
|
||||
'reference' => $machine->getReference(),
|
||||
'prix' => $machine->getPrix(),
|
||||
'siteId' => $site->getId(),
|
||||
'site' => [
|
||||
'id' => $site->getId(),
|
||||
'name' => $site->getName(),
|
||||
],
|
||||
'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()),
|
||||
'customFields' => $this->normalizeCustomFields($machine->getCustomFields()),
|
||||
'customFieldValues' => $this->normalizeCustomFieldValues($machine->getCustomFieldValues()),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MachineComponentLink[] $links
|
||||
*/
|
||||
private function normalizeComponentLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachineComponentLink $link): array {
|
||||
$composant = $link->getComposant();
|
||||
$parentLink = $link->getParentLink();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'composantId' => $composant->getId(),
|
||||
'composant' => $this->normalizeComposant($composant),
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'childLinks' => [],
|
||||
'pieceLinks' => [],
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MachinePieceLink[] $links
|
||||
*/
|
||||
private function normalizePieceLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachinePieceLink $link): array {
|
||||
$piece = $link->getPiece();
|
||||
$parentLink = $link->getParentLink();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'pieceId' => $piece->getId(),
|
||||
'piece' => $this->normalizePiece($piece),
|
||||
'parentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentLinkId' => $parentLink?->getId(),
|
||||
'parentComponentId' => $parentLink?->getComposant()->getId(),
|
||||
'overrides' => $this->normalizeOverrides($link),
|
||||
'quantity' => $this->resolvePieceQuantity($link),
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
|
||||
private function resolvePieceQuantity(MachinePieceLink $link): int
|
||||
{
|
||||
$parentLink = $link->getParentLink();
|
||||
|
||||
if (!$parentLink) {
|
||||
return $link->getQuantity();
|
||||
}
|
||||
|
||||
$composant = $parentLink->getComposant();
|
||||
$piece = $link->getPiece();
|
||||
|
||||
foreach ($composant->getPieceSlots() as $slot) {
|
||||
if ($slot->getSelectedPiece()?->getId() === $piece->getId()) {
|
||||
return $slot->getQuantity();
|
||||
}
|
||||
}
|
||||
|
||||
return $link->getQuantity();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MachineProductLink[] $links
|
||||
*/
|
||||
private function normalizeProductLinks(array $links): array
|
||||
{
|
||||
return array_map(function (MachineProductLink $link): array {
|
||||
$product = $link->getProduct();
|
||||
|
||||
return [
|
||||
'id' => $link->getId(),
|
||||
'linkId' => $link->getId(),
|
||||
'machineId' => $link->getMachine()->getId(),
|
||||
'productId' => $product->getId(),
|
||||
'product' => $this->normalizeProduct($product),
|
||||
'parentLinkId' => $link->getParentLink()?->getId(),
|
||||
'parentComponentLinkId' => $link->getParentComponentLink()?->getId(),
|
||||
'parentPieceLinkId' => $link->getParentPieceLink()?->getId(),
|
||||
];
|
||||
}, $links);
|
||||
}
|
||||
|
||||
private function normalizeComposant(Composant $composant): array
|
||||
{
|
||||
$type = $composant->getTypeComposant();
|
||||
|
||||
return [
|
||||
'id' => $composant->getId(),
|
||||
'name' => $composant->getName(),
|
||||
'reference' => $composant->getReference(),
|
||||
'prix' => $composant->getPrix(),
|
||||
'typeComposantId' => $type?->getId(),
|
||||
'typeComposant' => $this->normalizeModelType($type),
|
||||
'productId' => $composant->getProduct()?->getId(),
|
||||
'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null,
|
||||
'structure' => $this->buildStructureFromSlots($composant),
|
||||
'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()),
|
||||
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getComponentCustomFields()) : [],
|
||||
'customFieldValues' => $this->normalizeCustomFieldValues($composant->getCustomFieldValues()),
|
||||
];
|
||||
}
|
||||
|
||||
private function buildStructureFromSlots(Composant $composant): array
|
||||
{
|
||||
$pieces = [];
|
||||
foreach ($composant->getPieceSlots() as $slot) {
|
||||
$pieceData = [
|
||||
'slotId' => $slot->getId(),
|
||||
'typePieceId' => $slot->getTypePiece()?->getId(),
|
||||
'quantity' => $slot->getQuantity(),
|
||||
'selectedPieceId' => $slot->getSelectedPiece()?->getId(),
|
||||
];
|
||||
if ($slot->getSelectedPiece()) {
|
||||
$pieceData['resolvedPiece'] = $this->normalizePiece($slot->getSelectedPiece());
|
||||
}
|
||||
$pieces[] = $pieceData;
|
||||
}
|
||||
|
||||
$subcomponents = [];
|
||||
foreach ($composant->getSubcomponentSlots() as $slot) {
|
||||
$subcomponents[] = [
|
||||
'alias' => $slot->getAlias(),
|
||||
'familyCode' => $slot->getFamilyCode(),
|
||||
'typeComposantId' => $slot->getTypeComposant()?->getId(),
|
||||
'selectedComponentId' => $slot->getSelectedComposant()?->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
$products = [];
|
||||
foreach ($composant->getProductSlots() as $slot) {
|
||||
$products[] = [
|
||||
'typeProductId' => $slot->getTypeProduct()?->getId(),
|
||||
'familyCode' => $slot->getFamilyCode(),
|
||||
'selectedProductId' => $slot->getSelectedProduct()?->getId(),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'pieces' => $pieces,
|
||||
'subcomponents' => $subcomponents,
|
||||
'products' => $products,
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizePiece(Piece $piece): array
|
||||
{
|
||||
$type = $piece->getTypePiece();
|
||||
|
||||
return [
|
||||
'id' => $piece->getId(),
|
||||
'name' => $piece->getName(),
|
||||
'reference' => $piece->getReference(),
|
||||
'prix' => $piece->getPrix(),
|
||||
'typePieceId' => $type?->getId(),
|
||||
'typePiece' => $this->normalizeModelType($type),
|
||||
'productId' => $piece->getProduct()?->getId(),
|
||||
'product' => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null,
|
||||
'constructeurs' => $this->normalizeConstructeurs($piece->getConstructeurs()),
|
||||
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getPieceCustomFields()) : [],
|
||||
'customFieldValues' => $this->normalizeCustomFieldValues($piece->getCustomFieldValues()),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeProduct(Product $product): array
|
||||
{
|
||||
$type = $product->getTypeProduct();
|
||||
|
||||
return [
|
||||
'id' => $product->getId(),
|
||||
'name' => $product->getName(),
|
||||
'reference' => $product->getReference(),
|
||||
'supplierPrice' => $product->getSupplierPrice(),
|
||||
'typeProductId' => $type?->getId(),
|
||||
'typeProduct' => $this->normalizeModelType($type),
|
||||
'constructeurs' => $this->normalizeConstructeurs($product->getConstructeurs()),
|
||||
'customFields' => $type ? $this->normalizeCustomFieldDefinitions($type->getProductCustomFields()) : [],
|
||||
'customFieldValues' => $this->normalizeCustomFieldValues($product->getCustomFieldValues()),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeModelType(?ModelType $type): ?array
|
||||
{
|
||||
if (!$type instanceof ModelType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'id' => $type->getId(),
|
||||
'name' => $type->getName(),
|
||||
'code' => $type->getCode(),
|
||||
'category' => $type->getCategory()->value,
|
||||
'structure' => $type->getStructure(),
|
||||
];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private function normalizeCustomFields(Collection $customFields): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($customFields as $customField) {
|
||||
if (!$customField instanceof CustomField) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'id' => $customField->getId(),
|
||||
'name' => $customField->getName(),
|
||||
'type' => $customField->getType(),
|
||||
'required' => $customField->isRequired(),
|
||||
'options' => $customField->getOptions(),
|
||||
'defaultValue' => $customField->getDefaultValue(),
|
||||
'orderIndex' => $customField->getOrderIndex(),
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function normalizeCustomFieldDefinitions(Collection $customFields): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($customFields as $cf) {
|
||||
if (!$cf instanceof CustomField) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'id' => $cf->getId(),
|
||||
'name' => $cf->getName(),
|
||||
'type' => $cf->getType(),
|
||||
'required' => $cf->isRequired(),
|
||||
'options' => $cf->getOptions(),
|
||||
'defaultValue' => $cf->getDefaultValue(),
|
||||
'orderIndex' => $cf->getOrderIndex(),
|
||||
];
|
||||
}
|
||||
|
||||
usort($items, static fn (array $a, array $b) => $a['orderIndex'] <=> $b['orderIndex']);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function normalizeCustomFieldValues(Collection $customFieldValues): array
|
||||
{
|
||||
$items = [];
|
||||
foreach ($customFieldValues as $cfv) {
|
||||
if (!$cfv instanceof CustomFieldValue) {
|
||||
continue;
|
||||
}
|
||||
$cf = $cfv->getCustomField();
|
||||
$items[] = [
|
||||
'id' => $cfv->getId(),
|
||||
'value' => $cfv->getValue(),
|
||||
'customField' => [
|
||||
'id' => $cf->getId(),
|
||||
'name' => $cf->getName(),
|
||||
'type' => $cf->getType(),
|
||||
'required' => $cf->isRequired(),
|
||||
'options' => $cf->getOptions(),
|
||||
'defaultValue' => $cf->getDefaultValue(),
|
||||
'orderIndex' => $cf->getOrderIndex(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
private function normalizeOverrides(object $link): ?array
|
||||
{
|
||||
$name = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null;
|
||||
$reference = method_exists($link, 'getReferenceOverride') ? $link->getReferenceOverride() : null;
|
||||
$prix = method_exists($link, 'getPrixOverride') ? $link->getPrixOverride() : null;
|
||||
|
||||
if (null === $name && null === $reference && null === $prix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'name' => $name,
|
||||
'reference' => $reference,
|
||||
'prix' => $prix,
|
||||
];
|
||||
}
|
||||
|
||||
private function indexById(array $links): array
|
||||
{
|
||||
$indexed = [];
|
||||
foreach ($links as $link) {
|
||||
if (is_array($link) && isset($link['id'])) {
|
||||
$indexed[$link['id']] = $link;
|
||||
}
|
||||
}
|
||||
|
||||
return $indexed;
|
||||
}
|
||||
}
|
||||
51
src/Mcp/Tool/Machine/RemoveMachineLinkTool.php
Normal file
51
src/Mcp/Tool/Machine/RemoveMachineLinkTool.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\Machine;
|
||||
|
||||
use App\Mcp\Tool\McpToolHelper;
|
||||
use App\Repository\MachineComponentLinkRepository;
|
||||
use App\Repository\MachinePieceLinkRepository;
|
||||
use App\Repository\MachineProductLinkRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
#[McpTool(
|
||||
name: 'remove_machine_link',
|
||||
description: 'Remove a machine link by id and type (composant, piece, or product). Requires ROLE_GESTIONNAIRE.',
|
||||
)]
|
||||
class RemoveMachineLinkTool
|
||||
{
|
||||
use McpToolHelper;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly Security $security,
|
||||
private readonly MachineComponentLinkRepository $componentLinks,
|
||||
private readonly MachinePieceLinkRepository $pieceLinks,
|
||||
private readonly MachineProductLinkRepository $productLinks,
|
||||
) {}
|
||||
|
||||
public function __invoke(string $linkId, string $linkType): array
|
||||
{
|
||||
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
|
||||
|
||||
$link = match ($linkType) {
|
||||
'composant' => $this->componentLinks->find($linkId),
|
||||
'piece' => $this->pieceLinks->find($linkId),
|
||||
'product' => $this->productLinks->find($linkId),
|
||||
default => $this->mcpError('Validation', "Unknown link type '{$linkType}'. Expected composant, piece, or product."),
|
||||
};
|
||||
|
||||
if (null === $link) {
|
||||
$this->mcpError('NotFound', "Link {$linkId} of type {$linkType} not found.");
|
||||
}
|
||||
|
||||
$this->em->remove($link);
|
||||
$this->em->flush();
|
||||
|
||||
return $this->jsonResponse(['deleted' => true, 'id' => $linkId, 'type' => $linkType]);
|
||||
}
|
||||
}
|
||||
101
src/Mcp/Tool/Machine/UpdateMachineLinkTool.php
Normal file
101
src/Mcp/Tool/Machine/UpdateMachineLinkTool.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\Machine;
|
||||
|
||||
use App\Entity\MachineComponentLink;
|
||||
use App\Entity\MachinePieceLink;
|
||||
use App\Mcp\Tool\McpToolHelper;
|
||||
use App\Repository\MachineComponentLinkRepository;
|
||||
use App\Repository\MachinePieceLinkRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
#[McpTool(
|
||||
name: 'update_machine_link',
|
||||
description: 'Update overrides (nameOverride, referenceOverride, prixOverride) or quantity on an existing machine link. Requires ROLE_GESTIONNAIRE.',
|
||||
)]
|
||||
class UpdateMachineLinkTool
|
||||
{
|
||||
use McpToolHelper;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly Security $security,
|
||||
private readonly MachineComponentLinkRepository $componentLinks,
|
||||
private readonly MachinePieceLinkRepository $pieceLinks,
|
||||
) {}
|
||||
|
||||
public function __invoke(
|
||||
string $linkId,
|
||||
string $linkType,
|
||||
?string $nameOverride = null,
|
||||
?string $referenceOverride = null,
|
||||
?string $prixOverride = null,
|
||||
?int $quantity = null,
|
||||
): array {
|
||||
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
|
||||
|
||||
switch ($linkType) {
|
||||
case 'composant':
|
||||
$link = $this->componentLinks->find($linkId);
|
||||
if (null === $link) {
|
||||
$this->mcpError('NotFound', "MachineComponentLink {$linkId} not found.");
|
||||
}
|
||||
|
||||
$this->applyOverrides($link, $nameOverride, $referenceOverride, $prixOverride);
|
||||
$this->em->flush();
|
||||
|
||||
return $this->jsonResponse([
|
||||
'id' => $link->getId(),
|
||||
'type' => 'composant',
|
||||
'nameOverride' => $link->getNameOverride(),
|
||||
'referenceOverride' => $link->getReferenceOverride(),
|
||||
'prixOverride' => $link->getPrixOverride(),
|
||||
]);
|
||||
|
||||
case 'piece':
|
||||
$link = $this->pieceLinks->find($linkId);
|
||||
if (null === $link) {
|
||||
$this->mcpError('NotFound', "MachinePieceLink {$linkId} not found.");
|
||||
}
|
||||
|
||||
$this->applyOverrides($link, $nameOverride, $referenceOverride, $prixOverride);
|
||||
if (null !== $quantity) {
|
||||
$link->setQuantity($quantity);
|
||||
}
|
||||
$this->em->flush();
|
||||
|
||||
return $this->jsonResponse([
|
||||
'id' => $link->getId(),
|
||||
'type' => 'piece',
|
||||
'nameOverride' => $link->getNameOverride(),
|
||||
'referenceOverride' => $link->getReferenceOverride(),
|
||||
'prixOverride' => $link->getPrixOverride(),
|
||||
'quantity' => $link->getQuantity(),
|
||||
]);
|
||||
|
||||
case 'product':
|
||||
$this->mcpError('Validation', 'Product links do not have updatable overrides.');
|
||||
|
||||
// no break
|
||||
default:
|
||||
$this->mcpError('Validation', "Unknown link type '{$linkType}'. Expected composant, piece, or product.");
|
||||
}
|
||||
}
|
||||
|
||||
private function applyOverrides(MachineComponentLink|MachinePieceLink $link, ?string $nameOverride, ?string $referenceOverride, ?string $prixOverride): void
|
||||
{
|
||||
if (null !== $nameOverride) {
|
||||
$link->setNameOverride($nameOverride);
|
||||
}
|
||||
if (null !== $referenceOverride) {
|
||||
$link->setReferenceOverride($referenceOverride);
|
||||
}
|
||||
if (null !== $prixOverride) {
|
||||
$link->setPrixOverride($prixOverride);
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/Mcp/Tool/Slot/ListSlotsTool.php
Normal file
139
src/Mcp/Tool/Slot/ListSlotsTool.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\Slot;
|
||||
|
||||
use App\Entity\ComposantPieceSlot;
|
||||
use App\Entity\ComposantProductSlot;
|
||||
use App\Entity\ComposantSubcomponentSlot;
|
||||
use App\Entity\PieceProductSlot;
|
||||
use App\Mcp\Tool\McpToolHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
|
||||
#[McpTool(
|
||||
name: 'list_slots',
|
||||
description: 'List all slots for a composant or piece. Composants have piece/product/subcomponent slots. Pieces have product slots only.',
|
||||
)]
|
||||
class ListSlotsTool
|
||||
{
|
||||
use McpToolHelper;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function __invoke(string $entityType, string $entityId): array
|
||||
{
|
||||
if ('composant' === $entityType) {
|
||||
return $this->listComposantSlots($entityId);
|
||||
}
|
||||
|
||||
if ('piece' === $entityType) {
|
||||
return $this->listPieceSlots($entityId);
|
||||
}
|
||||
|
||||
$this->mcpError('validation', "entityType must be 'composant' or 'piece', got '{$entityType}'.");
|
||||
}
|
||||
|
||||
private function listComposantSlots(string $composantId): array
|
||||
{
|
||||
$pieceSlots = $this->em->createQueryBuilder()
|
||||
->select(
|
||||
'ps.id',
|
||||
"'piece' AS slotType",
|
||||
'ps.position',
|
||||
'ps.quantity',
|
||||
'tp.name AS typeName',
|
||||
'sp.id AS selectedEntityId',
|
||||
'sp.name AS selectedEntityName',
|
||||
)
|
||||
->from(ComposantPieceSlot::class, 'ps')
|
||||
->leftJoin('ps.typePiece', 'tp')
|
||||
->leftJoin('ps.selectedPiece', 'sp')
|
||||
->where('IDENTITY(ps.composant) = :cid')
|
||||
->setParameter('cid', $composantId)
|
||||
->orderBy('ps.position', 'ASC')
|
||||
->getQuery()
|
||||
->getArrayResult()
|
||||
;
|
||||
|
||||
$productSlots = $this->em->createQueryBuilder()
|
||||
->select(
|
||||
'prs.id',
|
||||
"'product' AS slotType",
|
||||
'prs.position',
|
||||
'tp.name AS typeName',
|
||||
'sp.id AS selectedEntityId',
|
||||
'sp.name AS selectedEntityName',
|
||||
)
|
||||
->from(ComposantProductSlot::class, 'prs')
|
||||
->leftJoin('prs.typeProduct', 'tp')
|
||||
->leftJoin('prs.selectedProduct', 'sp')
|
||||
->where('IDENTITY(prs.composant) = :cid')
|
||||
->setParameter('cid', $composantId)
|
||||
->orderBy('prs.position', 'ASC')
|
||||
->getQuery()
|
||||
->getArrayResult()
|
||||
;
|
||||
|
||||
$subSlots = $this->em->createQueryBuilder()
|
||||
->select(
|
||||
'ss.id',
|
||||
"'subcomponent' AS slotType",
|
||||
'ss.position',
|
||||
'ss.alias',
|
||||
'tc.name AS typeName',
|
||||
'sc.id AS selectedEntityId',
|
||||
'sc.name AS selectedEntityName',
|
||||
)
|
||||
->from(ComposantSubcomponentSlot::class, 'ss')
|
||||
->leftJoin('ss.typeComposant', 'tc')
|
||||
->leftJoin('ss.selectedComposant', 'sc')
|
||||
->where('IDENTITY(ss.composant) = :cid')
|
||||
->setParameter('cid', $composantId)
|
||||
->orderBy('ss.position', 'ASC')
|
||||
->getQuery()
|
||||
->getArrayResult()
|
||||
;
|
||||
|
||||
$slots = array_merge($pieceSlots, $productSlots, $subSlots);
|
||||
|
||||
return $this->jsonResponse([
|
||||
'entityType' => 'composant',
|
||||
'entityId' => $composantId,
|
||||
'slots' => $slots,
|
||||
'total' => count($slots),
|
||||
]);
|
||||
}
|
||||
|
||||
private function listPieceSlots(string $pieceId): array
|
||||
{
|
||||
$slots = $this->em->createQueryBuilder()
|
||||
->select(
|
||||
'pps.id',
|
||||
"'product' AS slotType",
|
||||
'pps.position',
|
||||
'tp.name AS typeName',
|
||||
'sp.id AS selectedEntityId',
|
||||
'sp.name AS selectedEntityName',
|
||||
)
|
||||
->from(PieceProductSlot::class, 'pps')
|
||||
->leftJoin('pps.typeProduct', 'tp')
|
||||
->leftJoin('pps.selectedProduct', 'sp')
|
||||
->where('IDENTITY(pps.piece) = :pid')
|
||||
->setParameter('pid', $pieceId)
|
||||
->orderBy('pps.position', 'ASC')
|
||||
->getQuery()
|
||||
->getArrayResult()
|
||||
;
|
||||
|
||||
return $this->jsonResponse([
|
||||
'entityType' => 'piece',
|
||||
'entityId' => $pieceId,
|
||||
'slots' => $slots,
|
||||
'total' => count($slots),
|
||||
]);
|
||||
}
|
||||
}
|
||||
178
src/Mcp/Tool/Slot/UpdateSlotsTool.php
Normal file
178
src/Mcp/Tool/Slot/UpdateSlotsTool.php
Normal file
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Mcp\Tool\Slot;
|
||||
|
||||
use App\Entity\Composant;
|
||||
use App\Entity\ComposantPieceSlot;
|
||||
use App\Entity\ComposantProductSlot;
|
||||
use App\Entity\ComposantSubcomponentSlot;
|
||||
use App\Entity\Piece;
|
||||
use App\Entity\PieceProductSlot;
|
||||
use App\Entity\Product;
|
||||
use App\Mcp\Tool\McpToolHelper;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mcp\Capability\Attribute\McpTool;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
#[McpTool(
|
||||
name: 'update_slots',
|
||||
description: 'Update selected entities on one or more slots. Each slot entry needs slotId, slotType, and the selected entity ID (or null to clear). Requires ROLE_GESTIONNAIRE.',
|
||||
)]
|
||||
class UpdateSlotsTool
|
||||
{
|
||||
use McpToolHelper;
|
||||
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly Security $security,
|
||||
) {}
|
||||
|
||||
public function __invoke(array $slots): array
|
||||
{
|
||||
$this->requireRole($this->security, 'ROLE_GESTIONNAIRE');
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($slots as $entry) {
|
||||
$slotId = $entry['slotId'] ?? null;
|
||||
$slotType = $entry['slotType'] ?? null;
|
||||
|
||||
if (null === $slotId || null === $slotType) {
|
||||
$this->mcpError('validation', 'Each slot entry must have slotId and slotType.');
|
||||
}
|
||||
|
||||
$results[] = match ($slotType) {
|
||||
'composant_piece' => $this->updateComposantPieceSlot($slotId, $entry),
|
||||
'composant_product' => $this->updateComposantProductSlot($slotId, $entry),
|
||||
'composant_subcomponent' => $this->updateComposantSubcomponentSlot($slotId, $entry),
|
||||
'piece_product' => $this->updatePieceProductSlot($slotId, $entry),
|
||||
default => $this->mcpError('validation', "Unknown slotType '{$slotType}'."),
|
||||
};
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
|
||||
return $this->jsonResponse([
|
||||
'updated' => $results,
|
||||
'total' => count($results),
|
||||
]);
|
||||
}
|
||||
|
||||
private function updateComposantPieceSlot(string $slotId, array $entry): array
|
||||
{
|
||||
$slot = $this->em->getRepository(ComposantPieceSlot::class)->find($slotId);
|
||||
|
||||
if (null === $slot) {
|
||||
$this->mcpError('not_found', "ComposantPieceSlot '{$slotId}' not found.");
|
||||
}
|
||||
|
||||
$selectedPieceId = $entry['selectedPieceId'] ?? null;
|
||||
$selectedPiece = null;
|
||||
|
||||
if (null !== $selectedPieceId) {
|
||||
$selectedPiece = $this->em->getRepository(Piece::class)->find($selectedPieceId);
|
||||
|
||||
if (null === $selectedPiece) {
|
||||
$this->mcpError('not_found', "Piece '{$selectedPieceId}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
$slot->setSelectedPiece($selectedPiece);
|
||||
|
||||
return [
|
||||
'slotId' => $slot->getId(),
|
||||
'slotType' => 'composant_piece',
|
||||
'selectedEntityId' => $selectedPiece?->getId(),
|
||||
'selectedEntityName' => $selectedPiece?->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
private function updateComposantProductSlot(string $slotId, array $entry): array
|
||||
{
|
||||
$slot = $this->em->getRepository(ComposantProductSlot::class)->find($slotId);
|
||||
|
||||
if (null === $slot) {
|
||||
$this->mcpError('not_found', "ComposantProductSlot '{$slotId}' not found.");
|
||||
}
|
||||
|
||||
$selectedProductId = $entry['selectedProductId'] ?? null;
|
||||
$selectedProduct = null;
|
||||
|
||||
if (null !== $selectedProductId) {
|
||||
$selectedProduct = $this->em->getRepository(Product::class)->find($selectedProductId);
|
||||
|
||||
if (null === $selectedProduct) {
|
||||
$this->mcpError('not_found', "Product '{$selectedProductId}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
$slot->setSelectedProduct($selectedProduct);
|
||||
|
||||
return [
|
||||
'slotId' => $slot->getId(),
|
||||
'slotType' => 'composant_product',
|
||||
'selectedEntityId' => $selectedProduct?->getId(),
|
||||
'selectedEntityName' => $selectedProduct?->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
private function updateComposantSubcomponentSlot(string $slotId, array $entry): array
|
||||
{
|
||||
$slot = $this->em->getRepository(ComposantSubcomponentSlot::class)->find($slotId);
|
||||
|
||||
if (null === $slot) {
|
||||
$this->mcpError('not_found', "ComposantSubcomponentSlot '{$slotId}' not found.");
|
||||
}
|
||||
|
||||
$selectedComposantId = $entry['selectedComposantId'] ?? null;
|
||||
$selectedComposant = null;
|
||||
|
||||
if (null !== $selectedComposantId) {
|
||||
$selectedComposant = $this->em->getRepository(Composant::class)->find($selectedComposantId);
|
||||
|
||||
if (null === $selectedComposant) {
|
||||
$this->mcpError('not_found', "Composant '{$selectedComposantId}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
$slot->setSelectedComposant($selectedComposant);
|
||||
|
||||
return [
|
||||
'slotId' => $slot->getId(),
|
||||
'slotType' => 'composant_subcomponent',
|
||||
'selectedEntityId' => $selectedComposant?->getId(),
|
||||
'selectedEntityName' => $selectedComposant?->getName(),
|
||||
];
|
||||
}
|
||||
|
||||
private function updatePieceProductSlot(string $slotId, array $entry): array
|
||||
{
|
||||
$slot = $this->em->getRepository(PieceProductSlot::class)->find($slotId);
|
||||
|
||||
if (null === $slot) {
|
||||
$this->mcpError('not_found', "PieceProductSlot '{$slotId}' not found.");
|
||||
}
|
||||
|
||||
$selectedProductId = $entry['selectedProductId'] ?? null;
|
||||
$selectedProduct = null;
|
||||
|
||||
if (null !== $selectedProductId) {
|
||||
$selectedProduct = $this->em->getRepository(Product::class)->find($selectedProductId);
|
||||
|
||||
if (null === $selectedProduct) {
|
||||
$this->mcpError('not_found', "Product '{$selectedProductId}' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
$slot->setSelectedProduct($selectedProduct);
|
||||
|
||||
return [
|
||||
'slotId' => $slot->getId(),
|
||||
'slotType' => 'piece_product',
|
||||
'selectedEntityId' => $selectedProduct?->getId(),
|
||||
'selectedEntityName' => $selectedProduct?->getName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
81
tests/Mcp/Tool/Machine/MachineLinksToolTest.php
Normal file
81
tests/Mcp/Tool/Machine/MachineLinksToolTest.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Mcp\Tool\Machine;
|
||||
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class MachineLinksToolTest extends AbstractApiTestCase
|
||||
{
|
||||
public function testListMachineLinks(): void
|
||||
{
|
||||
$machine = $this->createMachine(name: 'Machine Links Test');
|
||||
$composant = $this->createComposant(name: 'Comp A');
|
||||
$piece = $this->createPiece(name: 'Piece A');
|
||||
|
||||
$compLink = $this->createMachineComponentLink($machine, $composant);
|
||||
$pieceLink = $this->createMachinePieceLink($machine, $piece, parentLink: $compLink, quantity: 3);
|
||||
|
||||
$session = $this->createMcpClient('ROLE_VIEWER');
|
||||
$data = $this->callMcpTool($session, 'list_machine_links', ['machineId' => $machine->getId()]);
|
||||
|
||||
$this->assertArrayHasKey('_parsed', $data);
|
||||
$parsed = $data['_parsed'];
|
||||
|
||||
$this->assertSame($machine->getId(), $parsed['machineId']);
|
||||
$this->assertCount(1, $parsed['componentLinks']);
|
||||
$this->assertCount(1, $parsed['pieceLinks']);
|
||||
$this->assertSame($compLink->getId(), $parsed['componentLinks'][0]['id']);
|
||||
$this->assertSame($pieceLink->getId(), $parsed['pieceLinks'][0]['id']);
|
||||
$this->assertSame(3, $parsed['pieceLinks'][0]['quantity']);
|
||||
}
|
||||
|
||||
public function testAddMachineComponentLink(): void
|
||||
{
|
||||
$machine = $this->createMachine(name: 'Machine Add Test');
|
||||
$composant = $this->createComposant(name: 'Comp B');
|
||||
|
||||
$session = $this->createMcpClient('ROLE_GESTIONNAIRE');
|
||||
$data = $this->callMcpTool($session, 'add_machine_links', [
|
||||
'machineId' => $machine->getId(),
|
||||
'links' => [
|
||||
[
|
||||
'type' => 'composant',
|
||||
'entityId' => $composant->getId(),
|
||||
'nameOverride' => 'Custom Name',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('_parsed', $data);
|
||||
$created = $data['_parsed']['created'];
|
||||
$this->assertCount(1, $created);
|
||||
$this->assertSame('composant', $created[0]['type']);
|
||||
$this->assertSame($composant->getId(), $created[0]['entityId']);
|
||||
$this->assertNotEmpty($created[0]['id']);
|
||||
}
|
||||
|
||||
public function testRemoveMachineLink(): void
|
||||
{
|
||||
$machine = $this->createMachine(name: 'Machine Remove Test');
|
||||
$composant = $this->createComposant(name: 'Comp C');
|
||||
$link = $this->createMachineComponentLink($machine, $composant);
|
||||
|
||||
$session = $this->createMcpClient('ROLE_GESTIONNAIRE');
|
||||
$data = $this->callMcpTool($session, 'remove_machine_link', [
|
||||
'linkId' => $link->getId(),
|
||||
'linkType' => 'composant',
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('_parsed', $data);
|
||||
$this->assertTrue($data['_parsed']['deleted']);
|
||||
|
||||
// Verify the link is gone by listing
|
||||
$listData = $this->callMcpTool($session, 'list_machine_links', ['machineId' => $machine->getId()]);
|
||||
$this->assertCount(0, $listData['_parsed']['componentLinks']);
|
||||
}
|
||||
}
|
||||
164
tests/Mcp/Tool/Machine/MachineStructureToolTest.php
Normal file
164
tests/Mcp/Tool/Machine/MachineStructureToolTest.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Mcp\Tool\Machine;
|
||||
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class MachineStructureToolTest extends AbstractApiTestCase
|
||||
{
|
||||
public function testGetMachineStructure(): void
|
||||
{
|
||||
$site = $this->createSite(name: 'Site Structure');
|
||||
$machine = $this->createMachine(name: 'Machine Structure', site: $site, reference: 'REF-STRUCT');
|
||||
$composant = $this->createComposant(name: 'Composant Alpha');
|
||||
$piece = $this->createPiece(name: 'Piece Alpha', reference: 'REF-P1');
|
||||
$product = $this->createProduct(name: 'Product Alpha', reference: 'REF-PR1');
|
||||
|
||||
$componentLink = $this->createMachineComponentLink($machine, $composant);
|
||||
$this->createMachinePieceLink($machine, $piece, $componentLink, quantity: 3);
|
||||
$this->createMachineProductLink($machine, $product);
|
||||
|
||||
$session = $this->createMcpClient('ROLE_VIEWER');
|
||||
|
||||
$data = $this->callMcpTool($session, 'get_machine_structure', [
|
||||
'machineId' => $machine->getId(),
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('_parsed', $data);
|
||||
$parsed = $data['_parsed'];
|
||||
|
||||
// Machine info
|
||||
$this->assertSame('Machine Structure', $parsed['machine']['name']);
|
||||
$this->assertSame('REF-STRUCT', $parsed['machine']['reference']);
|
||||
$this->assertSame($site->getId(), $parsed['machine']['siteId']);
|
||||
|
||||
// Component links
|
||||
$this->assertCount(1, $parsed['componentLinks']);
|
||||
$this->assertSame($composant->getId(), $parsed['componentLinks'][0]['composantId']);
|
||||
$this->assertSame('Composant Alpha', $parsed['componentLinks'][0]['composant']['name']);
|
||||
|
||||
// Piece links
|
||||
$this->assertCount(1, $parsed['pieceLinks']);
|
||||
$this->assertSame($piece->getId(), $parsed['pieceLinks'][0]['pieceId']);
|
||||
$this->assertSame('Piece Alpha', $parsed['pieceLinks'][0]['piece']['name']);
|
||||
$this->assertSame(3, $parsed['pieceLinks'][0]['quantity']);
|
||||
|
||||
// Product links
|
||||
$this->assertCount(1, $parsed['productLinks']);
|
||||
$this->assertSame($product->getId(), $parsed['productLinks'][0]['productId']);
|
||||
$this->assertSame('Product Alpha', $parsed['productLinks'][0]['product']['name']);
|
||||
}
|
||||
|
||||
public function testGetMachineStructureNotFound(): void
|
||||
{
|
||||
$session = $this->createMcpClient('ROLE_VIEWER');
|
||||
|
||||
$data = $this->callMcpTool($session, 'get_machine_structure', [
|
||||
'machineId' => 'nonexistent-id',
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('error', $data);
|
||||
}
|
||||
|
||||
public function testGetMachineStructureWithOverrides(): void
|
||||
{
|
||||
$site = $this->createSite(name: 'Site Overrides');
|
||||
$machine = $this->createMachine(name: 'Machine Overrides', site: $site);
|
||||
$composant = $this->createComposant(name: 'Composant Override');
|
||||
|
||||
$componentLink = $this->createMachineComponentLink($machine, $composant);
|
||||
$componentLink->setNameOverride('Custom Name');
|
||||
$componentLink->setReferenceOverride('CUSTOM-REF');
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
$session = $this->createMcpClient('ROLE_VIEWER');
|
||||
|
||||
$data = $this->callMcpTool($session, 'get_machine_structure', [
|
||||
'machineId' => $machine->getId(),
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('_parsed', $data);
|
||||
$overrides = $data['_parsed']['componentLinks'][0]['overrides'];
|
||||
$this->assertSame('Custom Name', $overrides['name']);
|
||||
$this->assertSame('CUSTOM-REF', $overrides['reference']);
|
||||
}
|
||||
|
||||
public function testCloneMachine(): void
|
||||
{
|
||||
$site = $this->createSite(name: 'Site Source');
|
||||
$targetSite = $this->createSite(name: 'Site Target');
|
||||
$machine = $this->createMachine(name: 'Machine Source', site: $site, reference: 'REF-SRC');
|
||||
$composant = $this->createComposant(name: 'Composant Clone');
|
||||
$piece = $this->createPiece(name: 'Piece Clone');
|
||||
$product = $this->createProduct(name: 'Product Clone');
|
||||
|
||||
$componentLink = $this->createMachineComponentLink($machine, $composant);
|
||||
$this->createMachinePieceLink($machine, $piece, $componentLink, quantity: 2);
|
||||
$this->createMachineProductLink($machine, $product);
|
||||
|
||||
$session = $this->createMcpClient('ROLE_GESTIONNAIRE');
|
||||
|
||||
$data = $this->callMcpTool($session, 'clone_machine', [
|
||||
'machineId' => $machine->getId(),
|
||||
'name' => 'Machine Cloned',
|
||||
'siteId' => $targetSite->getId(),
|
||||
'reference' => 'REF-CLN',
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('_parsed', $data);
|
||||
$parsed = $data['_parsed'];
|
||||
|
||||
$this->assertSame('Machine Cloned', $parsed['name']);
|
||||
$this->assertSame('REF-CLN', $parsed['reference']);
|
||||
$this->assertSame($targetSite->getId(), $parsed['siteId']);
|
||||
$this->assertSame($machine->getId(), $parsed['clonedFrom']);
|
||||
$this->assertNotSame($machine->getId(), $parsed['id']);
|
||||
|
||||
// Verify the cloned machine has links by fetching its structure
|
||||
$structureData = $this->callMcpTool($session, 'get_machine_structure', [
|
||||
'machineId' => $parsed['id'],
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('_parsed', $structureData);
|
||||
$structure = $structureData['_parsed'];
|
||||
|
||||
$this->assertCount(1, $structure['componentLinks']);
|
||||
$this->assertSame($composant->getId(), $structure['componentLinks'][0]['composantId']);
|
||||
$this->assertCount(1, $structure['pieceLinks']);
|
||||
$this->assertCount(1, $structure['productLinks']);
|
||||
}
|
||||
|
||||
public function testCloneMachineRequiresGestionnaire(): void
|
||||
{
|
||||
$site = $this->createSite(name: 'Site Forbidden');
|
||||
$machine = $this->createMachine(name: 'Machine Forbidden', site: $site);
|
||||
$session = $this->createMcpClient('ROLE_VIEWER');
|
||||
|
||||
$data = $this->callMcpTool($session, 'clone_machine', [
|
||||
'machineId' => $machine->getId(),
|
||||
'name' => 'Should Fail',
|
||||
'siteId' => $site->getId(),
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('error', $data, 'Should fail with VIEWER role');
|
||||
}
|
||||
|
||||
public function testCloneMachineNotFound(): void
|
||||
{
|
||||
$site = $this->createSite(name: 'Site Clone NF');
|
||||
$session = $this->createMcpClient('ROLE_GESTIONNAIRE');
|
||||
|
||||
$data = $this->callMcpTool($session, 'clone_machine', [
|
||||
'machineId' => 'nonexistent-id',
|
||||
'name' => 'Should Fail',
|
||||
'siteId' => $site->getId(),
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('error', $data);
|
||||
}
|
||||
}
|
||||
86
tests/Mcp/Tool/Slot/SlotsToolTest.php
Normal file
86
tests/Mcp/Tool/Slot/SlotsToolTest.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Mcp\Tool\Slot;
|
||||
|
||||
use App\Tests\AbstractApiTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class SlotsToolTest extends AbstractApiTestCase
|
||||
{
|
||||
public function testListSlotsForComposant(): void
|
||||
{
|
||||
$composant = $this->createComposant(name: 'Comp Slots');
|
||||
$this->createComposantPieceSlot(composant: $composant, position: 0);
|
||||
$this->createComposantProductSlot(composant: $composant, position: 1);
|
||||
$this->createComposantSubcomponentSlot(composant: $composant, alias: 'Sub A', position: 2);
|
||||
|
||||
$session = $this->createMcpClient('ROLE_VIEWER');
|
||||
|
||||
$data = $this->callMcpTool($session, 'list_slots', [
|
||||
'entityType' => 'composant',
|
||||
'entityId' => $composant->getId(),
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('_parsed', $data);
|
||||
$parsed = $data['_parsed'];
|
||||
$this->assertSame('composant', $parsed['entityType']);
|
||||
$this->assertSame(3, $parsed['total']);
|
||||
|
||||
$slotTypes = array_column($parsed['slots'], 'slotType');
|
||||
$this->assertContains('piece', $slotTypes);
|
||||
$this->assertContains('product', $slotTypes);
|
||||
$this->assertContains('subcomponent', $slotTypes);
|
||||
}
|
||||
|
||||
public function testListSlotsForPiece(): void
|
||||
{
|
||||
$piece = $this->createPiece(name: 'Piece Slots');
|
||||
$this->createPieceProductSlot(piece: $piece, position: 0);
|
||||
$this->createPieceProductSlot(piece: $piece, position: 1);
|
||||
|
||||
$session = $this->createMcpClient('ROLE_VIEWER');
|
||||
|
||||
$data = $this->callMcpTool($session, 'list_slots', [
|
||||
'entityType' => 'piece',
|
||||
'entityId' => $piece->getId(),
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('_parsed', $data);
|
||||
$parsed = $data['_parsed'];
|
||||
$this->assertSame('piece', $parsed['entityType']);
|
||||
$this->assertSame(2, $parsed['total']);
|
||||
|
||||
foreach ($parsed['slots'] as $slot) {
|
||||
$this->assertSame('product', $slot['slotType']);
|
||||
}
|
||||
}
|
||||
|
||||
public function testUpdateSlotSelectsPiece(): void
|
||||
{
|
||||
$composant = $this->createComposant(name: 'Comp Update');
|
||||
$piece = $this->createPiece(name: 'Selected Piece');
|
||||
$slot = $this->createComposantPieceSlot(composant: $composant, position: 0);
|
||||
|
||||
$session = $this->createMcpClient('ROLE_GESTIONNAIRE');
|
||||
|
||||
$data = $this->callMcpTool($session, 'update_slots', [
|
||||
'slots' => [
|
||||
[
|
||||
'slotId' => $slot->getId(),
|
||||
'slotType' => 'composant_piece',
|
||||
'selectedPieceId' => $piece->getId(),
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertArrayHasKey('_parsed', $data);
|
||||
$parsed = $data['_parsed'];
|
||||
$this->assertSame(1, $parsed['total']);
|
||||
$this->assertSame($piece->getId(), $parsed['updated'][0]['selectedEntityId']);
|
||||
$this->assertSame('Selected Piece', $parsed['updated'][0]['selectedEntityName']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user