getObjectManager(); if (!$em instanceof EntityManagerInterface) { return; } $uow = $em->getUnitOfWork(); $actorProfileId = $this->resolveActorProfileId(); $pendingUpdates = []; $pendingSnapshots = []; $pendingProducts = []; foreach ($uow->getScheduledEntityInsertions() as $entity) { if (!$entity instanceof Product) { continue; } $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); $snapshot = $this->snapshotProduct($entity); $this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId)); } foreach ($uow->getScheduledEntityUpdates() as $entity) { if (!$entity instanceof Product) { continue; } $productId = (string) $entity->getId(); if ('' === $productId) { continue; } $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); if ([] !== $diff) { $pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff); $pendingSnapshots[$productId] = $this->snapshotProduct($entity); $pendingProducts[$productId] = $entity; } } foreach ($uow->getScheduledEntityDeletions() as $entity) { if (!$entity instanceof Product) { continue; } $snapshot = $this->snapshotProduct($entity); $this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId)); } // Capture constructeur collection updates, which are not included in the change set. foreach ($uow->getScheduledCollectionUpdates() as $collection) { $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts); } foreach ($uow->getScheduledCollectionDeletions() as $collection) { $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts); } $this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingProducts); foreach ($pendingUpdates as $productId => $diff) { if ([] === $diff) { continue; } $product = $pendingProducts[$productId] ?? null; if (!$product instanceof Product) { continue; } $snapshot = $pendingSnapshots[$productId] ?? $this->snapshotProduct($product); $this->persistAuditLog($em, new AuditLog('product', $productId, 'update', $diff, $snapshot, $actorProfileId)); } } /** * @param array> $pendingUpdates * @param array> $pendingSnapshots * @param array $pendingProducts */ private function collectCollectionUpdate( object $collection, array &$pendingUpdates, array &$pendingSnapshots, array &$pendingProducts, ): void { if (!$collection instanceof PersistentCollection) { return; } $owner = $collection->getOwner(); if (!$owner instanceof Product) { return; } $productId = (string) $owner->getId(); if ('' === $productId) { 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[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff); $pendingSnapshots[$productId] = $this->snapshotProduct($owner); $pendingProducts[$productId] = $owner; } /** * @param array> $pendingUpdates * @param array> $pendingSnapshots * @param array $pendingProducts */ private function collectCustomFieldValueChanges( UnitOfWork $uow, array &$pendingUpdates, array &$pendingSnapshots, array &$pendingProducts, ): void { foreach ($uow->getScheduledEntityInsertions() as $entity) { if ($entity instanceof CustomFieldValue) { $this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingProducts); } } 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, $pendingProducts); } } foreach ($uow->getScheduledEntityDeletions() as $entity) { if ($entity instanceof CustomFieldValue) { $this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingProducts); } } } /** * @param array> $pendingUpdates * @param array> $pendingSnapshots * @param array $pendingProducts */ private function trackCustomFieldValueChange( CustomFieldValue $cfv, mixed $from, mixed $to, array &$pendingUpdates, array &$pendingSnapshots, array &$pendingProducts, ): void { $owner = $cfv->getProduct(); if (!$owner instanceof Product) { 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->snapshotProduct($owner); $pendingProducts[$ownerId] = $owner; } private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void { $uow = $em->getUnitOfWork(); // Ensure identifiers and timestamps are set even when persisting during onFlush. $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]) { // Skip noisy timestamps managed automatically. 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 snapshotProduct(Product $product): array { return [ 'id' => $product->getId(), 'name' => $product->getName(), 'reference' => $product->getReference(), 'supplierPrice' => $product->getSupplierPrice(), 'typeProduct' => $this->normalizeValue($product->getTypeProduct()), 'constructeurIds' => $this->normalizeCollection($product->getConstructeurs()), ]; } /** * @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; } /** * @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 ModelType) { return [ 'id' => $value->getId(), 'name' => $value->getName(), 'code' => $value->getCode(), ]; } 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; } 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; } }