From cd2a3fac5590958dd56c9d81f3bcde085f522bbf Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 12 Feb 2026 14:27:07 +0100 Subject: [PATCH] feat(categories) : add bidirectional piece/component category conversion Backend service and controller for converting piece categories to component categories (and vice-versa). Uses raw SQL in a transaction to preserve IDs and transfer all related data (documents, custom fields, constructeurs). Includes php-cs-fixer formatting pass on existing controllers/entities. Co-Authored-By: Claude Opus 4.6 --- Inventory_frontend | 2 +- src/Controller/ComposantHistoryController.php | 21 +- src/Controller/CustomFieldValueController.php | 63 +-- src/Controller/DocumentQueryController.php | 27 +- .../MachineCustomFieldsController.php | 13 +- src/Controller/MachineSkeletonController.php | 298 ++++++------- .../ModelTypeConversionController.php | 55 +++ src/Controller/PieceHistoryController.php | 21 +- src/Controller/ProductHistoryController.php | 21 +- src/Controller/SessionProfileController.php | 27 +- src/Controller/SessionProfilesController.php | 18 +- .../QuoteStrategy/AlwaysQuoteStrategy.php | 15 +- src/Entity/AuditLog.php | 12 +- src/Entity/MachineComponentLink.php | 25 +- src/Entity/MachinePieceLink.php | 21 +- src/Entity/MachineProductLink.php | 21 +- .../TypeMachineComponentRequirement.php | 21 +- src/Entity/TypeMachinePieceRequirement.php | 21 +- src/Entity/TypeMachineProductRequirement.php | 21 +- src/Enum/ModelCategory.php | 4 +- .../PieceProductSyncSubscriber.php | 9 +- .../UniqueConstraintSubscriber.php | 7 +- .../ModelTypeCategoryConversionService.php | 418 ++++++++++++++++++ 23 files changed, 821 insertions(+), 340 deletions(-) create mode 100644 src/Controller/ModelTypeConversionController.php create mode 100644 src/Service/ModelTypeCategoryConversionService.php diff --git a/Inventory_frontend b/Inventory_frontend index 5cab154..c9054e5 160000 --- a/Inventory_frontend +++ b/Inventory_frontend @@ -1 +1 @@ -Subproject commit 5cab15422dd7b873801eb8b05f82c94a5d213787 +Subproject commit c9054e5b4dd1c5d6ebe112652a8b9a97f22dbf96 diff --git a/src/Controller/ComposantHistoryController.php b/src/Controller/ComposantHistoryController.php index c221198..2552a36 100644 --- a/src/Controller/ComposantHistoryController.php +++ b/src/Controller/ComposantHistoryController.php @@ -7,6 +7,7 @@ namespace App\Controller; use App\Repository\AuditLogRepository; use App\Repository\ComposantRepository; use App\Repository\ProfileRepository; +use DateTimeInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -17,8 +18,7 @@ final class ComposantHistoryController private readonly ComposantRepository $components, private readonly AuditLogRepository $auditLogs, private readonly ProfileRepository $profiles, - ) { - } + ) {} #[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])] public function __invoke(string $id): JsonResponse @@ -39,11 +39,11 @@ final class ComposantHistoryController )))); $actorMap = []; - if ($actorIds !== []) { + if ([] !== $actorIds) { $profiles = $this->profiles->findBy(['id' => $actorIds]); foreach ($profiles as $profile) { $label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName())); - if ($label === '') { + if ('' === $label) { $label = $profile->getEmail() ?? $profile->getId(); } $actorMap[$profile->getId()] = $label; @@ -55,16 +55,16 @@ final class ComposantHistoryController $actorId = $log->getActorProfileId(); return [ - 'id' => $log->getId(), - 'action' => $log->getAction(), - 'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM), - 'actor' => $actorId + 'id' => $log->getId(), + 'action' => $log->getAction(), + 'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM), + 'actor' => $actorId ? [ - 'id' => $actorId, + 'id' => $actorId, 'label' => $actorMap[$actorId] ?? $actorId, ] : null, - 'diff' => $log->getDiff(), + 'diff' => $log->getDiff(), 'snapshot' => $log->getSnapshot(), ]; }, @@ -77,4 +77,3 @@ final class ComposantHistoryController ]); } } - diff --git a/src/Controller/CustomFieldValueController.php b/src/Controller/CustomFieldValueController.php index 5573876..13388bf 100644 --- a/src/Controller/CustomFieldValueController.php +++ b/src/Controller/CustomFieldValueController.php @@ -29,8 +29,7 @@ class CustomFieldValueController extends AbstractController private readonly ComposantRepository $composantRepository, private readonly PieceRepository $pieceRepository, private readonly ProductRepository $productRepository, - ) { - } + ) {} #[Route('', name: 'custom_field_values_create', methods: ['POST'])] public function create(Request $request): JsonResponse @@ -80,7 +79,7 @@ class CustomFieldValueController extends AbstractController } $existing = $this->customFieldValueRepository->findOneBy([ - 'customField' => $customField, + 'customField' => $customField, $target['type'] => $target['entity'], ]); @@ -107,7 +106,7 @@ class CustomFieldValueController extends AbstractController { $target = $this->resolveTarget([ 'entityType' => $entityType, - 'entityId' => $entityId, + 'entityId' => $entityId, ]); if ($target instanceof JsonResponse) { @@ -173,7 +172,7 @@ class CustomFieldValueController extends AbstractController private function resolveCustomField(array $payload): CustomField|JsonResponse { $customFieldId = isset($payload['customFieldId']) ? trim((string) $payload['customFieldId']) : ''; - if ($customFieldId !== '') { + if ('' !== $customFieldId) { $customField = $this->customFieldRepository->find($customFieldId); if ($customField instanceof CustomField) { return $customField; @@ -183,7 +182,7 @@ class CustomFieldValueController extends AbstractController } $customFieldName = isset($payload['customFieldName']) ? trim((string) $payload['customFieldName']) : ''; - if ($customFieldName === '') { + if ('' === $customFieldName) { return $this->json(['success' => false, 'error' => 'customFieldId or customFieldName is required.'], 400); } @@ -205,30 +204,31 @@ class CustomFieldValueController extends AbstractController private function resolveTarget(array $payload): array|JsonResponse { $entityType = isset($payload['entityType']) ? strtolower((string) $payload['entityType']) : ''; - $entityId = isset($payload['entityId']) ? trim((string) $payload['entityId']) : ''; + $entityId = isset($payload['entityId']) ? trim((string) $payload['entityId']) : ''; - if ($entityType === '' || $entityId === '') { + if ('' === $entityType || '' === $entityId) { foreach (['machine', 'composant', 'piece', 'product'] as $candidate) { - $key = $candidate . 'Id'; + $key = $candidate.'Id'; if (!isset($payload[$key])) { continue; } $entityType = $candidate; - $entityId = trim((string) $payload[$key]); + $entityId = trim((string) $payload[$key]); + break; } } - if ($entityType === '' || $entityId === '') { + if ('' === $entityType || '' === $entityId) { return $this->json(['success' => false, 'error' => 'Entity target is missing.'], 400); } return match ($entityType) { - 'machine' => $this->resolveEntity('machine', $entityId, $this->machineRepository), + 'machine' => $this->resolveEntity('machine', $entityId, $this->machineRepository), 'composant' => $this->resolveEntity('composant', $entityId, $this->composantRepository), - 'piece' => $this->resolveEntity('piece', $entityId, $this->pieceRepository), - 'product' => $this->resolveEntity('product', $entityId, $this->productRepository), - default => $this->json(['success' => false, 'error' => 'Unsupported entity type.'], 400), + 'piece' => $this->resolveEntity('piece', $entityId, $this->pieceRepository), + 'product' => $this->resolveEntity('product', $entityId, $this->productRepository), + default => $this->json(['success' => false, 'error' => 'Unsupported entity type.'], 400), }; } @@ -247,15 +247,22 @@ class CustomFieldValueController extends AbstractController switch ($type) { case 'machine': $value->setMachine($entity); + break; + case 'composant': $value->setComposant($entity); + break; + case 'piece': $value->setPiece($entity); + break; + case 'product': $value->setProduct($entity); + break; } } @@ -265,23 +272,23 @@ class CustomFieldValueController extends AbstractController $customField = $value->getCustomField(); return [ - 'id' => $value->getId(), - 'value' => $value->getValue(), + 'id' => $value->getId(), + 'value' => $value->getValue(), 'customFieldId' => $customField->getId(), - 'customField' => [ - 'id' => $customField->getId(), - 'name' => $customField->getName(), - 'type' => $customField->getType(), - 'required' => $customField->isRequired(), - 'options' => $customField->getOptions(), + 'customField' => [ + 'id' => $customField->getId(), + 'name' => $customField->getName(), + 'type' => $customField->getType(), + 'required' => $customField->isRequired(), + 'options' => $customField->getOptions(), 'orderIndex' => $customField->getOrderIndex(), ], - 'machineId' => $value->getMachine()?->getId(), + 'machineId' => $value->getMachine()?->getId(), 'composantId' => $value->getComposant()?->getId(), - 'pieceId' => $value->getPiece()?->getId(), - 'productId' => $value->getProduct()?->getId(), - 'createdAt' => $value->getCreatedAt()->format(DATE_ATOM), - 'updatedAt' => $value->getUpdatedAt()->format(DATE_ATOM), + 'pieceId' => $value->getPiece()?->getId(), + 'productId' => $value->getProduct()?->getId(), + 'createdAt' => $value->getCreatedAt()->format(DATE_ATOM), + 'updatedAt' => $value->getUpdatedAt()->format(DATE_ATOM), ]; } } diff --git a/src/Controller/DocumentQueryController.php b/src/Controller/DocumentQueryController.php index 833e888..660dbd3 100644 --- a/src/Controller/DocumentQueryController.php +++ b/src/Controller/DocumentQueryController.php @@ -25,8 +25,7 @@ class DocumentQueryController extends AbstractController private readonly ComposantRepository $composantRepository, private readonly PieceRepository $pieceRepository, private readonly ProductRepository $productRepository, - ) { - } + ) {} #[Route('/site/{id}', name: 'documents_by_site', methods: ['GET'])] public function listBySite(string $id): JsonResponse @@ -100,19 +99,19 @@ class DocumentQueryController extends AbstractController { return array_map(static function (Document $document): array { return [ - 'id' => $document->getId(), - 'name' => $document->getName(), - 'filename' => $document->getFilename(), - 'path' => $document->getPath(), - 'mimeType' => $document->getMimeType(), - 'size' => $document->getSize(), - 'siteId' => $document->getSite()?->getId(), - 'machineId' => $document->getMachine()?->getId(), + 'id' => $document->getId(), + 'name' => $document->getName(), + 'filename' => $document->getFilename(), + 'path' => $document->getPath(), + 'mimeType' => $document->getMimeType(), + 'size' => $document->getSize(), + 'siteId' => $document->getSite()?->getId(), + 'machineId' => $document->getMachine()?->getId(), 'composantId' => $document->getComposant()?->getId(), - 'pieceId' => $document->getPiece()?->getId(), - 'productId' => $document->getProduct()?->getId(), - 'createdAt' => $document->getCreatedAt()->format(DATE_ATOM), - 'updatedAt' => $document->getUpdatedAt()->format(DATE_ATOM), + 'pieceId' => $document->getPiece()?->getId(), + 'productId' => $document->getProduct()?->getId(), + 'createdAt' => $document->getCreatedAt()->format(DATE_ATOM), + 'updatedAt' => $document->getUpdatedAt()->format(DATE_ATOM), ]; }, $documents); } diff --git a/src/Controller/MachineCustomFieldsController.php b/src/Controller/MachineCustomFieldsController.php index 734428c..fea3b7f 100644 --- a/src/Controller/MachineCustomFieldsController.php +++ b/src/Controller/MachineCustomFieldsController.php @@ -21,8 +21,7 @@ class MachineCustomFieldsController extends AbstractController private readonly EntityManagerInterface $entityManager, private readonly MachineRepository $machineRepository, private readonly CustomFieldValueRepository $customFieldValueRepository, - ) { - } + ) {} #[Route('/{id}/add-custom-fields', name: 'machine_add_custom_fields', methods: ['POST'])] public function addMissingCustomFields(string $id): JsonResponse @@ -42,7 +41,7 @@ class MachineCustomFieldsController extends AbstractController continue; } $existing = $this->customFieldValueRepository->findOneBy([ - 'machine' => $machine, + 'machine' => $machine, 'customField' => $customField, ]); if ($existing instanceof CustomFieldValue) { @@ -61,12 +60,12 @@ class MachineCustomFieldsController extends AbstractController $values = $this->customFieldValueRepository->findBy(['machine' => $machine]); return $this->json([ - 'success' => true, - 'machineId' => $machine->getId(), + 'success' => true, + 'machineId' => $machine->getId(), 'customFieldValues' => array_map( static fn (CustomFieldValue $value) => [ - 'id' => $value->getId(), - 'value' => $value->getValue(), + 'id' => $value->getId(), + 'value' => $value->getValue(), 'customFieldId' => $value->getCustomField()->getId(), ], $values diff --git a/src/Controller/MachineSkeletonController.php b/src/Controller/MachineSkeletonController.php index f38a9ba..edfd680 100644 --- a/src/Controller/MachineSkeletonController.php +++ b/src/Controller/MachineSkeletonController.php @@ -47,8 +47,7 @@ class MachineSkeletonController extends AbstractController private readonly TypeMachineComponentRequirementRepository $componentRequirementRepository, private readonly TypeMachinePieceRequirementRepository $pieceRequirementRepository, private readonly TypeMachineProductRequirementRepository $productRequirementRepository, - ) { - } + ) {} #[Route('/{id}/skeleton', name: 'machine_skeleton_get', methods: ['GET'])] public function getSkeleton(string $id): JsonResponse @@ -59,8 +58,8 @@ class MachineSkeletonController extends AbstractController } $componentLinks = $this->machineComponentLinkRepository->findBy(['machine' => $machine]); - $pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $machine]); - $productLinks = $this->machineProductLinkRepository->findBy(['machine' => $machine]); + $pieceLinks = $this->machinePieceLinkRepository->findBy(['machine' => $machine]); + $productLinks = $this->machineProductLinkRepository->findBy(['machine' => $machine]); return $this->json($this->normalizeMachineSkeletonResponse( $machine, @@ -84,8 +83,8 @@ class MachineSkeletonController extends AbstractController } $componentLinksPayload = $this->normalizePayloadList($payload['componentLinks'] ?? []); - $pieceLinksPayload = $this->normalizePayloadList($payload['pieceLinks'] ?? []); - $productLinksPayload = $this->normalizePayloadList($payload['productLinks'] ?? []); + $pieceLinksPayload = $this->normalizePayloadList($payload['pieceLinks'] ?? []); + $productLinksPayload = $this->normalizePayloadList($payload['productLinks'] ?? []); $componentLinks = $this->applyComponentLinks($machine, $componentLinksPayload); if ($componentLinks instanceof JsonResponse) { @@ -117,19 +116,20 @@ class MachineSkeletonController extends AbstractController 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])); - $keepIds = []; + $existing = $this->indexLinksById($this->machineComponentLinkRepository->findBy(['machine' => $machine])); + $keepIds = []; $pendingParents = []; - $links = []; + $links = []; foreach ($payload as $entry) { $linkId = $this->resolveIdentifier($entry, ['id', 'linkId']); - $link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineComponentLink(); + $link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineComponentLink(); if (!$linkId) { $linkId = $this->generateCuid(); } @@ -167,7 +167,7 @@ class MachineSkeletonController extends AbstractController $this->entityManager->persist($link); $links[$linkId] = $link; - $keepIds[] = $linkId; + $keepIds[] = $linkId; } foreach ($pendingParents as $linkId => $parentId) { @@ -190,15 +190,15 @@ class MachineSkeletonController extends AbstractController private function applyPieceLinks(Machine $machine, array $payload, array $componentLinks): array|JsonResponse { - $existing = $this->indexLinksById($this->machinePieceLinkRepository->findBy(['machine' => $machine])); + $existing = $this->indexLinksById($this->machinePieceLinkRepository->findBy(['machine' => $machine])); $componentIndex = $this->indexLinksById($componentLinks); - $keepIds = []; + $keepIds = []; $pendingParents = []; - $links = []; + $links = []; foreach ($payload as $entry) { $linkId = $this->resolveIdentifier($entry, ['id', 'linkId']); - $link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachinePieceLink(); + $link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachinePieceLink(); if (!$linkId) { $linkId = $this->generateCuid(); } @@ -236,7 +236,7 @@ class MachineSkeletonController extends AbstractController $this->entityManager->persist($link); $links[$linkId] = $link; - $keepIds[] = $linkId; + $keepIds[] = $linkId; } foreach ($pendingParents as $linkId => $parentId) { @@ -263,16 +263,16 @@ class MachineSkeletonController extends AbstractController array $componentLinks, array $pieceLinks, ): array|JsonResponse { - $existing = $this->indexLinksById($this->machineProductLinkRepository->findBy(['machine' => $machine])); + $existing = $this->indexLinksById($this->machineProductLinkRepository->findBy(['machine' => $machine])); $componentIndex = $this->indexLinksById($componentLinks); - $pieceIndex = $this->indexLinksById($pieceLinks); - $keepIds = []; + $pieceIndex = $this->indexLinksById($pieceLinks); + $keepIds = []; $pendingParents = []; - $links = []; + $links = []; foreach ($payload as $entry) { $linkId = $this->resolveIdentifier($entry, ['id', 'linkId']); - $link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineProductLink(); + $link = $linkId && isset($existing[$linkId]) ? $existing[$linkId] : new MachineProductLink(); if (!$linkId) { $linkId = $this->generateCuid(); } @@ -302,13 +302,13 @@ class MachineSkeletonController extends AbstractController $pendingParents[$linkId] = [ 'parentComponentLinkId' => $this->resolveIdentifier($entry, ['parentComponentLinkId']), - 'parentPieceLinkId' => $this->resolveIdentifier($entry, ['parentPieceLinkId']), - 'parentLinkId' => $this->resolveIdentifier($entry, ['parentLinkId']), + 'parentPieceLinkId' => $this->resolveIdentifier($entry, ['parentPieceLinkId']), + 'parentLinkId' => $this->resolveIdentifier($entry, ['parentLinkId']), ]; $this->entityManager->persist($link); $links[$linkId] = $link; - $keepIds[] = $linkId; + $keepIds[] = $linkId; } foreach ($pendingParents as $linkId => $parentIds) { @@ -338,8 +338,8 @@ class MachineSkeletonController extends AbstractController array $productLinks, ): array { $normalizedComponentLinks = $this->normalizeComponentLinks($componentLinks); - $componentIndex = $this->indexNormalizedLinks($normalizedComponentLinks); - $normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks); + $componentIndex = $this->indexNormalizedLinks($normalizedComponentLinks); + $normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks); // Build component hierarchy foreach ($normalizedComponentLinks as &$link) { @@ -354,10 +354,10 @@ class MachineSkeletonController extends AbstractController $this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks); return [ - 'machine' => $this->normalizeMachine($machine), + 'machine' => $this->normalizeMachine($machine), 'componentLinks' => array_values($componentIndex), - 'pieceLinks' => $normalizedPieceLinks, - 'productLinks' => $this->normalizeProductLinks($productLinks), + 'pieceLinks' => $normalizedPieceLinks, + 'productLinks' => $this->normalizeProductLinks($productLinks), ]; } @@ -400,26 +400,26 @@ class MachineSkeletonController extends AbstractController private function normalizeMachine(Machine $machine): array { - $site = $machine->getSite(); + $site = $machine->getSite(); $typeMachine = $machine->getTypeMachine(); return [ - 'id' => $machine->getId(), - 'name' => $machine->getName(), + 'id' => $machine->getId(), + 'name' => $machine->getName(), 'reference' => $machine->getReference(), - 'prix' => $machine->getPrix(), - 'siteId' => $site->getId(), - 'site' => [ - 'id' => $site->getId(), + 'prix' => $machine->getPrix(), + 'siteId' => $site->getId(), + 'site' => [ + 'id' => $site->getId(), 'name' => $site->getName(), ], 'typeMachineId' => $typeMachine?->getId(), - 'typeMachine' => $typeMachine ? [ - 'id' => $typeMachine->getId(), - 'name' => $typeMachine->getName(), - 'category' => $typeMachine->getCategory(), - 'description' => $typeMachine->getDescription(), - 'customFields' => $this->normalizeCustomFields($typeMachine->getCustomFields()), + 'typeMachine' => $typeMachine ? [ + 'id' => $typeMachine->getId(), + 'name' => $typeMachine->getName(), + 'category' => $typeMachine->getCategory(), + 'description' => $typeMachine->getDescription(), + 'customFields' => $this->normalizeCustomFields($typeMachine->getCustomFields()), 'componentRequirements' => $typeMachine->getComponentRequirements() ->map(fn (TypeMachineComponentRequirement $req) => $this->normalizeComponentRequirement($req)) ->toArray(), @@ -430,8 +430,8 @@ class MachineSkeletonController extends AbstractController ->map(fn (TypeMachineProductRequirement $req) => $this->normalizeProductRequirement($req)) ->toArray(), ] : null, - 'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()), - 'documents' => null, + 'constructeurs' => $this->normalizeConstructeurs($machine->getConstructeurs()), + 'documents' => null, 'customFieldValues' => null, ]; } @@ -444,13 +444,13 @@ class MachineSkeletonController extends AbstractController continue; } $items[] = [ - 'id' => $customField->getId(), - 'name' => $customField->getName(), - 'type' => $customField->getType(), - 'required' => $customField->isRequired(), - 'options' => $customField->getOptions(), + 'id' => $customField->getId(), + 'name' => $customField->getName(), + 'type' => $customField->getType(), + 'required' => $customField->isRequired(), + 'options' => $customField->getOptions(), 'defaultValue' => $customField->getDefaultValue(), - 'orderIndex' => $customField->getOrderIndex(), + 'orderIndex' => $customField->getOrderIndex(), ]; } @@ -460,26 +460,26 @@ class MachineSkeletonController extends AbstractController private function normalizeComponentLinks(array $links): array { return array_map(function (MachineComponentLink $link): array { - $composant = $link->getComposant(); - $requirement = $link->getTypeMachineComponentRequirement(); - $parentLink = $link->getParentLink(); + $composant = $link->getComposant(); + $requirement = $link->getTypeMachineComponentRequirement(); + $parentLink = $link->getParentLink(); $parentRequirementId = $parentLink?->getTypeMachineComponentRequirement()?->getId(); return [ - 'id' => $link->getId(), - 'linkId' => $link->getId(), - 'machineId' => $link->getMachine()->getId(), - 'composantId' => $composant->getId(), - 'composant' => $this->normalizeComposant($composant), - 'typeMachineComponentRequirementId' => $requirement?->getId(), - 'typeMachineComponentRequirement' => $requirement ? $this->normalizeComponentRequirement($requirement) : null, - 'parentLinkId' => $parentLink?->getId(), - 'parentComponentLinkId' => $parentLink?->getId(), - 'parentComponentId' => $parentLink?->getComposant()->getId(), + 'id' => $link->getId(), + 'linkId' => $link->getId(), + 'machineId' => $link->getMachine()->getId(), + 'composantId' => $composant->getId(), + 'composant' => $this->normalizeComposant($composant), + 'typeMachineComponentRequirementId' => $requirement?->getId(), + 'typeMachineComponentRequirement' => $requirement ? $this->normalizeComponentRequirement($requirement) : null, + 'parentLinkId' => $parentLink?->getId(), + 'parentComponentLinkId' => $parentLink?->getId(), + 'parentComponentId' => $parentLink?->getComposant()->getId(), 'parentMachineComponentRequirementId' => $parentRequirementId, - 'overrides' => $this->normalizeOverrides($link), - 'childLinks' => [], - 'pieceLinks' => [], + 'overrides' => $this->normalizeOverrides($link), + 'childLinks' => [], + 'pieceLinks' => [], ]; }, $links); } @@ -487,24 +487,24 @@ class MachineSkeletonController extends AbstractController private function normalizePieceLinks(array $links): array { return array_map(function (MachinePieceLink $link): array { - $piece = $link->getPiece(); - $requirement = $link->getTypeMachinePieceRequirement(); - $parentLink = $link->getParentLink(); + $piece = $link->getPiece(); + $requirement = $link->getTypeMachinePieceRequirement(); + $parentLink = $link->getParentLink(); $parentRequirementId = $parentLink?->getTypeMachineComponentRequirement()?->getId(); return [ - 'id' => $link->getId(), - 'linkId' => $link->getId(), - 'machineId' => $link->getMachine()->getId(), - 'pieceId' => $piece->getId(), - 'piece' => $this->normalizePiece($piece), - 'typeMachinePieceRequirementId' => $requirement?->getId(), - 'typeMachinePieceRequirement' => $requirement ? $this->normalizePieceRequirement($requirement) : null, - 'parentLinkId' => $parentLink?->getId(), - 'parentComponentLinkId' => $parentLink?->getId(), - 'parentComponentId' => $parentLink?->getComposant()->getId(), + 'id' => $link->getId(), + 'linkId' => $link->getId(), + 'machineId' => $link->getMachine()->getId(), + 'pieceId' => $piece->getId(), + 'piece' => $this->normalizePiece($piece), + 'typeMachinePieceRequirementId' => $requirement?->getId(), + 'typeMachinePieceRequirement' => $requirement ? $this->normalizePieceRequirement($requirement) : null, + 'parentLinkId' => $parentLink?->getId(), + 'parentComponentLinkId' => $parentLink?->getId(), + 'parentComponentId' => $parentLink?->getComposant()->getId(), 'parentMachineComponentRequirementId' => $parentRequirementId, - 'overrides' => $this->normalizeOverrides($link), + 'overrides' => $this->normalizeOverrides($link), ]; }, $links); } @@ -512,20 +512,20 @@ class MachineSkeletonController extends AbstractController private function normalizeProductLinks(array $links): array { return array_map(function (MachineProductLink $link): array { - $product = $link->getProduct(); + $product = $link->getProduct(); $requirement = $link->getTypeMachineProductRequirement(); return [ - 'id' => $link->getId(), - 'linkId' => $link->getId(), - 'machineId' => $link->getMachine()->getId(), - 'productId' => $product->getId(), - 'product' => $this->normalizeProduct($product), + 'id' => $link->getId(), + 'linkId' => $link->getId(), + 'machineId' => $link->getMachine()->getId(), + 'productId' => $product->getId(), + 'product' => $this->normalizeProduct($product), 'typeMachineProductRequirementId' => $requirement?->getId(), - 'typeMachineProductRequirement' => $requirement ? $this->normalizeProductRequirement($requirement) : null, - 'parentLinkId' => $link->getParentLink()?->getId(), - 'parentComponentLinkId' => $link->getParentComponentLink()?->getId(), - 'parentPieceLinkId' => $link->getParentPieceLink()?->getId(), + 'typeMachineProductRequirement' => $requirement ? $this->normalizeProductRequirement($requirement) : null, + 'parentLinkId' => $link->getParentLink()?->getId(), + 'parentComponentLinkId' => $link->getParentComponentLink()?->getId(), + 'parentPieceLinkId' => $link->getParentPieceLink()?->getId(), ]; }, $links); } @@ -533,49 +533,49 @@ class MachineSkeletonController extends AbstractController private function normalizeComposant(Composant $composant): array { return [ - 'id' => $composant->getId(), - 'name' => $composant->getName(), - 'reference' => $composant->getReference(), - 'prix' => $composant->getPrix(), + 'id' => $composant->getId(), + 'name' => $composant->getName(), + 'reference' => $composant->getReference(), + 'prix' => $composant->getPrix(), 'typeComposantId' => $composant->getTypeComposant()?->getId(), - 'typeComposant' => $this->normalizeModelType($composant->getTypeComposant()), - 'productId' => $composant->getProduct()?->getId(), - 'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null, - 'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()), - 'documents' => [], - 'customFields' => [], + 'typeComposant' => $this->normalizeModelType($composant->getTypeComposant()), + 'productId' => $composant->getProduct()?->getId(), + 'product' => $composant->getProduct() ? $this->normalizeProduct($composant->getProduct()) : null, + 'constructeurs' => $this->normalizeConstructeurs($composant->getConstructeurs()), + 'documents' => [], + 'customFields' => [], ]; } private function normalizePiece(Piece $piece): array { return [ - 'id' => $piece->getId(), - 'name' => $piece->getName(), - 'reference' => $piece->getReference(), - 'prix' => $piece->getPrix(), - 'typePieceId' => $piece->getTypePiece()?->getId(), - 'typePiece' => $this->normalizeModelType($piece->getTypePiece()), - 'productId' => $piece->getProduct()?->getId(), - 'product' => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null, + 'id' => $piece->getId(), + 'name' => $piece->getName(), + 'reference' => $piece->getReference(), + 'prix' => $piece->getPrix(), + 'typePieceId' => $piece->getTypePiece()?->getId(), + 'typePiece' => $this->normalizeModelType($piece->getTypePiece()), + 'productId' => $piece->getProduct()?->getId(), + 'product' => $piece->getProduct() ? $this->normalizeProduct($piece->getProduct()) : null, 'constructeurs' => $this->normalizeConstructeurs($piece->getConstructeurs()), - 'documents' => [], - 'customFields' => [], + 'documents' => [], + 'customFields' => [], ]; } private function normalizeProduct(Product $product): array { return [ - 'id' => $product->getId(), - 'name' => $product->getName(), - 'reference' => $product->getReference(), + 'id' => $product->getId(), + 'name' => $product->getName(), + 'reference' => $product->getReference(), 'supplierPrice' => $product->getSupplierPrice(), 'typeProductId' => $product->getTypeProduct()?->getId(), - 'typeProduct' => $this->normalizeModelType($product->getTypeProduct()), + 'typeProduct' => $this->normalizeModelType($product->getTypeProduct()), 'constructeurs' => $this->normalizeConstructeurs($product->getConstructeurs()), - 'documents' => [], - 'customFields' => [], + 'documents' => [], + 'customFields' => [], ]; } @@ -586,9 +586,9 @@ class MachineSkeletonController extends AbstractController } return [ - 'id' => $type->getId(), - 'name' => $type->getName(), - 'code' => $type->getCode(), + 'id' => $type->getId(), + 'name' => $type->getName(), + 'code' => $type->getCode(), 'category' => $type->getCategory()->value, ]; } @@ -596,39 +596,39 @@ class MachineSkeletonController extends AbstractController private function normalizeComponentRequirement(TypeMachineComponentRequirement $requirement): array { return [ - 'id' => $requirement->getId(), - 'label' => $requirement->getLabel(), - 'minCount' => $requirement->getMinCount(), - 'maxCount' => $requirement->getMaxCount(), - 'required' => $requirement->isRequired(), + 'id' => $requirement->getId(), + 'label' => $requirement->getLabel(), + 'minCount' => $requirement->getMinCount(), + 'maxCount' => $requirement->getMaxCount(), + 'required' => $requirement->isRequired(), 'typeComposantId' => $requirement->getTypeComposant()->getId(), - 'typeComposant' => $this->normalizeModelType($requirement->getTypeComposant()), + 'typeComposant' => $this->normalizeModelType($requirement->getTypeComposant()), ]; } private function normalizePieceRequirement(TypeMachinePieceRequirement $requirement): array { return [ - 'id' => $requirement->getId(), - 'label' => $requirement->getLabel(), - 'minCount' => $requirement->getMinCount(), - 'maxCount' => $requirement->getMaxCount(), - 'required' => $requirement->isRequired(), + 'id' => $requirement->getId(), + 'label' => $requirement->getLabel(), + 'minCount' => $requirement->getMinCount(), + 'maxCount' => $requirement->getMaxCount(), + 'required' => $requirement->isRequired(), 'typePieceId' => $requirement->getTypePiece()->getId(), - 'typePiece' => $this->normalizeModelType($requirement->getTypePiece()), + 'typePiece' => $this->normalizeModelType($requirement->getTypePiece()), ]; } private function normalizeProductRequirement(TypeMachineProductRequirement $requirement): array { return [ - 'id' => $requirement->getId(), - 'label' => $requirement->getLabel(), - 'minCount' => $requirement->getMinCount(), - 'maxCount' => $requirement->getMaxCount(), - 'required' => $requirement->isRequired(), + 'id' => $requirement->getId(), + 'label' => $requirement->getLabel(), + 'minCount' => $requirement->getMinCount(), + 'maxCount' => $requirement->getMaxCount(), + 'required' => $requirement->isRequired(), 'typeProductId' => $requirement->getTypeProduct()->getId(), - 'typeProduct' => $this->normalizeModelType($requirement->getTypeProduct()), + 'typeProduct' => $this->normalizeModelType($requirement->getTypeProduct()), ]; } @@ -637,8 +637,8 @@ class MachineSkeletonController extends AbstractController $items = []; foreach ($constructeurs as $constructeur) { $items[] = [ - 'id' => $constructeur->getId(), - 'name' => $constructeur->getName(), + 'id' => $constructeur->getId(), + 'name' => $constructeur->getName(), 'email' => $constructeur->getEmail(), 'phone' => $constructeur->getPhone(), ]; @@ -649,18 +649,18 @@ class MachineSkeletonController extends AbstractController private function normalizeOverrides(object $link): ?array { - $name = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null; + $name = method_exists($link, 'getNameOverride') ? $link->getNameOverride() : null; $reference = method_exists($link, 'getReferenceOverride') ? $link->getReferenceOverride() : null; - $prix = method_exists($link, 'getPrixOverride') ? $link->getPrixOverride() : null; + $prix = method_exists($link, 'getPrixOverride') ? $link->getPrixOverride() : null; - if ($name === null && $reference === null && $prix === null) { + if (null === $name && null === $reference && null === $prix) { return null; } return [ - 'name' => $name, + 'name' => $name, 'reference' => $reference, - 'prix' => $prix, + 'prix' => $prix, ]; } @@ -683,12 +683,12 @@ class MachineSkeletonController extends AbstractController private function stringOrNull(mixed $value): ?string { - if ($value === null) { + if (null === $value) { return null; } $string = trim((string) $value); - return $string === '' ? null : $string; + return '' === $string ? null : $string; } private function resolveIdentifier(array $entry, array $keys): ?string @@ -698,9 +698,10 @@ class MachineSkeletonController extends AbstractController continue; } $value = $entry[$key]; - if ($value === null || $value === '') { + if (null === $value || '' === $value) { continue; } + return (string) $value; } @@ -709,6 +710,7 @@ class MachineSkeletonController extends AbstractController /** * @param array $links + * * @return array */ private function indexLinksById(array $links): array @@ -751,6 +753,6 @@ class MachineSkeletonController extends AbstractController private function generateCuid(): string { - return 'cl' . bin2hex(random_bytes(12)); + return 'cl'.bin2hex(random_bytes(12)); } } diff --git a/src/Controller/ModelTypeConversionController.php b/src/Controller/ModelTypeConversionController.php new file mode 100644 index 0000000..85bb715 --- /dev/null +++ b/src/Controller/ModelTypeConversionController.php @@ -0,0 +1,55 @@ +modelTypes->find($id); + + if (!$modelType) { + return new JsonResponse( + ['message' => 'Catégorie introuvable.'], + Response::HTTP_NOT_FOUND, + ); + } + + return new JsonResponse($this->conversionService->checkConversion($id)); + } + + #[Route('/api/model_types/{id}/convert', name: 'api_model_type_convert', methods: ['POST'])] + public function convert(string $id): JsonResponse + { + $modelType = $this->modelTypes->find($id); + + if (!$modelType) { + return new JsonResponse( + ['message' => 'Catégorie introuvable.'], + Response::HTTP_NOT_FOUND, + ); + } + + $result = $this->conversionService->convert($id); + + if (!$result['success']) { + return new JsonResponse($result, Response::HTTP_CONFLICT); + } + + return new JsonResponse($result); + } +} diff --git a/src/Controller/PieceHistoryController.php b/src/Controller/PieceHistoryController.php index 423be7f..1392b8b 100644 --- a/src/Controller/PieceHistoryController.php +++ b/src/Controller/PieceHistoryController.php @@ -7,6 +7,7 @@ namespace App\Controller; use App\Repository\AuditLogRepository; use App\Repository\PieceRepository; use App\Repository\ProfileRepository; +use DateTimeInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -17,8 +18,7 @@ final class PieceHistoryController private readonly PieceRepository $pieces, private readonly AuditLogRepository $auditLogs, private readonly ProfileRepository $profiles, - ) { - } + ) {} #[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])] public function __invoke(string $id): JsonResponse @@ -39,11 +39,11 @@ final class PieceHistoryController )))); $actorMap = []; - if ($actorIds !== []) { + if ([] !== $actorIds) { $profiles = $this->profiles->findBy(['id' => $actorIds]); foreach ($profiles as $profile) { $label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName())); - if ($label === '') { + if ('' === $label) { $label = $profile->getEmail() ?? $profile->getId(); } $actorMap[$profile->getId()] = $label; @@ -55,16 +55,16 @@ final class PieceHistoryController $actorId = $log->getActorProfileId(); return [ - 'id' => $log->getId(), - 'action' => $log->getAction(), - 'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM), - 'actor' => $actorId + 'id' => $log->getId(), + 'action' => $log->getAction(), + 'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM), + 'actor' => $actorId ? [ - 'id' => $actorId, + 'id' => $actorId, 'label' => $actorMap[$actorId] ?? $actorId, ] : null, - 'diff' => $log->getDiff(), + 'diff' => $log->getDiff(), 'snapshot' => $log->getSnapshot(), ]; }, @@ -77,4 +77,3 @@ final class PieceHistoryController ]); } } - diff --git a/src/Controller/ProductHistoryController.php b/src/Controller/ProductHistoryController.php index 7af455a..ce1986b 100644 --- a/src/Controller/ProductHistoryController.php +++ b/src/Controller/ProductHistoryController.php @@ -7,6 +7,7 @@ namespace App\Controller; use App\Repository\AuditLogRepository; use App\Repository\ProductRepository; use App\Repository\ProfileRepository; +use DateTimeInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -17,8 +18,7 @@ final class ProductHistoryController private readonly ProductRepository $products, private readonly AuditLogRepository $auditLogs, private readonly ProfileRepository $profiles, - ) { - } + ) {} #[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])] public function __invoke(string $id): JsonResponse @@ -39,11 +39,11 @@ final class ProductHistoryController )))); $actorMap = []; - if ($actorIds !== []) { + if ([] !== $actorIds) { $profiles = $this->profiles->findBy(['id' => $actorIds]); foreach ($profiles as $profile) { $label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName())); - if ($label === '') { + if ('' === $label) { $label = $profile->getEmail() ?? $profile->getId(); } $actorMap[$profile->getId()] = $label; @@ -55,16 +55,16 @@ final class ProductHistoryController $actorId = $log->getActorProfileId(); return [ - 'id' => $log->getId(), - 'action' => $log->getAction(), - 'createdAt' => $log->getCreatedAt()->format(\DateTimeInterface::ATOM), - 'actor' => $actorId + 'id' => $log->getId(), + 'action' => $log->getAction(), + 'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM), + 'actor' => $actorId ? [ - 'id' => $actorId, + 'id' => $actorId, 'label' => $actorMap[$actorId] ?? $actorId, ] : null, - 'diff' => $log->getDiff(), + 'diff' => $log->getDiff(), 'snapshot' => $log->getSnapshot(), ]; }, @@ -77,4 +77,3 @@ final class ProductHistoryController ]); } } - diff --git a/src/Controller/SessionProfileController.php b/src/Controller/SessionProfileController.php index 5ff3600..2c7d2ad 100644 --- a/src/Controller/SessionProfileController.php +++ b/src/Controller/SessionProfileController.php @@ -12,9 +12,7 @@ use Symfony\Component\Routing\Attribute\Route; final class SessionProfileController { - public function __construct(private readonly ProfileRepository $profiles) - { - } + public function __construct(private readonly ProfileRepository $profiles) {} #[Route('/api/session/profile', name: 'api_session_profile_get', methods: ['GET'])] public function getActiveProfile(Request $request): JsonResponse @@ -32,16 +30,17 @@ final class SessionProfileController $profile = $this->profiles->find($profileId); if (!$profile || !$profile->isActive()) { $session->remove('profileId'); + return new JsonResponse(['message' => 'Profil introuvable ou inactif.'], JsonResponse::HTTP_UNAUTHORIZED); } return new JsonResponse([ - 'id' => $profile->getId(), + 'id' => $profile->getId(), 'firstName' => $profile->getFirstName(), - 'lastName' => $profile->getLastName(), - 'email' => $profile->getEmail(), - 'isActive' => $profile->isActive(), - 'roles' => $profile->getRoles(), + 'lastName' => $profile->getLastName(), + 'email' => $profile->getEmail(), + 'isActive' => $profile->isActive(), + 'roles' => $profile->getRoles(), ]); } @@ -53,7 +52,7 @@ final class SessionProfileController return new JsonResponse(['message' => 'Session indisponible.'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); } - $payload = $request->toArray(); + $payload = $request->toArray(); $profileId = $payload['profileId'] ?? null; if (!$profileId) { @@ -68,12 +67,12 @@ final class SessionProfileController $session->set('profileId', $profile->getId()); return new JsonResponse([ - 'id' => $profile->getId(), + 'id' => $profile->getId(), 'firstName' => $profile->getFirstName(), - 'lastName' => $profile->getLastName(), - 'email' => $profile->getEmail(), - 'isActive' => $profile->isActive(), - 'roles' => $profile->getRoles(), + 'lastName' => $profile->getLastName(), + 'email' => $profile->getEmail(), + 'isActive' => $profile->isActive(), + 'roles' => $profile->getRoles(), ]); } diff --git a/src/Controller/SessionProfilesController.php b/src/Controller/SessionProfilesController.php index 5141ecb..41217b2 100644 --- a/src/Controller/SessionProfilesController.php +++ b/src/Controller/SessionProfilesController.php @@ -16,8 +16,7 @@ final class SessionProfilesController public function __construct( private readonly ProfileRepository $profiles, private readonly EntityManagerInterface $entityManager - ) { - } + ) {} #[Route('/api/session/profiles', name: 'api_session_profiles_list', methods: ['GET'])] public function list(): JsonResponse @@ -27,7 +26,8 @@ final class SessionProfilesController ->setParameter('active', true) ->orderBy('p.firstName', 'ASC') ->getQuery() - ->getResult(); + ->getResult() + ; return new JsonResponse(array_map([$this, 'serializeProfile'], $items)); } @@ -35,11 +35,11 @@ final class SessionProfilesController #[Route('/api/session/profiles', name: 'api_session_profiles_create', methods: ['POST'])] public function create(Request $request): JsonResponse { - $payload = $request->toArray(); + $payload = $request->toArray(); $firstName = trim((string) ($payload['firstName'] ?? '')); - $lastName = trim((string) ($payload['lastName'] ?? '')); + $lastName = trim((string) ($payload['lastName'] ?? '')); - if ($firstName === '' || $lastName === '') { + if ('' === $firstName || '' === $lastName) { return new JsonResponse(['message' => 'firstName et lastName sont requis.'], JsonResponse::HTTP_BAD_REQUEST); } @@ -71,10 +71,10 @@ final class SessionProfilesController private function serializeProfile(Profile $profile): array { return [ - 'id' => $profile->getId(), + 'id' => $profile->getId(), 'firstName' => $profile->getFirstName(), - 'lastName' => $profile->getLastName(), - 'isActive' => $profile->isActive(), + 'lastName' => $profile->getLastName(), + 'isActive' => $profile->isActive(), ]; } } diff --git a/src/Doctrine/QuoteStrategy/AlwaysQuoteStrategy.php b/src/Doctrine/QuoteStrategy/AlwaysQuoteStrategy.php index e7d42b0..1e5ac31 100644 --- a/src/Doctrine/QuoteStrategy/AlwaysQuoteStrategy.php +++ b/src/Doctrine/QuoteStrategy/AlwaysQuoteStrategy.php @@ -33,8 +33,8 @@ final class AlwaysQuoteStrategy implements QuoteStrategy { $tableName = $platform->quoteSingleIdentifier($class->table['name']); - if (! empty($class->table['schema'])) { - return $platform->quoteSingleIdentifier($class->table['schema']) . '.' . $tableName; + if (!empty($class->table['schema'])) { + return $platform->quoteSingleIdentifier($class->table['schema']).'.'.$tableName; } return $tableName; @@ -56,10 +56,10 @@ final class AlwaysQuoteStrategy implements QuoteStrategy $schema = ''; if (isset($association->joinTable->schema)) { - $schema = $platform->quoteSingleIdentifier($association->joinTable->schema) . '.'; + $schema = $platform->quoteSingleIdentifier($association->joinTable->schema).'.'; } - return $schema . $platform->quoteSingleIdentifier($association->joinTable->name); + return $schema.$platform->quoteSingleIdentifier($association->joinTable->name); } public function getJoinColumnName(JoinColumnMapping $joinColumn, ClassMetadata $class, AbstractPlatform $platform): string @@ -82,12 +82,13 @@ final class AlwaysQuoteStrategy implements QuoteStrategy foreach ($class->identifier as $fieldName) { if (isset($class->fieldMappings[$fieldName])) { $quotedColumnNames[] = $this->getColumnName($fieldName, $class, $platform); + continue; } $assoc = $class->associationMappings[$fieldName]; assert($assoc->isToOneOwningSide()); - $joinColumns = $assoc->joinColumns; + $joinColumns = $assoc->joinColumns; $assocQuotedColumnNames = array_map( static fn (JoinColumnMapping $joinColumn) => $platform->quoteSingleIdentifier($joinColumn->name), $joinColumns, @@ -103,8 +104,8 @@ final class AlwaysQuoteStrategy implements QuoteStrategy string $columnName, int $counter, AbstractPlatform $platform, - ClassMetadata|null $class = null, + ?ClassMetadata $class = null, ): string { - return $this->getSQLResultCasing($platform, $columnName . '_' . $counter); + return $this->getSQLResultCasing($platform, $columnName.'_'.$counter); } } diff --git a/src/Entity/AuditLog.php b/src/Entity/AuditLog.php index de5d5e5..37d2a71 100644 --- a/src/Entity/AuditLog.php +++ b/src/Entity/AuditLog.php @@ -49,11 +49,11 @@ class AuditLog ?array $snapshot = null, ?string $actorProfileId = null, ) { - $this->entityType = $entityType; - $this->entityId = $entityId; - $this->action = $action; - $this->diff = $diff; - $this->snapshot = $snapshot; + $this->entityType = $entityType; + $this->entityId = $entityId; + $this->action = $action; + $this->diff = $diff; + $this->snapshot = $snapshot; $this->actorProfileId = $actorProfileId; } @@ -64,7 +64,7 @@ class AuditLog $this->createdAt = new DateTimeImmutable(); } - if ($this->id === null) { + if (null === $this->id) { $this->id = $this->generateCuid(); } } diff --git a/src/Entity/MachineComponentLink.php b/src/Entity/MachineComponentLink.php index 0b2fdc0..89ea580 100644 --- a/src/Entity/MachineComponentLink.php +++ b/src/Entity/MachineComponentLink.php @@ -6,6 +6,7 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use App\Repository\MachineComponentLinkRepository; +use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; @@ -65,26 +66,26 @@ class MachineComponentLink private ?string $prixOverride = null; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] - private \DateTimeImmutable $createdAt; + private DateTimeImmutable $createdAt; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] - private \DateTimeImmutable $updatedAt; + private DateTimeImmutable $updatedAt; public function __construct() { - $this->childLinks = new ArrayCollection(); - $this->pieceLinks = new ArrayCollection(); + $this->childLinks = new ArrayCollection(); + $this->pieceLinks = new ArrayCollection(); $this->productLinks = new ArrayCollection(); } #[ORM\PrePersist] public function setCreatedAtValue(): void { - $now = new \DateTimeImmutable(); + $now = new DateTimeImmutable(); $this->createdAt = $now; $this->updatedAt = $now; - if ($this->id === null) { + if (null === $this->id) { $this->id = $this->generateCuid(); } } @@ -92,12 +93,7 @@ class MachineComponentLink #[ORM\PreUpdate] public function setUpdatedAtValue(): void { - $this->updatedAt = new \DateTimeImmutable(); - } - - private function generateCuid(): string - { - return 'cl' . bin2hex(random_bytes(12)); + $this->updatedAt = new DateTimeImmutable(); } public function getId(): ?string @@ -195,4 +191,9 @@ class MachineComponentLink return $this; } + + private function generateCuid(): string + { + return 'cl'.bin2hex(random_bytes(12)); + } } diff --git a/src/Entity/MachinePieceLink.php b/src/Entity/MachinePieceLink.php index 45ffd48..66b0cbf 100644 --- a/src/Entity/MachinePieceLink.php +++ b/src/Entity/MachinePieceLink.php @@ -6,6 +6,7 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use App\Repository\MachinePieceLinkRepository; +use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; @@ -53,10 +54,10 @@ class MachinePieceLink private ?string $prixOverride = null; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] - private \DateTimeImmutable $createdAt; + private DateTimeImmutable $createdAt; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] - private \DateTimeImmutable $updatedAt; + private DateTimeImmutable $updatedAt; public function __construct() { @@ -66,11 +67,11 @@ class MachinePieceLink #[ORM\PrePersist] public function setCreatedAtValue(): void { - $now = new \DateTimeImmutable(); + $now = new DateTimeImmutable(); $this->createdAt = $now; $this->updatedAt = $now; - if ($this->id === null) { + if (null === $this->id) { $this->id = $this->generateCuid(); } } @@ -78,12 +79,7 @@ class MachinePieceLink #[ORM\PreUpdate] public function setUpdatedAtValue(): void { - $this->updatedAt = new \DateTimeImmutable(); - } - - private function generateCuid(): string - { - return 'cl' . bin2hex(random_bytes(12)); + $this->updatedAt = new DateTimeImmutable(); } public function getId(): ?string @@ -181,4 +177,9 @@ class MachinePieceLink return $this; } + + private function generateCuid(): string + { + return 'cl'.bin2hex(random_bytes(12)); + } } diff --git a/src/Entity/MachineProductLink.php b/src/Entity/MachineProductLink.php index d692788..029f4e1 100644 --- a/src/Entity/MachineProductLink.php +++ b/src/Entity/MachineProductLink.php @@ -6,6 +6,7 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiResource; use App\Repository\MachineProductLinkRepository; +use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; @@ -52,10 +53,10 @@ class MachineProductLink private ?MachinePieceLink $parentPieceLink = null; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] - private \DateTimeImmutable $createdAt; + private DateTimeImmutable $createdAt; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] - private \DateTimeImmutable $updatedAt; + private DateTimeImmutable $updatedAt; public function __construct() { @@ -65,11 +66,11 @@ class MachineProductLink #[ORM\PrePersist] public function setCreatedAtValue(): void { - $now = new \DateTimeImmutable(); + $now = new DateTimeImmutable(); $this->createdAt = $now; $this->updatedAt = $now; - if ($this->id === null) { + if (null === $this->id) { $this->id = $this->generateCuid(); } } @@ -77,12 +78,7 @@ class MachineProductLink #[ORM\PreUpdate] public function setUpdatedAtValue(): void { - $this->updatedAt = new \DateTimeImmutable(); - } - - private function generateCuid(): string - { - return 'cl' . bin2hex(random_bytes(12)); + $this->updatedAt = new DateTimeImmutable(); } public function getId(): ?string @@ -168,4 +164,9 @@ class MachineProductLink return $this; } + + private function generateCuid(): string + { + return 'cl'.bin2hex(random_bytes(12)); + } } diff --git a/src/Entity/TypeMachineComponentRequirement.php b/src/Entity/TypeMachineComponentRequirement.php index a58091f..be94926 100644 --- a/src/Entity/TypeMachineComponentRequirement.php +++ b/src/Entity/TypeMachineComponentRequirement.php @@ -7,6 +7,7 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use App\Repository\TypeMachineComponentRequirementRepository; +use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; @@ -65,10 +66,10 @@ class TypeMachineComponentRequirement private Collection $machineComponentLinks; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] - private \DateTimeImmutable $createdAt; + private DateTimeImmutable $createdAt; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] - private \DateTimeImmutable $updatedAt; + private DateTimeImmutable $updatedAt; public function __construct() { @@ -78,11 +79,11 @@ class TypeMachineComponentRequirement #[ORM\PrePersist] public function setCreatedAtValue(): void { - $now = new \DateTimeImmutable(); + $now = new DateTimeImmutable(); $this->createdAt = $now; $this->updatedAt = $now; - if ($this->id === null) { + if (null === $this->id) { $this->id = $this->generateCuid(); } } @@ -90,12 +91,7 @@ class TypeMachineComponentRequirement #[ORM\PreUpdate] public function setUpdatedAtValue(): void { - $this->updatedAt = new \DateTimeImmutable(); - } - - private function generateCuid(): string - { - return 'cl' . bin2hex(random_bytes(12)); + $this->updatedAt = new DateTimeImmutable(); } public function getId(): ?string @@ -205,4 +201,9 @@ class TypeMachineComponentRequirement return $this; } + + private function generateCuid(): string + { + return 'cl'.bin2hex(random_bytes(12)); + } } diff --git a/src/Entity/TypeMachinePieceRequirement.php b/src/Entity/TypeMachinePieceRequirement.php index 8060f9f..64d24c8 100644 --- a/src/Entity/TypeMachinePieceRequirement.php +++ b/src/Entity/TypeMachinePieceRequirement.php @@ -7,6 +7,7 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use App\Repository\TypeMachinePieceRequirementRepository; +use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; @@ -65,10 +66,10 @@ class TypeMachinePieceRequirement private Collection $machinePieceLinks; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] - private \DateTimeImmutable $createdAt; + private DateTimeImmutable $createdAt; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] - private \DateTimeImmutable $updatedAt; + private DateTimeImmutable $updatedAt; public function __construct() { @@ -78,11 +79,11 @@ class TypeMachinePieceRequirement #[ORM\PrePersist] public function setCreatedAtValue(): void { - $now = new \DateTimeImmutable(); + $now = new DateTimeImmutable(); $this->createdAt = $now; $this->updatedAt = $now; - if ($this->id === null) { + if (null === $this->id) { $this->id = $this->generateCuid(); } } @@ -90,12 +91,7 @@ class TypeMachinePieceRequirement #[ORM\PreUpdate] public function setUpdatedAtValue(): void { - $this->updatedAt = new \DateTimeImmutable(); - } - - private function generateCuid(): string - { - return 'cl' . bin2hex(random_bytes(12)); + $this->updatedAt = new DateTimeImmutable(); } public function getId(): ?string @@ -205,4 +201,9 @@ class TypeMachinePieceRequirement return $this; } + + private function generateCuid(): string + { + return 'cl'.bin2hex(random_bytes(12)); + } } diff --git a/src/Entity/TypeMachineProductRequirement.php b/src/Entity/TypeMachineProductRequirement.php index b300d21..1a6c394 100644 --- a/src/Entity/TypeMachineProductRequirement.php +++ b/src/Entity/TypeMachineProductRequirement.php @@ -7,6 +7,7 @@ namespace App\Entity; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\ApiResource; use App\Repository\TypeMachineProductRequirementRepository; +use DateTimeImmutable; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; @@ -65,10 +66,10 @@ class TypeMachineProductRequirement private Collection $machineProductLinks; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] - private \DateTimeImmutable $createdAt; + private DateTimeImmutable $createdAt; #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] - private \DateTimeImmutable $updatedAt; + private DateTimeImmutable $updatedAt; public function __construct() { @@ -78,11 +79,11 @@ class TypeMachineProductRequirement #[ORM\PrePersist] public function setCreatedAtValue(): void { - $now = new \DateTimeImmutable(); + $now = new DateTimeImmutable(); $this->createdAt = $now; $this->updatedAt = $now; - if ($this->id === null) { + if (null === $this->id) { $this->id = $this->generateCuid(); } } @@ -90,12 +91,7 @@ class TypeMachineProductRequirement #[ORM\PreUpdate] public function setUpdatedAtValue(): void { - $this->updatedAt = new \DateTimeImmutable(); - } - - private function generateCuid(): string - { - return 'cl' . bin2hex(random_bytes(12)); + $this->updatedAt = new DateTimeImmutable(); } public function getId(): ?string @@ -205,4 +201,9 @@ class TypeMachineProductRequirement return $this; } + + private function generateCuid(): string + { + return 'cl'.bin2hex(random_bytes(12)); + } } diff --git a/src/Enum/ModelCategory.php b/src/Enum/ModelCategory.php index edd84d9..4e97808 100644 --- a/src/Enum/ModelCategory.php +++ b/src/Enum/ModelCategory.php @@ -7,6 +7,6 @@ namespace App\Enum; enum ModelCategory: string { case COMPONENT = 'COMPONENT'; - case PIECE = 'PIECE'; - case PRODUCT = 'PRODUCT'; + case PIECE = 'PIECE'; + case PRODUCT = 'PRODUCT'; } diff --git a/src/EventSubscriber/PieceProductSyncSubscriber.php b/src/EventSubscriber/PieceProductSyncSubscriber.php index b0c51ec..b926be6 100644 --- a/src/EventSubscriber/PieceProductSyncSubscriber.php +++ b/src/EventSubscriber/PieceProductSyncSubscriber.php @@ -16,9 +16,7 @@ use Doctrine\ORM\Events; */ final class PieceProductSyncSubscriber implements EventSubscriber { - public function __construct(private readonly ProductRepository $productRepository) - { - } + public function __construct(private readonly ProductRepository $productRepository) {} public function getSubscribedEvents(): array { @@ -47,7 +45,7 @@ final class PieceProductSyncSubscriber implements EventSubscriber $this->syncPrimaryProduct($entity); - $em = $args->getObjectManager(); + $em = $args->getObjectManager(); $meta = $em->getClassMetadata(Piece::class); $em->getUnitOfWork()->recomputeSingleEntityChangeSet($meta, $entity); } @@ -56,7 +54,7 @@ final class PieceProductSyncSubscriber implements EventSubscriber { $productIds = $piece->getProductIds(); - if ($productIds === []) { + if ([] === $productIds) { // If no explicit list is provided, keep the legacy relation as-is. return; } @@ -77,4 +75,3 @@ final class PieceProductSyncSubscriber implements EventSubscriber $piece->setProduct($primaryProduct); } } - diff --git a/src/EventSubscriber/UniqueConstraintSubscriber.php b/src/EventSubscriber/UniqueConstraintSubscriber.php index 1847de3..dd5fbb4 100644 --- a/src/EventSubscriber/UniqueConstraintSubscriber.php +++ b/src/EventSubscriber/UniqueConstraintSubscriber.php @@ -9,6 +9,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\KernelEvents; +use Throwable; final class UniqueConstraintSubscriber implements EventSubscriberInterface { @@ -30,15 +31,15 @@ final class UniqueConstraintSubscriber implements EventSubscriberInterface $event->setResponse(new JsonResponse( [ 'success' => false, - 'error' => 'nom duplique', + 'error' => 'nom duplique', ], JsonResponse::HTTP_CONFLICT )); } - private function findUniqueConstraintViolation(\Throwable $throwable): ?UniqueConstraintViolationException + private function findUniqueConstraintViolation(Throwable $throwable): ?UniqueConstraintViolationException { - for ($current = $throwable; $current !== null; $current = $current->getPrevious()) { + for ($current = $throwable; null !== $current; $current = $current->getPrevious()) { if ($current instanceof UniqueConstraintViolationException) { return $current; } diff --git a/src/Service/ModelTypeCategoryConversionService.php b/src/Service/ModelTypeCategoryConversionService.php new file mode 100644 index 0000000..fbc2775 --- /dev/null +++ b/src/Service/ModelTypeCategoryConversionService.php @@ -0,0 +1,418 @@ +, blockers: list} + */ + public function checkConversion(string $modelTypeId): array + { + $modelType = $this->modelTypes->find($modelTypeId); + + if (!$modelType) { + return [ + 'canConvert' => false, + 'direction' => null, + 'itemCount' => 0, + 'names' => [], + 'blockers' => ['Catégorie introuvable.'], + ]; + } + + $category = $modelType->getCategory(); + + if (ModelCategory::PRODUCT === $category) { + return [ + 'canConvert' => false, + 'direction' => null, + 'itemCount' => 0, + 'names' => [], + 'blockers' => ['La conversion n\'est pas disponible pour les catégories de produit.'], + ]; + } + + if (ModelCategory::PIECE === $category) { + return $this->checkPieceToComponent($modelTypeId, $modelType->getName()); + } + + return $this->checkComponentToPiece($modelTypeId, $modelType->getName()); + } + + /** + * @return array{success: bool, convertedCount: int, error: null|string} + */ + public function convert(string $modelTypeId): array + { + $check = $this->checkConversion($modelTypeId); + + if (!$check['canConvert']) { + return [ + 'success' => false, + 'convertedCount' => 0, + 'error' => implode(' ', $check['blockers']), + ]; + } + + $modelType = $this->modelTypes->find($modelTypeId); + + if (!$modelType) { + return ['success' => false, 'convertedCount' => 0, 'error' => 'Catégorie introuvable.']; + } + + $category = $modelType->getCategory(); + + $this->connection->beginTransaction(); + + try { + if (ModelCategory::PIECE === $category) { + $count = $this->convertPieceToComponent($modelTypeId); + } else { + $count = $this->convertComponentToPiece($modelTypeId); + } + + $this->connection->commit(); + + return ['success' => true, 'convertedCount' => $count, 'error' => null]; + } catch (Throwable $e) { + $this->connection->rollBack(); + + return ['success' => false, 'convertedCount' => 0, 'error' => $e->getMessage()]; + } + } + + /** + * @return array{canConvert: bool, direction: string, itemCount: int, names: list, blockers: list} + */ + private function checkPieceToComponent(string $modelTypeId, string $modelTypeName): array + { + $blockers = []; + + $pieceCount = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM pieces WHERE typepieceid = :id', + ['id' => $modelTypeId], + ); + + $names = $this->connection->fetchFirstColumn( + 'SELECT name FROM pieces WHERE typepieceid = :id ORDER BY name', + ['id' => $modelTypeId], + ); + + // Check machine links + $machineLinked = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM machine_piece_links mpl + JOIN pieces p ON mpl.pieceid = p.id + WHERE p.typepieceid = :id', + ['id' => $modelTypeId], + ); + + if ($machineLinked > 0) { + $blockers[] = sprintf('%d pièce(s) liée(s) à des machines.', $machineLinked); + } + + // Check type machine requirements + $requirementCount = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM type_machine_piece_requirements WHERE typepieceid = :id', + ['id' => $modelTypeId], + ); + + if ($requirementCount > 0) { + $blockers[] = sprintf('Utilisé dans %d modèle(s) de type de machine.', $requirementCount); + } + + // Check name collision with existing composants + $collisions = $this->connection->fetchFirstColumn( + 'SELECT p.name FROM pieces p + WHERE p.typepieceid = :id + AND p.name IN (SELECT c.name FROM composants c)', + ['id' => $modelTypeId], + ); + + if ([] !== $collisions) { + $blockers[] = sprintf( + 'Collision de nom avec des composants existants : %s.', + implode(', ', $collisions), + ); + } + + // Check ModelType name collision + $nameCollision = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM model_types WHERE category = :cat AND name = :name AND id != :id', + ['cat' => ModelCategory::COMPONENT->value, 'name' => $modelTypeName, 'id' => $modelTypeId], + ); + + if ($nameCollision > 0) { + $blockers[] = sprintf('Une catégorie de composant « %s » existe déjà.', $modelTypeName); + } + + return [ + 'canConvert' => [] === $blockers, + 'direction' => 'piece_to_component', + 'itemCount' => $pieceCount, + 'names' => $names, + 'blockers' => $blockers, + ]; + } + + /** + * @return array{canConvert: bool, direction: string, itemCount: int, names: list, blockers: list} + */ + private function checkComponentToPiece(string $modelTypeId, string $modelTypeName): array + { + $blockers = []; + + $composantCount = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM composants WHERE typecomposantid = :id', + ['id' => $modelTypeId], + ); + + $names = $this->connection->fetchFirstColumn( + 'SELECT name FROM composants WHERE typecomposantid = :id ORDER BY name', + ['id' => $modelTypeId], + ); + + // Check machine links + $machineLinked = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM machine_component_links mcl + JOIN composants c ON mcl.composantid = c.id + WHERE c.typecomposantid = :id', + ['id' => $modelTypeId], + ); + + if ($machineLinked > 0) { + $blockers[] = sprintf('%d composant(s) lié(s) à des machines.', $machineLinked); + } + + // Check type machine requirements + $requirementCount = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM type_machine_component_requirements WHERE typecomposantid = :id', + ['id' => $modelTypeId], + ); + + if ($requirementCount > 0) { + $blockers[] = sprintf('Utilisé dans %d modèle(s) de type de machine.', $requirementCount); + } + + // Check if any composant has pieces or sub-components in structure + $withStructure = $this->connection->fetchAllAssociative( + 'SELECT name, structure FROM composants WHERE typecomposantid = :id AND structure IS NOT NULL', + ['id' => $modelTypeId], + ); + + foreach ($withStructure as $row) { + $structure = json_decode($row['structure'], true); + + if (!is_array($structure)) { + continue; + } + + $hasPieces = !empty($structure['pieces']); + $hasSubcomponents = !empty($structure['subcomponents']); + + if ($hasPieces || $hasSubcomponents) { + $parts = []; + + if ($hasPieces) { + $parts[] = 'pièces'; + } + + if ($hasSubcomponents) { + $parts[] = 'sous-composants'; + } + + $blockers[] = sprintf( + 'Le composant « %s » contient des %s dans sa structure.', + $row['name'], + implode(' et ', $parts), + ); + } + } + + // Check name collision with existing pieces + $collisions = $this->connection->fetchFirstColumn( + 'SELECT c.name FROM composants c + WHERE c.typecomposantid = :id + AND c.name IN (SELECT p.name FROM pieces p)', + ['id' => $modelTypeId], + ); + + if ([] !== $collisions) { + $blockers[] = sprintf( + 'Collision de nom avec des pièces existantes : %s.', + implode(', ', $collisions), + ); + } + + // Check ModelType name collision + $nameCollision = (int) $this->connection->fetchOne( + 'SELECT COUNT(*) FROM model_types WHERE category = :cat AND name = :name AND id != :id', + ['cat' => ModelCategory::PIECE->value, 'name' => $modelTypeName, 'id' => $modelTypeId], + ); + + if ($nameCollision > 0) { + $blockers[] = sprintf('Une catégorie de pièce « %s » existe déjà.', $modelTypeName); + } + + return [ + 'canConvert' => [] === $blockers, + 'direction' => 'component_to_piece', + 'itemCount' => $composantCount, + 'names' => $names, + 'blockers' => $blockers, + ]; + } + + private function convertPieceToComponent(string $modelTypeId): int + { + // 1. Insert into composants from pieces + $count = $this->connection->executeStatement( + 'INSERT INTO composants (id, name, reference, prix, structure, typecomposantid, productid, createdat, updatedat) + SELECT id, name, reference, prix, NULL, typepieceid, productid, createdat, updatedat + FROM pieces + WHERE typepieceid = :id', + ['id' => $modelTypeId], + ); + + // 2. Transfer constructeur associations + $this->connection->executeStatement( + 'INSERT INTO _composantconstructeurs (a, b) + SELECT pc.a, pc.b FROM _piececonstructeurs pc + WHERE pc.a IN (SELECT id FROM composants WHERE typecomposantid = :id)', + ['id' => $modelTypeId], + ); + + $this->connection->executeStatement( + 'DELETE FROM _piececonstructeurs + WHERE a IN (SELECT id FROM pieces WHERE typepieceid = :id)', + ['id' => $modelTypeId], + ); + + // 3. Transfer document references + $this->connection->executeStatement( + 'UPDATE documents SET composantid = pieceid, pieceid = NULL + WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)', + ['id' => $modelTypeId], + ); + + // 4. Transfer custom_field_values references + $this->connection->executeStatement( + 'UPDATE custom_field_values SET composantid = pieceid, pieceid = NULL + WHERE pieceid IN (SELECT id FROM pieces WHERE typepieceid = :id)', + ['id' => $modelTypeId], + ); + + // 5. Transfer custom_fields from typePiece to typeComposant + $this->connection->executeStatement( + 'UPDATE custom_fields SET typecomposantid = typepieceid, typepieceid = NULL + WHERE typepieceid = :id', + ['id' => $modelTypeId], + ); + + // 6. Delete original pieces + $this->connection->executeStatement( + 'DELETE FROM pieces WHERE typepieceid = :id', + ['id' => $modelTypeId], + ); + + // 7. Update ModelType + $this->connection->executeStatement( + 'UPDATE model_types + SET category = :cat, + componentskeleton = pieceskeleton, + pieceskeleton = NULL, + updatedat = :now + WHERE id = :id', + [ + 'cat' => ModelCategory::COMPONENT->value, + 'now' => new DateTimeImmutable()->format('Y-m-d H:i:s'), + 'id' => $modelTypeId, + ], + ); + + return $count; + } + + private function convertComponentToPiece(string $modelTypeId): int + { + // 1. Insert into pieces from composants + $count = $this->connection->executeStatement( + 'INSERT INTO pieces (id, name, reference, prix, productids, typepieceid, productid, createdat, updatedat) + SELECT id, name, reference, prix, NULL, typecomposantid, productid, createdat, updatedat + FROM composants + WHERE typecomposantid = :id', + ['id' => $modelTypeId], + ); + + // 2. Transfer constructeur associations + $this->connection->executeStatement( + 'INSERT INTO _piececonstructeurs (a, b) + SELECT cc.a, cc.b FROM _composantconstructeurs cc + WHERE cc.a IN (SELECT id FROM pieces WHERE typepieceid = :id)', + ['id' => $modelTypeId], + ); + + $this->connection->executeStatement( + 'DELETE FROM _composantconstructeurs + WHERE a IN (SELECT id FROM composants WHERE typecomposantid = :id)', + ['id' => $modelTypeId], + ); + + // 3. Transfer document references + $this->connection->executeStatement( + 'UPDATE documents SET pieceid = composantid, composantid = NULL + WHERE composantid IN (SELECT id FROM composants WHERE typecomposantid = :id)', + ['id' => $modelTypeId], + ); + + // 4. Transfer custom_field_values references + $this->connection->executeStatement( + 'UPDATE custom_field_values SET pieceid = composantid, composantid = NULL + WHERE composantid IN (SELECT id FROM composants WHERE typecomposantid = :id)', + ['id' => $modelTypeId], + ); + + // 5. Transfer custom_fields from typeComposant to typePiece + $this->connection->executeStatement( + 'UPDATE custom_fields SET typepieceid = typecomposantid, typecomposantid = NULL + WHERE typecomposantid = :id', + ['id' => $modelTypeId], + ); + + // 6. Delete original composants + $this->connection->executeStatement( + 'DELETE FROM composants WHERE typecomposantid = :id', + ['id' => $modelTypeId], + ); + + // 7. Update ModelType + $this->connection->executeStatement( + 'UPDATE model_types + SET category = :cat, + pieceskeleton = componentskeleton, + componentskeleton = NULL, + updatedat = :now + WHERE id = :id', + [ + 'cat' => ModelCategory::PIECE->value, + 'now' => new DateTimeImmutable()->format('Y-m-d H:i:s'), + 'id' => $modelTypeId, + ], + ); + + return $count; + } +}