From 02ff8b1a966786293ed8412bc88f3d206b54616a Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 12 Feb 2026 14:51:26 +0100 Subject: [PATCH] feat(audit) : extend audit logging to machines, constructeurs, model types, documents and conversions Co-Authored-By: Claude Opus 4.6 --- Inventory_frontend | 2 +- VERSION | 2 +- src/Controller/MachineHistoryController.php | 79 ++++ .../ConstructeurAuditSubscriber.php | 168 +++++++ .../DocumentAuditSubscriber.php | 192 ++++++++ .../MachineAuditSubscriber.php | 412 ++++++++++++++++++ .../ModelTypeAuditSubscriber.php | 180 ++++++++ .../ModelTypeCategoryConversionService.php | 76 ++++ 8 files changed, 1109 insertions(+), 2 deletions(-) create mode 100644 src/Controller/MachineHistoryController.php create mode 100644 src/EventSubscriber/ConstructeurAuditSubscriber.php create mode 100644 src/EventSubscriber/DocumentAuditSubscriber.php create mode 100644 src/EventSubscriber/MachineAuditSubscriber.php create mode 100644 src/EventSubscriber/ModelTypeAuditSubscriber.php diff --git a/Inventory_frontend b/Inventory_frontend index 2fffe4a..62127a3 160000 --- a/Inventory_frontend +++ b/Inventory_frontend @@ -1 +1 @@ -Subproject commit 2fffe4a3684b6020bfc157a8fab5d2523a430bd5 +Subproject commit 62127a33f51f28fc51ce2a81a37535f066293bf4 diff --git a/VERSION b/VERSION index dc1e644..9c6d629 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.6.0 +1.6.1 diff --git a/src/Controller/MachineHistoryController.php b/src/Controller/MachineHistoryController.php new file mode 100644 index 0000000..acc49bb --- /dev/null +++ b/src/Controller/MachineHistoryController.php @@ -0,0 +1,79 @@ +machines->find($id); + if (!$machine) { + return new JsonResponse( + ['message' => 'Machine introuvable.'], + Response::HTTP_NOT_FOUND, + ); + } + + $logs = $this->auditLogs->findEntityHistory('machine', $id, 200); + + $actorIds = array_values(array_unique(array_filter(array_map( + static fn ($log) => $log->getActorProfileId(), + $logs, + )))); + + $actorMap = []; + if ([] !== $actorIds) { + $profiles = $this->profiles->findBy(['id' => $actorIds]); + foreach ($profiles as $profile) { + $label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName())); + if ('' === $label) { + $label = $profile->getEmail() ?? $profile->getId(); + } + $actorMap[$profile->getId()] = $label; + } + } + + $items = array_map( + static function ($log) use ($actorMap) { + $actorId = $log->getActorProfileId(); + + return [ + 'id' => $log->getId(), + 'action' => $log->getAction(), + 'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM), + 'actor' => $actorId + ? [ + 'id' => $actorId, + 'label' => $actorMap[$actorId] ?? $actorId, + ] + : null, + 'diff' => $log->getDiff(), + 'snapshot' => $log->getSnapshot(), + ]; + }, + $logs, + ); + + return new JsonResponse([ + 'items' => array_values($items), + 'total' => count($items), + ]); + } +} diff --git a/src/EventSubscriber/ConstructeurAuditSubscriber.php b/src/EventSubscriber/ConstructeurAuditSubscriber.php new file mode 100644 index 0000000..428dead --- /dev/null +++ b/src/EventSubscriber/ConstructeurAuditSubscriber.php @@ -0,0 +1,168 @@ +getObjectManager(); + if (!$em instanceof EntityManagerInterface) { + return; + } + + $uow = $em->getUnitOfWork(); + $actorProfileId = $this->resolveActorProfileId(); + + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if (!$entity instanceof Constructeur) { + continue; + } + + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + $snapshot = $this->snapshotConstructeur($entity); + $this->persistAuditLog($em, new AuditLog('constructeur', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId)); + } + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if (!$entity instanceof Constructeur) { + continue; + } + + $id = (string) $entity->getId(); + if ('' === $id) { + continue; + } + + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + if ([] !== $diff) { + $snapshot = $this->snapshotConstructeur($entity); + $this->persistAuditLog($em, new AuditLog('constructeur', $id, 'update', $diff, $snapshot, $actorProfileId)); + } + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + if (!$entity instanceof Constructeur) { + continue; + } + + $snapshot = $this->snapshotConstructeur($entity); + $this->persistAuditLog($em, new AuditLog('constructeur', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId)); + } + } + + private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void + { + $uow = $em->getUnitOfWork(); + $log->initializeAuditLog(); + $em->persist($log); + + $meta = $em->getClassMetadata(AuditLog::class); + $uow->computeChangeSet($meta, $log); + } + + /** + * @param array $changeSet + * + * @return array + */ + private function buildDiffFromChangeSet(array $changeSet): array + { + $diff = []; + foreach ($changeSet as $field => [$oldValue, $newValue]) { + if ('updatedAt' === $field || 'createdAt' === $field) { + continue; + } + + $normalizedOld = $this->normalizeValue($oldValue); + $normalizedNew = $this->normalizeValue($newValue); + + if ($normalizedOld === $normalizedNew) { + continue; + } + + $diff[$field] = [ + 'from' => $normalizedOld, + 'to' => $normalizedNew, + ]; + } + + return $diff; + } + + private function snapshotConstructeur(Constructeur $constructeur): array + { + return [ + 'id' => $constructeur->getId(), + 'name' => $constructeur->getName(), + 'email' => $constructeur->getEmail(), + 'phone' => $constructeur->getPhone(), + ]; + } + + private function normalizeValue(mixed $value): mixed + { + if (null === $value || is_scalar($value)) { + return $value; + } + + if ($value instanceof DateTimeInterface) { + return $value->format(DateTimeInterface::ATOM); + } + + return (string) $value; + } + + private 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; + } +} diff --git a/src/EventSubscriber/DocumentAuditSubscriber.php b/src/EventSubscriber/DocumentAuditSubscriber.php new file mode 100644 index 0000000..f40cdeb --- /dev/null +++ b/src/EventSubscriber/DocumentAuditSubscriber.php @@ -0,0 +1,192 @@ +getObjectManager(); + if (!$em instanceof EntityManagerInterface) { + return; + } + + $uow = $em->getUnitOfWork(); + $actorProfileId = $this->resolveActorProfileId(); + + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if (!$entity instanceof Document) { + continue; + } + + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + $snapshot = $this->snapshotDocument($entity); + $this->persistAuditLog($em, new AuditLog('document', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId)); + } + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if (!$entity instanceof Document) { + continue; + } + + $id = (string) $entity->getId(); + if ('' === $id) { + continue; + } + + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + if ([] !== $diff) { + $snapshot = $this->snapshotDocument($entity); + $this->persistAuditLog($em, new AuditLog('document', $id, 'update', $diff, $snapshot, $actorProfileId)); + } + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + if (!$entity instanceof Document) { + continue; + } + + $snapshot = $this->snapshotDocument($entity); + $this->persistAuditLog($em, new AuditLog('document', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId)); + } + } + + private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void + { + $uow = $em->getUnitOfWork(); + $log->initializeAuditLog(); + $em->persist($log); + + $meta = $em->getClassMetadata(AuditLog::class); + $uow->computeChangeSet($meta, $log); + } + + /** + * @param array $changeSet + * + * @return array + */ + private function buildDiffFromChangeSet(array $changeSet): array + { + $diff = []; + foreach ($changeSet as $field => [$oldValue, $newValue]) { + if ('updatedAt' === $field || 'createdAt' === $field) { + continue; + } + + $normalizedOld = $this->normalizeValue($oldValue); + $normalizedNew = $this->normalizeValue($newValue); + + if ($normalizedOld === $normalizedNew) { + continue; + } + + $diff[$field] = [ + 'from' => $normalizedOld, + 'to' => $normalizedNew, + ]; + } + + return $diff; + } + + private function snapshotDocument(Document $document): array + { + return [ + 'id' => $document->getId(), + 'name' => $document->getName(), + 'filename' => $document->getFilename(), + 'mimeType' => $document->getMimeType(), + 'size' => $document->getSize(), + 'machine' => $this->normalizeValue($document->getMachine()), + 'composant' => $this->normalizeValue($document->getComposant()), + 'piece' => $this->normalizeValue($document->getPiece()), + 'product' => $this->normalizeValue($document->getProduct()), + 'site' => $this->normalizeValue($document->getSite()), + ]; + } + + private function normalizeValue(mixed $value): mixed + { + if (null === $value || is_scalar($value)) { + return $value; + } + + if ($value instanceof DateTimeInterface) { + return $value->format(DateTimeInterface::ATOM); + } + + if ($value instanceof Machine || $value instanceof Composant || $value instanceof Piece || $value instanceof Product || $value instanceof Site) { + return [ + 'id' => $value->getId(), + 'name' => $value->getName(), + ]; + } + + if (is_object($value) && method_exists($value, 'getId')) { + return (string) $value->getId(); + } + + return (string) $value; + } + + private 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; + } +} diff --git a/src/EventSubscriber/MachineAuditSubscriber.php b/src/EventSubscriber/MachineAuditSubscriber.php new file mode 100644 index 0000000..8d0a89f --- /dev/null +++ b/src/EventSubscriber/MachineAuditSubscriber.php @@ -0,0 +1,412 @@ +getObjectManager(); + if (!$em instanceof EntityManagerInterface) { + return; + } + + $uow = $em->getUnitOfWork(); + $actorProfileId = $this->resolveActorProfileId(); + $pendingUpdates = []; + $pendingSnapshots = []; + $pendingMachines = []; + + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if (!$entity instanceof Machine) { + continue; + } + + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + $snapshot = $this->snapshotMachine($entity); + $this->persistAuditLog($em, new AuditLog('machine', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId)); + } + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if (!$entity instanceof Machine) { + continue; + } + + $machineId = (string) $entity->getId(); + if ('' === $machineId) { + continue; + } + + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + if ([] !== $diff) { + $pendingUpdates[$machineId] = $this->mergeDiffs($pendingUpdates[$machineId] ?? [], $diff); + $pendingSnapshots[$machineId] = $this->snapshotMachine($entity); + $pendingMachines[$machineId] = $entity; + } + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + if (!$entity instanceof Machine) { + continue; + } + + $snapshot = $this->snapshotMachine($entity); + $this->persistAuditLog($em, new AuditLog('machine', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId)); + } + + foreach ($uow->getScheduledCollectionUpdates() as $collection) { + $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingMachines); + } + foreach ($uow->getScheduledCollectionDeletions() as $collection) { + $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingMachines); + } + + $this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingMachines); + + foreach ($pendingUpdates as $machineId => $diff) { + if ([] === $diff) { + continue; + } + + $machine = $pendingMachines[$machineId] ?? null; + if (!$machine instanceof Machine) { + continue; + } + + $snapshot = $pendingSnapshots[$machineId] ?? $this->snapshotMachine($machine); + $this->persistAuditLog($em, new AuditLog('machine', $machineId, 'update', $diff, $snapshot, $actorProfileId)); + } + } + + /** + * @param array> $pendingUpdates + * @param array> $pendingSnapshots + * @param array $pendingMachines + */ + private function collectCollectionUpdate( + object $collection, + array &$pendingUpdates, + array &$pendingSnapshots, + array &$pendingMachines, + ): void { + if (!$collection instanceof PersistentCollection) { + return; + } + + $owner = $collection->getOwner(); + if (!$owner instanceof Machine) { + return; + } + + $machineId = (string) $owner->getId(); + if ('' === $machineId) { + return; + } + + $mapping = $collection->getMapping(); + $fieldName = $mapping['fieldName'] ?? null; + if ('constructeurs' !== $fieldName) { + return; + } + + $before = $this->normalizeCollection($collection->getSnapshot()); + $after = $this->normalizeCollection($collection->toArray()); + + if ($before === $after) { + return; + } + + $diff = [ + 'constructeurIds' => [ + 'from' => $before, + 'to' => $after, + ], + ]; + + $pendingUpdates[$machineId] = $this->mergeDiffs($pendingUpdates[$machineId] ?? [], $diff); + $pendingSnapshots[$machineId] = $this->snapshotMachine($owner); + $pendingMachines[$machineId] = $owner; + } + + /** + * @param array> $pendingUpdates + * @param array> $pendingSnapshots + * @param array $pendingMachines + */ + private function collectCustomFieldValueChanges( + UnitOfWork $uow, + array &$pendingUpdates, + array &$pendingSnapshots, + array &$pendingMachines, + ): void { + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if ($entity instanceof CustomFieldValue) { + $this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingMachines); + } + } + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if (!$entity instanceof CustomFieldValue) { + continue; + } + $changeSet = $uow->getEntityChangeSet($entity); + if (!isset($changeSet['value'])) { + continue; + } + [$oldVal, $newVal] = $changeSet['value']; + if ($oldVal !== $newVal) { + $this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingMachines); + } + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + if ($entity instanceof CustomFieldValue) { + $this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingMachines); + } + } + } + + /** + * @param array> $pendingUpdates + * @param array> $pendingSnapshots + * @param array $pendingMachines + */ + private function trackCustomFieldValueChange( + CustomFieldValue $cfv, + mixed $from, + mixed $to, + array &$pendingUpdates, + array &$pendingSnapshots, + array &$pendingMachines, + ): void { + $owner = $cfv->getMachine(); + if (!$owner instanceof Machine) { + return; + } + + $ownerId = (string) $owner->getId(); + if ('' === $ownerId) { + return; + } + + $fieldName = 'customField:'.$cfv->getCustomField()->getName(); + $diff = [$fieldName => ['from' => $from, 'to' => $to]]; + + $pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff); + $pendingSnapshots[$ownerId] = $this->snapshotMachine($owner); + $pendingMachines[$ownerId] = $owner; + } + + private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void + { + $uow = $em->getUnitOfWork(); + $log->initializeAuditLog(); + $em->persist($log); + + $meta = $em->getClassMetadata(AuditLog::class); + $uow->computeChangeSet($meta, $log); + } + + /** + * @param array $changeSet + * + * @return array + */ + private function buildDiffFromChangeSet(array $changeSet): array + { + $diff = []; + foreach ($changeSet as $field => [$oldValue, $newValue]) { + if ('updatedAt' === $field || 'createdAt' === $field) { + continue; + } + + $normalizedOld = $this->normalizeValue($oldValue); + $normalizedNew = $this->normalizeValue($newValue); + + if ($normalizedOld === $normalizedNew) { + continue; + } + + $diff[$field] = [ + 'from' => $normalizedOld, + 'to' => $normalizedNew, + ]; + } + + return $diff; + } + + private function snapshotMachine(Machine $machine): array + { + return [ + 'id' => $machine->getId(), + 'name' => $machine->getName(), + 'reference' => $machine->getReference(), + 'prix' => $machine->getPrix(), + 'site' => $this->normalizeValue($machine->getSite()), + 'typeMachine' => $this->normalizeValue($machine->getTypeMachine()), + 'constructeurIds' => $this->normalizeCollection($machine->getConstructeurs()), + ]; + } + + /** + * @param iterable $items + * + * @return list + */ + private function normalizeCollection(iterable $items): array + { + $entries = []; + $seen = []; + foreach ($items as $item) { + if (is_object($item) && method_exists($item, 'getId')) { + $id = $item->getId(); + if (null === $id || '' === $id || isset($seen[(string) $id])) { + continue; + } + $seen[(string) $id] = true; + if (method_exists($item, 'getName')) { + $entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()]; + } else { + $entries[] = (string) $id; + } + } + } + + return $entries; + } + + private function normalizeValue(mixed $value): mixed + { + if (null === $value || is_scalar($value)) { + return $value; + } + + if ($value instanceof DateTimeInterface) { + return $value->format(DateTimeInterface::ATOM); + } + + if ($value instanceof Site) { + return [ + 'id' => $value->getId(), + 'name' => $value->getName(), + ]; + } + + if ($value instanceof TypeMachine) { + return [ + 'id' => $value->getId(), + 'name' => $value->getName(), + ]; + } + + if ($value instanceof ModelType) { + return [ + 'id' => $value->getId(), + 'name' => $value->getName(), + 'code' => $value->getCode(), + ]; + } + + if ($value instanceof Product) { + return [ + 'id' => $value->getId(), + 'name' => $value->getName(), + 'reference' => $value->getReference(), + ]; + } + + if ($value instanceof Collection) { + return $this->normalizeCollection($value); + } + + if (is_object($value) && method_exists($value, 'getId')) { + return (string) $value->getId(); + } + + if (is_array($value)) { + return $value; + } + + return (string) $value; + } + + /** + * @param array $base + * @param array $extra + * + * @return array + */ + private function mergeDiffs(array $base, array $extra): array + { + foreach ($extra as $field => $change) { + $base[$field] = $change; + } + + return $base; + } + + private 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; + } +} diff --git a/src/EventSubscriber/ModelTypeAuditSubscriber.php b/src/EventSubscriber/ModelTypeAuditSubscriber.php new file mode 100644 index 0000000..aa1528e --- /dev/null +++ b/src/EventSubscriber/ModelTypeAuditSubscriber.php @@ -0,0 +1,180 @@ +getObjectManager(); + if (!$em instanceof EntityManagerInterface) { + return; + } + + $uow = $em->getUnitOfWork(); + $actorProfileId = $this->resolveActorProfileId(); + + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if (!$entity instanceof ModelType) { + continue; + } + + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + $snapshot = $this->snapshotModelType($entity); + $this->persistAuditLog($em, new AuditLog('model_type', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId)); + } + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + if (!$entity instanceof ModelType) { + continue; + } + + $id = (string) $entity->getId(); + if ('' === $id) { + continue; + } + + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + if ([] !== $diff) { + $snapshot = $this->snapshotModelType($entity); + $this->persistAuditLog($em, new AuditLog('model_type', $id, 'update', $diff, $snapshot, $actorProfileId)); + } + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + if (!$entity instanceof ModelType) { + continue; + } + + $snapshot = $this->snapshotModelType($entity); + $this->persistAuditLog($em, new AuditLog('model_type', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId)); + } + } + + private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void + { + $uow = $em->getUnitOfWork(); + $log->initializeAuditLog(); + $em->persist($log); + + $meta = $em->getClassMetadata(AuditLog::class); + $uow->computeChangeSet($meta, $log); + } + + /** + * @param array $changeSet + * + * @return array + */ + private function buildDiffFromChangeSet(array $changeSet): array + { + $diff = []; + foreach ($changeSet as $field => [$oldValue, $newValue]) { + if ('updatedAt' === $field || 'createdAt' === $field) { + continue; + } + + $normalizedOld = $this->normalizeValue($oldValue); + $normalizedNew = $this->normalizeValue($newValue); + + if ($normalizedOld === $normalizedNew) { + continue; + } + + $diff[$field] = [ + 'from' => $normalizedOld, + 'to' => $normalizedNew, + ]; + } + + return $diff; + } + + private function snapshotModelType(ModelType $modelType): array + { + return [ + 'id' => $modelType->getId(), + 'name' => $modelType->getName(), + 'code' => $modelType->getCode(), + 'category' => $modelType->getCategory()->value, + 'notes' => $modelType->getNotes(), + 'description' => $modelType->getDescription(), + ]; + } + + private function normalizeValue(mixed $value): mixed + { + if (null === $value || is_scalar($value)) { + return $value; + } + + if ($value instanceof DateTimeInterface) { + return $value->format(DateTimeInterface::ATOM); + } + + if ($value instanceof ModelCategory) { + return $value->value; + } + + if (is_array($value)) { + return $value; + } + + return (string) $value; + } + + private 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; + } +} diff --git a/src/Service/ModelTypeCategoryConversionService.php b/src/Service/ModelTypeCategoryConversionService.php index fbc2775..1125be8 100644 --- a/src/Service/ModelTypeCategoryConversionService.php +++ b/src/Service/ModelTypeCategoryConversionService.php @@ -4,10 +4,14 @@ 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 @@ -15,6 +19,8 @@ final class ModelTypeCategoryConversionService public function __construct( private readonly Connection $connection, private readonly ModelTypeRepository $modelTypes, + private readonly RequestStack $requestStack, + private readonly Security $security, ) {} /** @@ -76,6 +82,11 @@ final class ModelTypeCategoryConversionService $category = $modelType->getCategory(); + $direction = ModelCategory::PIECE === $category ? 'piece_to_component' : 'component_to_piece'; + $names = $check['names']; + $modelName = $modelType->getName(); + $modelCode = $modelType->getCode(); + $this->connection->beginTransaction(); try { @@ -85,6 +96,8 @@ final class ModelTypeCategoryConversionService $count = $this->convertComponentToPiece($modelTypeId); } + $this->logConversionAudit($modelTypeId, $modelName, $modelCode, $direction, $count, $names); + $this->connection->commit(); return ['success' => true, 'convertedCount' => $count, 'error' => null]; @@ -415,4 +428,67 @@ final class ModelTypeCategoryConversionService return $count; } + + /** + * @param list $names + */ + private function logConversionAudit( + string $modelTypeId, + string $modelName, + string $modelCode, + string $direction, + int $convertedCount, + array $names, + ): void { + $now = new DateTimeImmutable()->format('Y-m-d H:i:s'); + $id = 'cl'.bin2hex(random_bytes(12)); + + $snapshot = [ + 'id' => $modelTypeId, + 'name' => $modelName, + 'code' => $modelCode, + ]; + + $diff = [ + 'direction' => ['from' => null, 'to' => $direction], + 'convertedCount' => ['from' => null, 'to' => $convertedCount], + 'convertedNames' => ['from' => null, 'to' => $names], + ]; + + $this->connection->executeStatement( + 'INSERT INTO audit_logs (id, entitytype, entityid, action, diff, snapshot, actorprofileid, createdat) + VALUES (:id, :entityType, :entityId, :action, :diff, :snapshot, :actor, :now)', + [ + 'id' => $id, + 'entityType' => 'model_type', + 'entityId' => $modelTypeId, + 'action' => 'convert', + 'diff' => json_encode($diff), + 'snapshot' => json_encode($snapshot), + 'actor' => $this->resolveActorProfileId(), + '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; + } }