denyAccessUnlessGranted('ROLE_VIEWER'); $machine = $this->machineRepository->find($id); if (!$machine instanceof Machine) { return $this->json(['success' => false, 'error' => 'Machine not found.'], 404); } $componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC']); $pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC']); $productLinks = $this->machineProductLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC']); return $this->json($this->normalizeStructureResponse( $machine, $componentLinks, $pieceLinks, $productLinks )); } #[Route('/{id}/structure', name: 'machine_structure_update', methods: ['PATCH'])] public function updateStructure(string $id, Request $request): JsonResponse { $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE'); $machine = $this->machineRepository->find($id); if (!$machine instanceof Machine) { return $this->json(['success' => false, 'error' => 'Machine not found.'], 404); } $payload = json_decode($request->getContent(), true); if (!is_array($payload)) { return $this->json(['success' => false, 'error' => 'Invalid JSON payload.'], 400); } $componentLinksPayload = $this->normalizePayloadList($payload['componentLinks'] ?? []); $pieceLinksPayload = $this->normalizePayloadList($payload['pieceLinks'] ?? []); $productLinksPayload = $this->normalizePayloadList($payload['productLinks'] ?? []); $componentLinks = $this->applyComponentLinks($machine, $componentLinksPayload); if ($componentLinks instanceof JsonResponse) { return $componentLinks; } $pieceLinks = $this->applyPieceLinks($machine, $pieceLinksPayload, $componentLinks); if ($pieceLinks instanceof JsonResponse) { return $pieceLinks; } $productLinks = $this->applyProductLinks($machine, $productLinksPayload, $componentLinks, $pieceLinks); if ($productLinks instanceof JsonResponse) { return $productLinks; } $this->entityManager->flush(); return $this->json($this->normalizeStructureResponse( $machine, $componentLinks, $pieceLinks, $productLinks )); } #[Route('/{id}/clone', name: 'machine_clone', methods: ['POST'])] public function cloneMachine(string $id, Request $request): JsonResponse { $this->denyAccessUnlessGranted('ROLE_GESTIONNAIRE'); $source = $this->machineRepository->find($id); if (!$source instanceof Machine) { return $this->json(['success' => false, 'error' => 'Machine source introuvable.'], 404); } $payload = json_decode($request->getContent(), true); if (!is_array($payload) || empty($payload['name']) || empty($payload['siteId'])) { return $this->json(['success' => false, 'error' => 'name et siteId sont requis.'], 400); } $site = $this->entityManager->getRepository(Site::class)->find($payload['siteId']); if (!$site) { return $this->json(['success' => false, 'error' => 'Site introuvable.'], 404); } // Create new machine $newMachine = new Machine(); $newMachine->setName($payload['name']); $newMachine->setSite($site); if (!empty($payload['reference'])) { $newMachine->setReference($payload['reference']); } $newMachine->setPrix($source->getPrix()); // 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); } $this->entityManager->persist($newMachine); // Copy custom fields and values $this->cloneCustomFields($source, $newMachine); // Copy component links (preserving hierarchy) $componentLinkMap = $this->cloneComponentLinks($source, $newMachine); // Copy piece links $pieceLinkMap = $this->clonePieceLinks($source, $newMachine, $componentLinkMap); // Copy product links $this->cloneProductLinks($source, $newMachine, $componentLinkMap, $pieceLinkMap); // Copy context field values $this->cloneContextFieldValues($componentLinkMap, $pieceLinkMap); $this->entityManager->flush(); $componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $newMachine], ['createdAt' => 'ASC']); $pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $newMachine], ['createdAt' => 'ASC']); $productLinks = $this->machineProductLinkRepository->findBy(['machine' => $newMachine], ['createdAt' => 'ASC']); return $this->json($this->normalizeStructureResponse( $newMachine, $componentLinks, $pieceLinks, $productLinks ), 201); } private function cloneCustomFields(Machine $source, Machine $target): void { $cfMap = []; 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->setMachineContextOnly($cf->isMachineContextOnly()); $newCf->setMachine($target); $this->entityManager->persist($newCf); $cfMap[$cf->getId()] = $newCf; } foreach ($source->getCustomFieldValues() as $cfv) { $originalCf = $cfv->getCustomField(); $newCf = $cfMap[$originalCf->getId()] ?? null; if (!$newCf) { continue; } $newValue = new CustomFieldValue(); $newValue->setMachine($target); $newValue->setCustomField($newCf); $newValue->setValue($cfv->getValue()); $this->entityManager->persist($newValue); } } /** * @return array Map of old link ID → new link */ private function cloneComponentLinks(Machine $source, Machine $target): array { $sourceLinks = $this->machineComponentLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']); $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 $componentLinkMap * * @return array Map of old link ID → new link */ private function clonePieceLinks(Machine $source, Machine $target, array $componentLinkMap): array { $sourceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']); $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 $componentLinkMap * @param array $pieceLinkMap */ private function cloneProductLinks( Machine $source, Machine $target, array $componentLinkMap, array $pieceLinkMap, ): void { $sourceLinks = $this->machineProductLinkRepository->findBy(['machine' => $source], ['createdAt' => 'ASC']); $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()]); } } } /** * @param array $componentLinkMap * @param array $pieceLinkMap */ private function cloneContextFieldValues( array $componentLinkMap, array $pieceLinkMap, ): void { foreach ($componentLinkMap as $oldLinkId => $newLink) { $oldLink = $this->machineComponentLinkRepository->find($oldLinkId); if (!$oldLink) { continue; } foreach ($oldLink->getContextFieldValues() as $cfv) { $newValue = new CustomFieldValue(); $newValue->setCustomField($cfv->getCustomField()); $newValue->setValue($cfv->getValue()); $newValue->setMachineComponentLink($newLink); $newValue->setComposant($newLink->getComposant()); $this->entityManager->persist($newValue); } } foreach ($pieceLinkMap as $oldLinkId => $newLink) { $oldLink = $this->machinePieceLinkRepository->find($oldLinkId); if (!$oldLink) { continue; } foreach ($oldLink->getContextFieldValues() as $cfv) { $newValue = new CustomFieldValue(); $newValue->setCustomField($cfv->getCustomField()); $newValue->setValue($cfv->getValue()); $newValue->setMachinePieceLink($newLink); $newValue->setPiece($newLink->getPiece()); $this->entityManager->persist($newValue); } } } private function normalizePayloadList(mixed $value): array { if (!is_array($value)) { return []; } return array_values(array_filter($value, static fn ($item) => is_array($item))); } private function applyComponentLinks(Machine $machine, array $payload): array|JsonResponse { $existing = $this->indexLinksById($this->machineComponentLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC'])); $keepIds = []; $pendingParents = []; $links = []; foreach ($payload as $entry) { $linkId = $this->resolveIdentifier($entry, ['id', 'linkId']); $link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineComponentLink(); if (!$linkId) { $linkId = $this->generateCuid(); } if (!$link->getId()) { $link->setId($linkId); } $composantId = $this->resolveIdentifier($entry, ['composantId', 'componentId', 'idComposant']); if (!$composantId) { return $this->json(['success' => false, 'error' => 'Composant requis.'], 400); } $composant = $this->composantRepository->find($composantId); if (!$composant instanceof Composant) { return $this->json(['success' => false, 'error' => 'Composant introuvable.'], 404); } $link->setMachine($machine); $link->setComposant($composant); $this->applyOverrides($link, $entry['overrides'] ?? null); $pendingParents[$linkId] = $this->resolveIdentifier($entry, [ 'parentComponentLinkId', 'parentLinkId', 'parentMachineComponentLinkId', ]); $this->entityManager->persist($link); $links[$linkId] = $link; $keepIds[] = $linkId; } foreach ($pendingParents as $linkId => $parentId) { if (!$parentId || !isset($links[$linkId])) { continue; } $parent = $links[$parentId] ?? $existing[$parentId] ?? null; if ($parent instanceof MachineComponentLink) { $links[$linkId]->setParentLink($parent); } } $this->removeMissingLinks($existing, $keepIds); return array_values($links); } private function applyPieceLinks(Machine $machine, array $payload, array $componentLinks): array|JsonResponse { $existing = $this->indexLinksById($this->machinePieceLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC'])); $componentIndex = $this->indexLinksById($componentLinks); $keepIds = []; $pendingParents = []; $links = []; foreach ($payload as $entry) { $linkId = $this->resolveIdentifier($entry, ['id', 'linkId']); $link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachinePieceLink(); if (!$linkId) { $linkId = $this->generateCuid(); } if (!$link->getId()) { $link->setId($linkId); } $pieceId = $this->resolveIdentifier($entry, ['pieceId']); if (!$pieceId) { return $this->json(['success' => false, 'error' => 'Pièce requise.'], 400); } $piece = $this->pieceRepository->find($pieceId); if (!$piece instanceof Piece) { return $this->json(['success' => false, 'error' => 'Pièce introuvable.'], 404); } $link->setMachine($machine); $link->setPiece($piece); $this->applyOverrides($link, $entry['overrides'] ?? null); if (!isset($entry['parentComponentLinkId']) && !isset($entry['parentLinkId'])) { $quantity = isset($entry['quantity']) ? (int) $entry['quantity'] : $link->getQuantity(); $link->setQuantity(max(1, $quantity)); } $pendingParents[$linkId] = $this->resolveIdentifier($entry, [ 'parentComponentLinkId', 'parentLinkId', 'parentMachineComponentLinkId', ]); $this->entityManager->persist($link); $links[$linkId] = $link; $keepIds[] = $linkId; } foreach ($pendingParents as $linkId => $parentId) { if (!$parentId || !isset($links[$linkId])) { continue; } $parent = $componentIndex[$parentId] ?? null; if ($parent instanceof MachineComponentLink) { $links[$linkId]->setParentLink($parent); } } $this->removeMissingLinks($existing, $keepIds); return array_values($links); } private function applyProductLinks( Machine $machine, array $payload, array $componentLinks, array $pieceLinks, ): array|JsonResponse { $existing = $this->indexLinksById($this->machineProductLinkRepository->findBy(['machine' => $machine], ['createdAt' => 'ASC'])); $componentIndex = $this->indexLinksById($componentLinks); $pieceIndex = $this->indexLinksById($pieceLinks); $keepIds = []; $pendingParents = []; $links = []; foreach ($payload as $entry) { $linkId = $this->resolveIdentifier($entry, ['id', 'linkId']); $link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineProductLink(); if (!$linkId) { $linkId = $this->generateCuid(); } if (!$link->getId()) { $link->setId($linkId); } $productId = $this->resolveIdentifier($entry, ['productId']); if (!$productId) { return $this->json(['success' => false, 'error' => 'Produit requis.'], 400); } $product = $this->productRepository->find($productId); if (!$product instanceof Product) { return $this->json(['success' => false, 'error' => 'Produit introuvable.'], 404); } $link->setMachine($machine); $link->setProduct($product); $pendingParents[$linkId] = [ 'parentComponentLinkId' => $this->resolveIdentifier($entry, ['parentComponentLinkId']), 'parentPieceLinkId' => $this->resolveIdentifier($entry, ['parentPieceLinkId']), 'parentLinkId' => $this->resolveIdentifier($entry, ['parentLinkId']), ]; $this->entityManager->persist($link); $links[$linkId] = $link; $keepIds[] = $linkId; } foreach ($pendingParents as $linkId => $parentIds) { if (!isset($links[$linkId])) { continue; } if (!empty($parentIds['parentComponentLinkId']) && isset($componentIndex[$parentIds['parentComponentLinkId']])) { $links[$linkId]->setParentComponentLink($componentIndex[$parentIds['parentComponentLinkId']]); } if (!empty($parentIds['parentPieceLinkId']) && isset($pieceIndex[$parentIds['parentPieceLinkId']])) { $links[$linkId]->setParentPieceLink($pieceIndex[$parentIds['parentPieceLinkId']]); } if (!empty($parentIds['parentLinkId']) && isset($links[$parentIds['parentLinkId']])) { $links[$linkId]->setParentLink($links[$parentIds['parentLinkId']]); } } $this->removeMissingLinks($existing, $keepIds); return array_values($links); } private function normalizeStructureResponse( Machine $machine, array $componentLinks, array $pieceLinks, array $productLinks, ): array { $normalizedComponentLinks = $this->normalizeComponentLinks($componentLinks); $componentIndex = $this->indexNormalizedLinks($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'] ?? $child['linkId'] ?? null; if ($childId) { foreach ($pieceLinks as $pieceLink) { $parentId = $pieceLink['parentComponentLinkId'] ?? null; if ($parentId === $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->normalizeConstructeurLinks($machine->getConstructeurLinks()), 'customFields' => $this->normalizeCustomFields($machine->getCustomFields()), 'documents' => null, 'customFieldValues' => $this->normalizeCustomFieldValues($machine->getCustomFieldValues()), ]; } 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(), 'machineContextOnly' => $customField->isMachineContextOnly(), ]; } return $items; } private function normalizeComponentLinks(array $links): array { return array_map(function (MachineComponentLink $link): array { $composant = $link->getComposant(); $modelType = $link->getModelType(); $parentLink = $link->getParentLink(); $type = $composant?->getTypeComposant(); return [ 'id' => $link->getId(), 'linkId' => $link->getId(), 'machineId' => $link->getMachine()->getId(), 'composantId' => $composant?->getId(), 'composant' => $composant ? $this->normalizeComposant($composant) : null, 'modelTypeId' => $modelType?->getId(), 'modelType' => $modelType ? $this->normalizeModelType($modelType) : null, 'pendingEntity' => null === $composant, 'parentLinkId' => $parentLink?->getId(), 'parentComponentLinkId' => $parentLink?->getId(), 'parentComponentId' => $parentLink?->getComposant()?->getId(), 'overrides' => $this->normalizeOverrides($link), 'childLinks' => [], 'pieceLinks' => [], 'contextCustomFields' => $type ? $this->normalizeContextCustomFieldDefinitions($type->getComponentCustomFields()) : [], 'contextCustomFieldValues' => $this->normalizeCustomFieldValues($link->getContextFieldValues()), ]; }, $links); } private function normalizePieceLinks(array $links): array { return array_map(function (MachinePieceLink $link): array { $piece = $link->getPiece(); $modelType = $link->getModelType(); $parentLink = $link->getParentLink(); $type = $piece?->getTypePiece(); return [ 'id' => $link->getId(), 'linkId' => $link->getId(), 'machineId' => $link->getMachine()->getId(), 'pieceId' => $piece?->getId(), 'piece' => $piece ? $this->normalizePiece($piece) : null, 'modelTypeId' => $modelType?->getId(), 'modelType' => $modelType ? $this->normalizeModelType($modelType) : null, 'pendingEntity' => null === $piece, 'parentLinkId' => $parentLink?->getId(), 'parentComponentLinkId' => $parentLink?->getId(), 'parentComponentId' => $parentLink?->getComposant()?->getId(), 'overrides' => $this->normalizeOverrides($link), 'quantity' => $piece ? $this->resolvePieceQuantity($link) : 1, 'contextCustomFields' => $type ? $this->normalizeContextCustomFieldDefinitions($type->getPieceCustomFields()) : [], 'contextCustomFieldValues' => $this->normalizeCustomFieldValues($link->getContextFieldValues()), ]; }, $links); } private function resolvePieceQuantity(MachinePieceLink $link): int { $parentLink = $link->getParentLink(); $piece = $link->getPiece(); if (!$parentLink || !$piece) { return $link->getQuantity(); } $composant = $parentLink->getComposant(); if (!$composant) { return $link->getQuantity(); } foreach ($composant->getPieceSlots() as $slot) { if ($slot->getSelectedPiece()?->getId() === $piece->getId()) { return $slot->getQuantity(); } } return $link->getQuantity(); } private function normalizeProductLinks(array $links): array { return array_map(function (MachineProductLink $link): array { $product = $link->getProduct(); $modelType = $link->getModelType(); return [ 'id' => $link->getId(), 'linkId' => $link->getId(), 'machineId' => $link->getMachine()->getId(), 'productId' => $product?->getId(), 'product' => $product ? $this->normalizeProduct($product) : null, 'modelTypeId' => $modelType?->getId(), 'modelType' => $modelType ? $this->normalizeModelType($modelType) : null, 'pendingEntity' => null === $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->normalizeConstructeurLinks($composant->getConstructeurLinks()), 'documents' => [], '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(), 'typePiece' => $this->normalizeModelType($slot->getTypePiece()), '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->normalizeConstructeurLinks($piece->getConstructeurLinks()), 'documents' => [], '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->normalizeConstructeurLinks($product->getConstructeurLinks()), 'documents' => [], '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 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; } 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(), 'machineContextOnly' => $cf->isMachineContextOnly(), ]; } 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(), 'machineContextOnly' => $cf->isMachineContextOnly(), ], ]; } return $items; } private function normalizeContextCustomFieldDefinitions(Collection $customFields): array { $items = []; foreach ($customFields as $cf) { if (!$cf instanceof CustomField || !$cf->isMachineContextOnly()) { continue; } $items[] = [ 'id' => $cf->getId(), 'name' => $cf->getName(), 'type' => $cf->getType(), 'required' => $cf->isRequired(), 'options' => $cf->getOptions(), 'defaultValue' => $cf->getDefaultValue(), 'orderIndex' => $cf->getOrderIndex(), 'machineContextOnly' => true, ]; } usort($items, static fn (array $a, array $b) => $a['orderIndex'] <=> $b['orderIndex']); 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 applyOverrides(object $link, mixed $overrides): void { if (!is_array($overrides)) { return; } if (array_key_exists('name', $overrides) && method_exists($link, 'setNameOverride')) { $link->setNameOverride($this->stringOrNull($overrides['name'])); } if (array_key_exists('reference', $overrides) && method_exists($link, 'setReferenceOverride')) { $link->setReferenceOverride($this->stringOrNull($overrides['reference'])); } if (array_key_exists('prix', $overrides) && method_exists($link, 'setPrixOverride')) { $link->setPrixOverride($this->stringOrNull($overrides['prix'])); } } private function stringOrNull(mixed $value): ?string { if (null === $value) { return null; } $string = trim((string) $value); return '' === $string ? null : $string; } private function resolveIdentifier(array $entry, array $keys): ?string { foreach ($keys as $key) { if (!array_key_exists($key, $entry)) { continue; } $value = $entry[$key]; if (null === $value || '' === $value) { continue; } return (string) $value; } return null; } /** * @param array $links * * @return array */ private function indexLinksById(array $links): array { $indexed = []; foreach ($links as $link) { if (method_exists($link, 'getId') && $link->getId()) { $indexed[$link->getId()] = $link; } } return $indexed; } private function indexNormalizedLinks(array $links): array { $indexed = []; foreach ($links as $link) { if (is_array($link) && isset($link['id'])) { $indexed[$link['id']] = $link; } } return $indexed; } private function removeMissingLinks(array $existing, array $keepIds): void { $keep = array_flip($keepIds); foreach ($existing as $link) { if (!method_exists($link, 'getId')) { continue; } $id = $link->getId(); if ($id && !isset($keep[$id])) { $this->entityManager->remove($link); } } } private function generateCuid(): string { return 'cl'.bin2hex(random_bytes(12)); } }