diff --git a/src/EventSubscriber/ComposantAuditSubscriber.php b/src/EventSubscriber/ComposantAuditSubscriber.php index 100bcdb..97cf099 100644 --- a/src/EventSubscriber/ComposantAuditSubscriber.php +++ b/src/EventSubscriber/ComposantAuditSubscriber.php @@ -6,8 +6,11 @@ namespace App\EventSubscriber; use App\Entity\AuditLog; use App\Entity\Composant; +use App\Entity\CustomFieldValue; use App\Entity\ModelType; use App\Entity\Product; +use App\Entity\Profile; +use DateTimeInterface; use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; use Doctrine\Common\Collections\Collection; use Doctrine\Common\EventSubscriber; @@ -15,15 +18,24 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Events; use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\UnitOfWork; +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; +use function is_scalar; +use function method_exists; #[AsDoctrineListener(event: Events::onFlush)] final class ComposantAuditSubscriber implements EventSubscriber { - public function __construct(private readonly RequestStack $requestStack) - { - } + public function __construct( + private readonly RequestStack $requestStack, + private readonly Security $security, + ) {} public function getSubscribedEvents(): array { @@ -39,10 +51,10 @@ final class ComposantAuditSubscriber implements EventSubscriber return; } - $uow = $em->getUnitOfWork(); - $actorProfileId = $this->resolveActorProfileId(); - $pendingUpdates = []; - $pendingSnapshots = []; + $uow = $em->getUnitOfWork(); + $actorProfileId = $this->resolveActorProfileId(); + $pendingUpdates = []; + $pendingSnapshots = []; $pendingComponents = []; foreach ($uow->getScheduledEntityInsertions() as $entity) { @@ -50,7 +62,7 @@ final class ComposantAuditSubscriber implements EventSubscriber continue; } - $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); $snapshot = $this->snapshotComposant($entity); $this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId)); } @@ -61,14 +73,14 @@ final class ComposantAuditSubscriber implements EventSubscriber } $componentId = (string) $entity->getId(); - if ($componentId === '') { + if ('' === $componentId) { continue; } $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); - if ($diff !== []) { - $pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff); - $pendingSnapshots[$componentId] = $this->snapshotComposant($entity); + if ([] !== $diff) { + $pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff); + $pendingSnapshots[$componentId] = $this->snapshotComposant($entity); $pendingComponents[$componentId] = $entity; } } @@ -89,8 +101,10 @@ final class ComposantAuditSubscriber implements EventSubscriber $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents); } + $this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingComponents); + foreach ($pendingUpdates as $componentId => $diff) { - if ($diff === []) { + if ([] === $diff) { continue; } @@ -106,8 +120,8 @@ final class ComposantAuditSubscriber implements EventSubscriber /** * @param array> $pendingUpdates - * @param array> $pendingSnapshots - * @param array $pendingComponents + * @param array> $pendingSnapshots + * @param array $pendingComponents */ private function collectCollectionUpdate( object $collection, @@ -125,18 +139,18 @@ final class ComposantAuditSubscriber implements EventSubscriber } $componentId = (string) $owner->getId(); - if ($componentId === '') { + if ('' === $componentId) { return; } - $mapping = $collection->getMapping(); + $mapping = $collection->getMapping(); $fieldName = $mapping['fieldName'] ?? null; - if ($fieldName !== 'constructeurs') { + if ('constructeurs' !== $fieldName) { return; } $before = $this->normalizeCollection($collection->getSnapshot()); - $after = $this->normalizeCollection($collection->toArray()); + $after = $this->normalizeCollection($collection->toArray()); if ($before === $after) { return; @@ -145,15 +159,84 @@ final class ComposantAuditSubscriber implements EventSubscriber $diff = [ 'constructeurIds' => [ 'from' => $before, - 'to' => $after, + 'to' => $after, ], ]; - $pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff); - $pendingSnapshots[$componentId] = $this->snapshotComposant($owner); + $pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff); + $pendingSnapshots[$componentId] = $this->snapshotComposant($owner); $pendingComponents[$componentId] = $owner; } + /** + * @param array> $pendingUpdates + * @param array> $pendingSnapshots + * @param array $pendingComponents + */ + private function collectCustomFieldValueChanges( + UnitOfWork $uow, + array &$pendingUpdates, + array &$pendingSnapshots, + array &$pendingComponents, + ): void { + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if ($entity instanceof CustomFieldValue) { + $this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingComponents); + } + } + + 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, $pendingComponents); + } + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + if ($entity instanceof CustomFieldValue) { + $this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingComponents); + } + } + } + + /** + * @param array> $pendingUpdates + * @param array> $pendingSnapshots + * @param array $pendingComponents + */ + private function trackCustomFieldValueChange( + CustomFieldValue $cfv, + mixed $from, + mixed $to, + array &$pendingUpdates, + array &$pendingSnapshots, + array &$pendingComponents, + ): void { + $owner = $cfv->getComposant(); + if (!$owner instanceof Composant) { + 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->snapshotComposant($owner); + $pendingComponents[$ownerId] = $owner; + } + private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void { $uow = $em->getUnitOfWork(); @@ -166,13 +249,14 @@ final class ComposantAuditSubscriber implements EventSubscriber /** * @param array $changeSet + * * @return array */ private function buildDiffFromChangeSet(array $changeSet): array { $diff = []; foreach ($changeSet as $field => [$oldValue, $newValue]) { - if ($field === 'updatedAt' || $field === 'createdAt') { + if ('updatedAt' === $field || 'createdAt' === $field) { continue; } @@ -185,7 +269,7 @@ final class ComposantAuditSubscriber implements EventSubscriber $diff[$field] = [ 'from' => $normalizedOld, - 'to' => $normalizedNew, + 'to' => $normalizedNew, ]; } @@ -195,28 +279,29 @@ final class ComposantAuditSubscriber implements EventSubscriber private function snapshotComposant(Composant $component): array { return [ - 'id' => $component->getId(), - 'name' => $component->getName(), - 'reference' => $component->getReference(), - 'prix' => $component->getPrix(), - 'structure' => $component->getStructure(), - 'typeComposant' => $this->normalizeValue($component->getTypeComposant()), - 'product' => $this->normalizeValue($component->getProduct()), + 'id' => $component->getId(), + 'name' => $component->getName(), + 'reference' => $component->getReference(), + 'prix' => $component->getPrix(), + 'structure' => $component->getStructure(), + 'typeComposant' => $this->normalizeValue($component->getTypeComposant()), + 'product' => $this->normalizeValue($component->getProduct()), 'constructeurIds' => $this->normalizeCollection($component->getConstructeurs()), ]; } /** * @param iterable $items + * * @return list */ private function normalizeCollection(iterable $items): array { $ids = []; foreach ($items as $item) { - if (\is_object($item) && \method_exists($item, 'getId')) { + if (is_object($item) && method_exists($item, 'getId')) { $id = $item->getId(); - if ($id !== null && $id !== '') { + if (null !== $id && '' !== $id) { $ids[] = (string) $id; } } @@ -229,17 +314,17 @@ final class ComposantAuditSubscriber implements EventSubscriber private function normalizeValue(mixed $value): mixed { - if ($value === null || \is_scalar($value)) { + if (null === $value || is_scalar($value)) { return $value; } - if ($value instanceof \DateTimeInterface) { - return $value->format(\DateTimeInterface::ATOM); + if ($value instanceof DateTimeInterface) { + return $value->format(DateTimeInterface::ATOM); } if ($value instanceof ModelType) { return [ - 'id' => $value->getId(), + 'id' => $value->getId(), 'name' => $value->getName(), 'code' => $value->getCode(), ]; @@ -247,8 +332,8 @@ final class ComposantAuditSubscriber implements EventSubscriber if ($value instanceof Product) { return [ - 'id' => $value->getId(), - 'name' => $value->getName(), + 'id' => $value->getId(), + 'name' => $value->getName(), 'reference' => $value->getReference(), ]; } @@ -257,11 +342,11 @@ final class ComposantAuditSubscriber implements EventSubscriber return $this->normalizeCollection($value); } - if (\is_object($value) && \method_exists($value, 'getId')) { + if (is_object($value) && method_exists($value, 'getId')) { return (string) $value->getId(); } - if (\is_array($value)) { + if (is_array($value)) { return $value; } @@ -271,6 +356,7 @@ final class ComposantAuditSubscriber implements EventSubscriber /** * @param array $base * @param array $extra + * * @return array */ private function mergeDiffs(array $base, array $extra): array @@ -284,17 +370,23 @@ final class ComposantAuditSubscriber implements EventSubscriber private function resolveActorProfileId(): ?string { - $session = $this->requestStack->getSession(); - if (!$session instanceof SessionInterface) { - return null; + 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.) } - $profileId = $session->get('profileId'); - if (!$profileId) { - return null; + $user = $this->security->getUser(); + if ($user instanceof Profile) { + return $user->getId(); } - return (string) $profileId; + return null; } } - diff --git a/src/EventSubscriber/PieceAuditSubscriber.php b/src/EventSubscriber/PieceAuditSubscriber.php index c84bb37..2e2b5b1 100644 --- a/src/EventSubscriber/PieceAuditSubscriber.php +++ b/src/EventSubscriber/PieceAuditSubscriber.php @@ -5,9 +5,12 @@ declare(strict_types=1); namespace App\EventSubscriber; use App\Entity\AuditLog; +use App\Entity\CustomFieldValue; use App\Entity\ModelType; use App\Entity\Piece; use App\Entity\Product; +use App\Entity\Profile; +use DateTimeInterface; use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; use Doctrine\Common\Collections\Collection; use Doctrine\Common\EventSubscriber; @@ -15,15 +18,24 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Events; use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\UnitOfWork; +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; +use function is_scalar; +use function method_exists; #[AsDoctrineListener(event: Events::onFlush)] final class PieceAuditSubscriber implements EventSubscriber { - public function __construct(private readonly RequestStack $requestStack) - { - } + public function __construct( + private readonly RequestStack $requestStack, + private readonly Security $security, + ) {} public function getSubscribedEvents(): array { @@ -39,18 +51,18 @@ final class PieceAuditSubscriber implements EventSubscriber return; } - $uow = $em->getUnitOfWork(); - $actorProfileId = $this->resolveActorProfileId(); - $pendingUpdates = []; + $uow = $em->getUnitOfWork(); + $actorProfileId = $this->resolveActorProfileId(); + $pendingUpdates = []; $pendingSnapshots = []; - $pendingPieces = []; + $pendingPieces = []; foreach ($uow->getScheduledEntityInsertions() as $entity) { if (!$entity instanceof Piece) { continue; } - $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); $snapshot = $this->snapshotPiece($entity); $this->persistAuditLog($em, new AuditLog('piece', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId)); } @@ -61,15 +73,15 @@ final class PieceAuditSubscriber implements EventSubscriber } $pieceId = (string) $entity->getId(); - if ($pieceId === '') { + if ('' === $pieceId) { continue; } $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); - if ($diff !== []) { - $pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff); + if ([] !== $diff) { + $pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff); $pendingSnapshots[$pieceId] = $this->snapshotPiece($entity); - $pendingPieces[$pieceId] = $entity; + $pendingPieces[$pieceId] = $entity; } } @@ -89,8 +101,10 @@ final class PieceAuditSubscriber implements EventSubscriber $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingPieces); } + $this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingPieces); + foreach ($pendingUpdates as $pieceId => $diff) { - if ($diff === []) { + if ([] === $diff) { continue; } @@ -106,8 +120,8 @@ final class PieceAuditSubscriber implements EventSubscriber /** * @param array> $pendingUpdates - * @param array> $pendingSnapshots - * @param array $pendingPieces + * @param array> $pendingSnapshots + * @param array $pendingPieces */ private function collectCollectionUpdate( object $collection, @@ -125,18 +139,18 @@ final class PieceAuditSubscriber implements EventSubscriber } $pieceId = (string) $owner->getId(); - if ($pieceId === '') { + if ('' === $pieceId) { return; } - $mapping = $collection->getMapping(); + $mapping = $collection->getMapping(); $fieldName = $mapping['fieldName'] ?? null; - if ($fieldName !== 'constructeurs') { + if ('constructeurs' !== $fieldName) { return; } $before = $this->normalizeCollection($collection->getSnapshot()); - $after = $this->normalizeCollection($collection->toArray()); + $after = $this->normalizeCollection($collection->toArray()); if ($before === $after) { return; @@ -145,13 +159,82 @@ final class PieceAuditSubscriber implements EventSubscriber $diff = [ 'constructeurIds' => [ 'from' => $before, - 'to' => $after, + 'to' => $after, ], ]; - $pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff); + $pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff); $pendingSnapshots[$pieceId] = $this->snapshotPiece($owner); - $pendingPieces[$pieceId] = $owner; + $pendingPieces[$pieceId] = $owner; + } + + /** + * @param array> $pendingUpdates + * @param array> $pendingSnapshots + * @param array $pendingPieces + */ + private function collectCustomFieldValueChanges( + UnitOfWork $uow, + array &$pendingUpdates, + array &$pendingSnapshots, + array &$pendingPieces, + ): void { + foreach ($uow->getScheduledEntityInsertions() as $entity) { + if ($entity instanceof CustomFieldValue) { + $this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingPieces); + } + } + + 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, $pendingPieces); + } + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + if ($entity instanceof CustomFieldValue) { + $this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingPieces); + } + } + } + + /** + * @param array> $pendingUpdates + * @param array> $pendingSnapshots + * @param array $pendingPieces + */ + private function trackCustomFieldValueChange( + CustomFieldValue $cfv, + mixed $from, + mixed $to, + array &$pendingUpdates, + array &$pendingSnapshots, + array &$pendingPieces, + ): void { + $owner = $cfv->getPiece(); + if (!$owner instanceof Piece) { + 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->snapshotPiece($owner); + $pendingPieces[$ownerId] = $owner; } private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void @@ -166,13 +249,14 @@ final class PieceAuditSubscriber implements EventSubscriber /** * @param array $changeSet + * * @return array */ private function buildDiffFromChangeSet(array $changeSet): array { $diff = []; foreach ($changeSet as $field => [$oldValue, $newValue]) { - if ($field === 'updatedAt' || $field === 'createdAt') { + if ('updatedAt' === $field || 'createdAt' === $field) { continue; } @@ -185,7 +269,7 @@ final class PieceAuditSubscriber implements EventSubscriber $diff[$field] = [ 'from' => $normalizedOld, - 'to' => $normalizedNew, + 'to' => $normalizedNew, ]; } @@ -195,28 +279,29 @@ final class PieceAuditSubscriber implements EventSubscriber private function snapshotPiece(Piece $piece): array { return [ - 'id' => $piece->getId(), - 'name' => $piece->getName(), - 'reference' => $piece->getReference(), - 'prix' => $piece->getPrix(), - 'typePiece' => $this->normalizeValue($piece->getTypePiece()), - 'product' => $this->normalizeValue($piece->getProduct()), - 'productIds' => $piece->getProductIds(), + 'id' => $piece->getId(), + 'name' => $piece->getName(), + 'reference' => $piece->getReference(), + 'prix' => $piece->getPrix(), + 'typePiece' => $this->normalizeValue($piece->getTypePiece()), + 'product' => $this->normalizeValue($piece->getProduct()), + 'productIds' => $piece->getProductIds(), 'constructeurIds' => $this->normalizeCollection($piece->getConstructeurs()), ]; } /** * @param iterable $items + * * @return list */ private function normalizeCollection(iterable $items): array { $ids = []; foreach ($items as $item) { - if (\is_object($item) && \method_exists($item, 'getId')) { + if (is_object($item) && method_exists($item, 'getId')) { $id = $item->getId(); - if ($id !== null && $id !== '') { + if (null !== $id && '' !== $id) { $ids[] = (string) $id; } } @@ -229,17 +314,17 @@ final class PieceAuditSubscriber implements EventSubscriber private function normalizeValue(mixed $value): mixed { - if ($value === null || \is_scalar($value)) { + if (null === $value || is_scalar($value)) { return $value; } - if ($value instanceof \DateTimeInterface) { - return $value->format(\DateTimeInterface::ATOM); + if ($value instanceof DateTimeInterface) { + return $value->format(DateTimeInterface::ATOM); } if ($value instanceof ModelType) { return [ - 'id' => $value->getId(), + 'id' => $value->getId(), 'name' => $value->getName(), 'code' => $value->getCode(), ]; @@ -247,8 +332,8 @@ final class PieceAuditSubscriber implements EventSubscriber if ($value instanceof Product) { return [ - 'id' => $value->getId(), - 'name' => $value->getName(), + 'id' => $value->getId(), + 'name' => $value->getName(), 'reference' => $value->getReference(), ]; } @@ -257,11 +342,11 @@ final class PieceAuditSubscriber implements EventSubscriber return $this->normalizeCollection($value); } - if (\is_object($value) && \method_exists($value, 'getId')) { + if (is_object($value) && method_exists($value, 'getId')) { return (string) $value->getId(); } - if (\is_array($value)) { + if (is_array($value)) { return $value; } @@ -271,6 +356,7 @@ final class PieceAuditSubscriber implements EventSubscriber /** * @param array $base * @param array $extra + * * @return array */ private function mergeDiffs(array $base, array $extra): array @@ -284,17 +370,23 @@ final class PieceAuditSubscriber implements EventSubscriber private function resolveActorProfileId(): ?string { - $session = $this->requestStack->getSession(); - if (!$session instanceof SessionInterface) { - return null; + 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.) } - $profileId = $session->get('profileId'); - if (!$profileId) { - return null; + $user = $this->security->getUser(); + if ($user instanceof Profile) { + return $user->getId(); } - return (string) $profileId; + return null; } } - diff --git a/src/EventSubscriber/ProductAuditSubscriber.php b/src/EventSubscriber/ProductAuditSubscriber.php index 76e2aca..61a7c89 100644 --- a/src/EventSubscriber/ProductAuditSubscriber.php +++ b/src/EventSubscriber/ProductAuditSubscriber.php @@ -5,8 +5,11 @@ declare(strict_types=1); namespace App\EventSubscriber; use App\Entity\AuditLog; +use App\Entity\CustomFieldValue; use App\Entity\ModelType; use App\Entity\Product; +use App\Entity\Profile; +use DateTimeInterface; use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener; use Doctrine\Common\Collections\Collection; use Doctrine\Common\EventSubscriber; @@ -14,8 +17,16 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Events; use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\UnitOfWork; +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; +use function is_scalar; +use function method_exists; /** * Record a lightweight, per-product audit trail. @@ -27,9 +38,10 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface; #[AsDoctrineListener(event: Events::onFlush)] final class ProductAuditSubscriber implements EventSubscriber { - public function __construct(private readonly RequestStack $requestStack) - { - } + public function __construct( + private readonly RequestStack $requestStack, + private readonly Security $security, + ) {} public function getSubscribedEvents(): array { @@ -45,18 +57,18 @@ final class ProductAuditSubscriber implements EventSubscriber return; } - $uow = $em->getUnitOfWork(); - $actorProfileId = $this->resolveActorProfileId(); - $pendingUpdates = []; + $uow = $em->getUnitOfWork(); + $actorProfileId = $this->resolveActorProfileId(); + $pendingUpdates = []; $pendingSnapshots = []; - $pendingProducts = []; + $pendingProducts = []; foreach ($uow->getScheduledEntityInsertions() as $entity) { if (!$entity instanceof Product) { continue; } - $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); + $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); $snapshot = $this->snapshotProduct($entity); $this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId)); } @@ -67,15 +79,15 @@ final class ProductAuditSubscriber implements EventSubscriber } $productId = (string) $entity->getId(); - if ($productId === '') { + if ('' === $productId) { continue; } $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity)); - if ($diff !== []) { - $pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff); + if ([] !== $diff) { + $pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff); $pendingSnapshots[$productId] = $this->snapshotProduct($entity); - $pendingProducts[$productId] = $entity; + $pendingProducts[$productId] = $entity; } } @@ -96,8 +108,10 @@ final class ProductAuditSubscriber implements EventSubscriber $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts); } + $this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingProducts); + foreach ($pendingUpdates as $productId => $diff) { - if ($diff === []) { + if ([] === $diff) { continue; } @@ -113,8 +127,8 @@ final class ProductAuditSubscriber implements EventSubscriber /** * @param array> $pendingUpdates - * @param array> $pendingSnapshots - * @param array $pendingProducts + * @param array> $pendingSnapshots + * @param array $pendingProducts */ private function collectCollectionUpdate( object $collection, @@ -132,18 +146,18 @@ final class ProductAuditSubscriber implements EventSubscriber } $productId = (string) $owner->getId(); - if ($productId === '') { + if ('' === $productId) { return; } - $mapping = $collection->getMapping(); + $mapping = $collection->getMapping(); $fieldName = $mapping['fieldName'] ?? null; - if ($fieldName !== 'constructeurs') { + if ('constructeurs' !== $fieldName) { return; } $before = $this->normalizeCollection($collection->getSnapshot()); - $after = $this->normalizeCollection($collection->toArray()); + $after = $this->normalizeCollection($collection->toArray()); if ($before === $after) { return; @@ -152,13 +166,82 @@ final class ProductAuditSubscriber implements EventSubscriber $diff = [ 'constructeurIds' => [ 'from' => $before, - 'to' => $after, + 'to' => $after, ], ]; - $pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff); + $pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff); $pendingSnapshots[$productId] = $this->snapshotProduct($owner); - $pendingProducts[$productId] = $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 @@ -174,6 +257,7 @@ final class ProductAuditSubscriber implements EventSubscriber /** * @param array $changeSet + * * @return array */ private function buildDiffFromChangeSet(array $changeSet): array @@ -181,7 +265,7 @@ final class ProductAuditSubscriber implements EventSubscriber $diff = []; foreach ($changeSet as $field => [$oldValue, $newValue]) { // Skip noisy timestamps managed automatically. - if ($field === 'updatedAt' || $field === 'createdAt') { + if ('updatedAt' === $field || 'createdAt' === $field) { continue; } @@ -194,7 +278,7 @@ final class ProductAuditSubscriber implements EventSubscriber $diff[$field] = [ 'from' => $normalizedOld, - 'to' => $normalizedNew, + 'to' => $normalizedNew, ]; } @@ -204,11 +288,11 @@ final class ProductAuditSubscriber implements EventSubscriber private function snapshotProduct(Product $product): array { return [ - 'id' => $product->getId(), - 'name' => $product->getName(), - 'reference' => $product->getReference(), - 'supplierPrice' => $product->getSupplierPrice(), - 'typeProduct' => $this->normalizeValue($product->getTypeProduct()), + 'id' => $product->getId(), + 'name' => $product->getName(), + 'reference' => $product->getReference(), + 'supplierPrice' => $product->getSupplierPrice(), + 'typeProduct' => $this->normalizeValue($product->getTypeProduct()), 'constructeurIds' => $this->normalizeCollection($product->getConstructeurs()), ]; } @@ -216,6 +300,7 @@ final class ProductAuditSubscriber implements EventSubscriber /** * @param array $base * @param array $extra + * * @return array */ private function mergeDiffs(array $base, array $extra): array @@ -229,15 +314,16 @@ final class ProductAuditSubscriber implements EventSubscriber /** * @param iterable $items + * * @return list */ private function normalizeCollection(iterable $items): array { $ids = []; foreach ($items as $item) { - if (\is_object($item) && \method_exists($item, 'getId')) { + if (is_object($item) && method_exists($item, 'getId')) { $id = $item->getId(); - if ($id !== null && $id !== '') { + if (null !== $id && '' !== $id) { $ids[] = (string) $id; } } @@ -250,17 +336,17 @@ final class ProductAuditSubscriber implements EventSubscriber private function normalizeValue(mixed $value): mixed { - if ($value === null || \is_scalar($value)) { + if (null === $value || is_scalar($value)) { return $value; } - if ($value instanceof \DateTimeInterface) { - return $value->format(\DateTimeInterface::ATOM); + if ($value instanceof DateTimeInterface) { + return $value->format(DateTimeInterface::ATOM); } if ($value instanceof ModelType) { return [ - 'id' => $value->getId(), + 'id' => $value->getId(), 'name' => $value->getName(), 'code' => $value->getCode(), ]; @@ -270,11 +356,11 @@ final class ProductAuditSubscriber implements EventSubscriber return $this->normalizeCollection($value); } - if (\is_object($value) && \method_exists($value, 'getId')) { + if (is_object($value) && method_exists($value, 'getId')) { return (string) $value->getId(); } - if (\is_array($value)) { + if (is_array($value)) { return $value; } @@ -283,16 +369,23 @@ final class ProductAuditSubscriber implements EventSubscriber private function resolveActorProfileId(): ?string { - $session = $this->requestStack->getSession(); - if (!$session instanceof SessionInterface) { - return null; + 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.) } - $profileId = $session->get('profileId'); - if (!$profileId) { - return null; + $user = $this->security->getUser(); + if ($user instanceof Profile) { + return $user->getId(); } - return (string) $profileId; + return null; } }