From e432153083f72bf8de2180045f0225fc6f1ac222 Mon Sep 17 00:00:00 2001 From: Matthieu Date: Wed, 6 May 2026 10:14:23 +0200 Subject: [PATCH] refactor : simplification globale (vague 1 + 2) - ActorProfileResolver : service unique partage par AbstractAuditSubscriber, EntityVersionService et ModelTypeCategoryConversionService (3 implementations dupliquees+divergentes) - corrige un bug latent : EntityVersionService restoraitsans le fallback Security::getUser, loggant actor=null hors session - machine-clone : clonage des contextFieldValues integre dans cloneComponentLinks/clonePieceLinks, supprime cloneContextFieldValues et son find() en boucle - helpers extraits : serializeProductSlots (EntityVersionService), updateModelTypeCategory (ModelTypeCategoryConversionService) - supprime collectCollectionUpdate() vide + ses appels (AbstractAuditSubscriber) - useMachineDetailData : retire debug ref couplee a isEditMode, componentTypeLabelMap/pieceTypeLabelMap jamais consommes, double assignation machine.productLinks - PieceItem : retire l'init pieceData dans onMounted (deja couvert par reactive() et le watcher) Co-Authored-By: Claude Opus 4.7 --- frontend/app/components/PieceItem.vue | 5 -- .../app/composables/useMachineDetailData.ts | 30 ++------- src/Controller/MachineStructureController.php | 66 +++++++------------ .../AbstractAuditSubscriber.php | 55 ++-------------- .../MachineAuditSubscriber.php | 2 +- src/Service/ActorProfileResolver.php | 41 ++++++++++++ src/Service/EntityVersionService.php | 50 ++++++-------- .../ModelTypeCategoryConversionService.php | 53 ++++----------- 8 files changed, 105 insertions(+), 197 deletions(-) create mode 100644 src/Service/ActorProfileResolver.php diff --git a/frontend/app/components/PieceItem.vue b/frontend/app/components/PieceItem.vue index 71c9ea6..38b865b 100644 --- a/frontend/app/components/PieceItem.vue +++ b/frontend/app/components/PieceItem.vue @@ -739,12 +739,7 @@ watch( ) onMounted(() => { - pieceData.name = props.piece.name || '' - pieceData.reference = props.piece.reference || '' - pieceData.prix = props.piece.prix || '' - pieceData.quantity = props.piece.quantity ?? 1 loadProducts().catch(() => {}) - if (pieceData.productId) ensureProductLoaded(pieceData.productId) if (!props.piece.documents?.length) refreshDocuments() }) diff --git a/frontend/app/composables/useMachineDetailData.ts b/frontend/app/composables/useMachineDetailData.ts index 83c387c..d99c698 100644 --- a/frontend/app/composables/useMachineDetailData.ts +++ b/frontend/app/composables/useMachineDetailData.ts @@ -119,7 +119,6 @@ export function useMachineDetailData(machineId: string) { if (!machineName.value.trim()) return false return true }) - const debug = ref(false) const componentsCollapsed = ref(true) const collapseToggleToken = ref(0) @@ -227,22 +226,6 @@ export function useMachineDetailData(machineId: string) { const componentTypeOptions = computed(() => componentTypes.value || []) const pieceTypeOptions = computed(() => pieceTypes.value || []) - const componentTypeLabelMap = computed(() => { - const map = new Map() - componentTypeOptions.value.forEach((type) => { - if (type?.id) map.set(type.id as string, (type.name as string) || '') - }) - return map - }) - - const pieceTypeLabelMap = computed(() => { - const map = new Map() - pieceTypeOptions.value.forEach((type) => { - if (type?.id) map.set(type.id as string, (type.name as string) || '') - }) - return map - }) - // Machine field methods const initMachineFields = () => { if (machine.value) { @@ -306,7 +289,6 @@ export function useMachineDetailData(machineId: string) { // UI methods const toggleEditMode = () => { isEditMode.value = !isEditMode.value - debug.value = !debug.value if (isEditMode.value && !machineDocumentsLoaded.value) { refreshMachineDocuments() } @@ -432,12 +414,6 @@ export function useMachineDetailData(machineId: string) { await productsPromise const linksApplied = applyMachineLinks(machineResult.data) - if (machine.value) { - machine.value.componentLinks = machineComponentLinks.value - machine.value.pieceLinks = machinePieceLinks.value - machine.value.productLinks = machineProductLinks.value - } - if (!linksApplied) { components.value = transformComponentCustomFields(machinePayload.components || []) pieces.value = transformCustomFields(machinePayload.pieces || []) @@ -447,6 +423,8 @@ export function useMachineDetailData(machineId: string) { } if (machine.value) { + machine.value.componentLinks = machineComponentLinks.value + machine.value.pieceLinks = machinePieceLinks.value machine.value.productLinks = machineProductLinks.value } @@ -496,11 +474,11 @@ export function useMachineDetailData(machineId: string) { // UI state machineDocumentFiles, machineDocumentsUploading, machineDocumentsLoaded, machineCustomFields, pendingContextFieldUpdates, previewDocument, previewVisible, - isEditMode, debug, + isEditMode, componentsCollapsed, collapseToggleToken, piecesCollapsed, pieceCollapseToggleToken, // Computed - componentTypeOptions, pieceTypeOptions, componentTypeLabelMap, pieceTypeLabelMap, + componentTypeOptions, pieceTypeOptions, productInventory, productById, flattenedComponents, machinePieces, machineDirectProducts, machineDocumentsList, visibleMachineCustomFields, diff --git a/src/Controller/MachineStructureController.php b/src/Controller/MachineStructureController.php index 7331a55..3a999b1 100644 --- a/src/Controller/MachineStructureController.php +++ b/src/Controller/MachineStructureController.php @@ -162,9 +162,6 @@ class MachineStructureController extends AbstractController // 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']); @@ -230,6 +227,17 @@ class MachineStructureController extends AbstractController $newLink->setReferenceOverride($link->getReferenceOverride()); $newLink->setPrixOverride($link->getPrixOverride()); $this->entityManager->persist($newLink); + + foreach ($link->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); + $newLink->getContextFieldValues()->add($newValue); + } + $linkMap[$link->getId()] = $newLink; } @@ -269,6 +277,17 @@ class MachineStructureController extends AbstractController } $this->entityManager->persist($newLink); + + foreach ($link->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); + $newLink->getContextFieldValues()->add($newValue); + } + $linkMap[$link->getId()] = $newLink; } @@ -317,47 +336,6 @@ class MachineStructureController extends AbstractController } } - /** - * @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); - $newLink->getContextFieldValues()->add($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); - $newLink->getContextFieldValues()->add($newValue); - } - } - } - private function normalizePayloadList(mixed $value): array { if (!is_array($value)) { diff --git a/src/EventSubscriber/AbstractAuditSubscriber.php b/src/EventSubscriber/AbstractAuditSubscriber.php index 924375b..91627d3 100644 --- a/src/EventSubscriber/AbstractAuditSubscriber.php +++ b/src/EventSubscriber/AbstractAuditSubscriber.php @@ -11,8 +11,8 @@ use App\Entity\Machine; use App\Entity\ModelType; use App\Entity\Piece; use App\Entity\Product; -use App\Entity\Profile; use App\Entity\Site; +use App\Service\ActorProfileResolver; use BackedEnum; use DateTimeInterface; use Doctrine\Common\Collections\Collection; @@ -22,10 +22,6 @@ use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Events; use Doctrine\ORM\UnitOfWork; use Error; -use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Session\SessionInterface; -use Throwable; use function is_array; use function is_object; @@ -35,8 +31,7 @@ use function method_exists; abstract class AbstractAuditSubscriber implements EventSubscriber { public function __construct( - private readonly RequestStack $requestStack, - private readonly Security $security, + private readonly ActorProfileResolver $actorProfileResolver, ) {} public function getSubscribedEvents(): array @@ -61,7 +56,7 @@ abstract class AbstractAuditSubscriber implements EventSubscriber } } - $actorProfileId = $this->resolveActorProfileId(); + $actorProfileId = $this->actorProfileResolver->resolve(); $entityType = $this->entityType(); if ($this->hasCollectionTracking()) { @@ -278,28 +273,6 @@ abstract class AbstractAuditSubscriber implements EventSubscriber return $entity->getVersion(); } - protected function resolveActorProfileId(): ?string - { - try { - $session = $this->requestStack->getSession(); - if ($session instanceof SessionInterface) { - $profileId = $session->get('profileId'); - if ($profileId) { - return (string) $profileId; - } - } - } catch (Throwable) { - // No session available (CLI context, etc.) - } - - $user = $this->security->getUser(); - if ($user instanceof Profile) { - return $user->getId(); - } - - return null; - } - private function onFlushSimple(EntityManagerInterface $em, UnitOfWork $uow, ?string $actorProfileId, string $entityType): void { foreach ($uow->getScheduledEntityInsertions() as $entity) { @@ -385,13 +358,10 @@ abstract class AbstractAuditSubscriber implements EventSubscriber $this->persistAuditLog($em, new AuditLog($entityType, (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId)); } - foreach ($uow->getScheduledCollectionUpdates() as $collection) { - $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingEntities); - } - foreach ($uow->getScheduledCollectionDeletions() as $collection) { - $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingEntities); - } - + // Note: scheduled collection updates/deletions are intentionally not + // tracked here — constructeurs are now persisted as ConstructeurLink + // entities (OneToMany), so Doctrine no longer fires collection events + // for them. Custom field values are handled below. $this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingEntities); foreach ($pendingUpdates as $entityId => $diff) { @@ -411,17 +381,6 @@ abstract class AbstractAuditSubscriber implements EventSubscriber } } - /** - * No-op: constructeurs are now tracked as ConstructeurLink entities (OneToMany), - * so Doctrine no longer fires collection update events for them. - */ - private function collectCollectionUpdate( - object $collection, - array &$pendingUpdates, - array &$pendingSnapshots, - array &$pendingEntities, - ): void {} - private function collectCustomFieldValueChanges( UnitOfWork $uow, array &$pendingUpdates, diff --git a/src/EventSubscriber/MachineAuditSubscriber.php b/src/EventSubscriber/MachineAuditSubscriber.php index c19272d..1e1b358 100644 --- a/src/EventSubscriber/MachineAuditSubscriber.php +++ b/src/EventSubscriber/MachineAuditSubscriber.php @@ -31,7 +31,7 @@ final class MachineAuditSubscriber extends AbstractAuditSubscriber } $uow = $em->getUnitOfWork(); - $actorProfileId = $this->resolveActorProfileId(); + $actorProfileId = $this->actorProfileResolver->resolve(); $this->processLinkChanges($em, $uow, $actorProfileId); } diff --git a/src/Service/ActorProfileResolver.php b/src/Service/ActorProfileResolver.php new file mode 100644 index 0000000..0717150 --- /dev/null +++ b/src/Service/ActorProfileResolver.php @@ -0,0 +1,41 @@ +requestStack->getSession(); + if ($session instanceof SessionInterface) { + $profileId = $session->get('profileId'); + if ($profileId) { + return (string) $profileId; + } + } + } catch (Throwable) { + // No session available (CLI context, etc.) + } + + $user = $this->security->getUser(); + if ($user instanceof Profile) { + return $user->getId(); + } + + return null; + } +} diff --git a/src/Service/EntityVersionService.php b/src/Service/EntityVersionService.php index 1fd2a6a..5e837d4 100644 --- a/src/Service/EntityVersionService.php +++ b/src/Service/EntityVersionService.php @@ -34,7 +34,6 @@ use DateTimeInterface; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use LogicException; -use Symfony\Component\HttpFoundation\RequestStack; use Throwable; final class EntityVersionService @@ -56,7 +55,7 @@ final class EntityVersionService public function __construct( private readonly AuditLogRepository $auditLogs, private readonly EntityManagerInterface $em, - private readonly RequestStack $requestStack, + private readonly ActorProfileResolver $actorProfileResolver, private readonly MachineRepository $machines, private readonly ComposantRepository $composants, private readonly PieceRepository $pieces, @@ -187,7 +186,7 @@ final class EntityVersionService 'restore', ['restoredFromVersion' => $version, 'restoreMode' => $restoreMode], $this->buildCurrentSnapshot($entityType, $entity), - $this->resolveActorProfileId(), + $this->actorProfileResolver->resolve(), $newVersion, ); $this->em->persist($restoreAuditLog); @@ -917,25 +916,11 @@ final class EntityVersionService 'position' => $slot->getPosition(), ]; } - $snapshot['productSlots'] = []; - foreach ($entity->getProductSlots() as $slot) { - $snapshot['productSlots'][] = [ - 'id' => $slot->getId(), 'typeProductId' => $slot->getTypeProduct()?->getId(), - 'selectedProductId' => $slot->getSelectedProduct()?->getId(), - 'familyCode' => $slot->getFamilyCode(), 'position' => $slot->getPosition(), - ]; - } + $snapshot['productSlots'] = $this->serializeProductSlots($entity->getProductSlots()); } if ('piece' === $entityType) { - $snapshot['productSlots'] = []; - foreach ($entity->getProductSlots() as $slot) { - $snapshot['productSlots'][] = [ - 'id' => $slot->getId(), 'typeProductId' => $slot->getTypeProduct()?->getId(), - 'selectedProductId' => $slot->getSelectedProduct()?->getId(), - 'familyCode' => $slot->getFamilyCode(), 'position' => $slot->getPosition(), - ]; - } + $snapshot['productSlots'] = $this->serializeProductSlots($entity->getProductSlots()); } // Custom field values @@ -953,21 +938,24 @@ final class EntityVersionService } /** - * Resolve the current actor profile ID from the session. - * Mirrors AbstractAuditSubscriber::resolveActorProfileId(). + * @param iterable $slots + * + * @return list> */ - private function resolveActorProfileId(): ?string + private function serializeProductSlots(iterable $slots): array { - try { - $session = $this->requestStack->getSession(); - $profileId = $session->get('profileId'); - if ($profileId) { - return (string) $profileId; - } - } catch (Throwable) { - // No session available (CLI context, etc.) + $serialized = []; + foreach ($slots as $slot) { + $serialized[] = [ + 'id' => $slot->getId(), + 'typeProductId' => $slot->getTypeProduct()?->getId(), + 'selectedProductId' => $slot->getSelectedProduct()?->getId(), + 'familyCode' => $slot->getFamilyCode(), + 'position' => $slot->getPosition(), + ]; } - return null; + return $serialized; } + } diff --git a/src/Service/ModelTypeCategoryConversionService.php b/src/Service/ModelTypeCategoryConversionService.php index 5856119..928f871 100644 --- a/src/Service/ModelTypeCategoryConversionService.php +++ b/src/Service/ModelTypeCategoryConversionService.php @@ -4,23 +4,17 @@ declare(strict_types=1); namespace App\Service; -use App\Entity\Profile; use App\Enum\ModelCategory; use App\Repository\ModelTypeRepository; use DateTimeImmutable; use Doctrine\DBAL\Connection; -use Symfony\Bundle\SecurityBundle\Security; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Session\SessionInterface; -use Throwable; final class ModelTypeCategoryConversionService { public function __construct( private readonly Connection $connection, private readonly ModelTypeRepository $modelTypes, - private readonly RequestStack $requestStack, - private readonly Security $security, + private readonly ActorProfileResolver $actorProfileResolver, ) {} /** @@ -327,17 +321,7 @@ final class ModelTypeCategoryConversionService ); // 7. Update ModelType - $this->connection->executeStatement( - 'UPDATE model_types - SET category = :cat, - updatedat = :now - WHERE id = :id', - [ - 'cat' => ModelCategory::COMPONENT->value, - 'now' => new DateTimeImmutable()->format('Y-m-d H:i:s'), - 'id' => $modelTypeId, - ], - ); + $this->updateModelTypeCategory($modelTypeId, ModelCategory::COMPONENT); return $count; } @@ -406,19 +390,24 @@ final class ModelTypeCategoryConversionService ); // 7. Update ModelType + $this->updateModelTypeCategory($modelTypeId, ModelCategory::PIECE); + + return $count; + } + + private function updateModelTypeCategory(string $modelTypeId, ModelCategory $category): void + { $this->connection->executeStatement( 'UPDATE model_types SET category = :cat, updatedat = :now WHERE id = :id', [ - 'cat' => ModelCategory::PIECE->value, + 'cat' => $category->value, 'now' => new DateTimeImmutable()->format('Y-m-d H:i:s'), 'id' => $modelTypeId, ], ); - - return $count; } /** @@ -457,30 +446,10 @@ final class ModelTypeCategoryConversionService 'action' => 'convert', 'diff' => json_encode($diff), 'snapshot' => json_encode($snapshot), - 'actor' => $this->resolveActorProfileId(), + 'actor' => $this->actorProfileResolver->resolve(), 'now' => $now, ], ); } - private function resolveActorProfileId(): ?string - { - try { - $session = $this->requestStack->getSession(); - if ($session instanceof SessionInterface) { - $profileId = $session->get('profileId'); - if ($profileId) { - return (string) $profileId; - } - } - } catch (Throwable) { - } - - $user = $this->security->getUser(); - if ($user instanceof Profile) { - return $user->getId(); - } - - return null; - } }