Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39e503ae18 | ||
|
|
70ed354c42 | ||
|
|
ba98ae37f4 | ||
|
|
906d39793f | ||
|
|
f970c1928d | ||
|
|
2a1d966b87 | ||
|
|
a393b62e9f | ||
|
|
1247f72af6 | ||
|
|
6735bf252c | ||
|
|
508066d39f | ||
|
|
70956c204e |
Submodule Inventory_frontend updated: 9f7dd12b34...675820532c
@@ -5,3 +5,5 @@ api_platform:
|
||||
stateless: false
|
||||
cache_headers:
|
||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||
pagination_items_per_page: 30
|
||||
pagination_maximum_items_per_page: 200
|
||||
|
||||
87
src/Controller/ActivityLogController.php
Normal file
87
src/Controller/ActivityLogController.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Repository\AuditLogRepository;
|
||||
use App\Repository\ProfileRepository;
|
||||
use DateTimeInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
final class ActivityLogController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuditLogRepository $auditLogs,
|
||||
private readonly ProfileRepository $profiles,
|
||||
) {}
|
||||
|
||||
#[Route('/api/activity-logs', name: 'api_activity_logs', methods: ['GET'])]
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$page = max(1, $request->query->getInt('page', 1));
|
||||
$itemsPerPage = min(100, max(1, $request->query->getInt('itemsPerPage', 30)));
|
||||
|
||||
$filters = [];
|
||||
if ($entityType = $request->query->get('entityType')) {
|
||||
$filters['entityType'] = $entityType;
|
||||
}
|
||||
if ($action = $request->query->get('action')) {
|
||||
$filters['action'] = $action;
|
||||
}
|
||||
|
||||
$result = $this->auditLogs->findAllPaginated($page, $itemsPerPage, $filters);
|
||||
|
||||
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||
static fn ($log) => $log->getActorProfileId(),
|
||||
$result['items'],
|
||||
))));
|
||||
|
||||
$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();
|
||||
$snapshot = $log->getSnapshot();
|
||||
|
||||
return [
|
||||
'id' => $log->getId(),
|
||||
'entityType' => $log->getEntityType(),
|
||||
'entityId' => $log->getEntityId(),
|
||||
'entityName' => $snapshot['name'] ?? null,
|
||||
'entityRef' => $snapshot['reference'] ?? null,
|
||||
'action' => $log->getAction(),
|
||||
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||
'actor' => $actorId
|
||||
? [
|
||||
'id' => $actorId,
|
||||
'label' => $actorMap[$actorId] ?? $actorId,
|
||||
]
|
||||
: null,
|
||||
'diff' => $log->getDiff(),
|
||||
'snapshot' => $snapshot,
|
||||
];
|
||||
},
|
||||
$result['items'],
|
||||
);
|
||||
|
||||
return new JsonResponse([
|
||||
'items' => array_values($items),
|
||||
'total' => $result['total'],
|
||||
'page' => $page,
|
||||
'itemsPerPage' => $itemsPerPage,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
#[ApiResource(
|
||||
normalizationContext: ['groups' => ['composant:read']],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 500
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Composant
|
||||
{
|
||||
@@ -144,7 +144,7 @@ class Composant
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource(
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 500
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Constructeur
|
||||
{
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use App\Repository\DocumentRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
@@ -13,7 +14,10 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
#[ORM\Entity(repositoryClass: DocumentRepository::class)]
|
||||
#[ORM\Table(name: 'documents')]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiResource]
|
||||
#[ApiResource(
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Document
|
||||
{
|
||||
#[ORM\Id]
|
||||
@@ -62,19 +66,19 @@ class Document
|
||||
private ?Site $site = 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;
|
||||
|
||||
#[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();
|
||||
}
|
||||
}
|
||||
@@ -82,12 +86,7 @@ class Document
|
||||
#[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
|
||||
@@ -222,13 +221,18 @@ class Document
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): \DateTimeImmutable
|
||||
public function getCreatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): \DateTimeImmutable
|
||||
public function getUpdatedAt(): DateTimeImmutable
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
private function generateCuid(): string
|
||||
{
|
||||
return 'cl'.bin2hex(random_bytes(12));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
|
||||
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
|
||||
use ApiPlatform\Metadata\ApiFilter;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
@@ -21,9 +22,10 @@ use Symfony\Component\Serializer\Annotation\Groups;
|
||||
#[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])]
|
||||
#[ORM\HasLifecycleCallbacks]
|
||||
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
|
||||
#[ApiResource(
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 500
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class ModelType
|
||||
{
|
||||
@@ -178,7 +180,7 @@ class ModelType
|
||||
|
||||
public function setName(string $name): static
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
#[ApiResource(
|
||||
normalizationContext: ['groups' => ['piece:read']],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 500
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Piece
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
#[ApiResource(
|
||||
normalizationContext: ['groups' => ['product:read']],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 500
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Product
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
new Delete(),
|
||||
],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 500
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class Site
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
new Delete(),
|
||||
],
|
||||
paginationClientItemsPerPage: true,
|
||||
paginationMaximumItemsPerPage: 500
|
||||
paginationMaximumItemsPerPage: 200
|
||||
)]
|
||||
class TypeMachine
|
||||
{
|
||||
|
||||
@@ -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<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Composant> $pendingComponents
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Composant> $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<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Composant> $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<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Composant> $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<string, array{0:mixed, 1:mixed}> $changeSet
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
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,51 +279,57 @@ 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<mixed> $items
|
||||
* @return list<string>
|
||||
*
|
||||
* @return list<array{id: string, name: string}|string>
|
||||
*/
|
||||
private function normalizeCollection(iterable $items): array
|
||||
{
|
||||
$ids = [];
|
||||
$entries = [];
|
||||
$seen = [];
|
||||
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 !== '') {
|
||||
$ids[] = (string) $id;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort($ids);
|
||||
|
||||
return array_values(array_unique($ids));
|
||||
return $entries;
|
||||
}
|
||||
|
||||
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 +337,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 +347,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 +361,7 @@ final class ComposantAuditSubscriber implements EventSubscriber
|
||||
/**
|
||||
* @param array<string, array{from:mixed, to:mixed}> $base
|
||||
* @param array<string, array{from:mixed, to:mixed}> $extra
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
private function mergeDiffs(array $base, array $extra): array
|
||||
@@ -284,17 +375,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Piece> $pendingPieces
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Piece> $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<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Piece> $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<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Piece> $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<string, array{0:mixed, 1:mixed}> $changeSet
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
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,51 +279,57 @@ 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<mixed> $items
|
||||
* @return list<string>
|
||||
*
|
||||
* @return list<array{id: string, name: string}|string>
|
||||
*/
|
||||
private function normalizeCollection(iterable $items): array
|
||||
{
|
||||
$ids = [];
|
||||
$entries = [];
|
||||
$seen = [];
|
||||
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 !== '') {
|
||||
$ids[] = (string) $id;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort($ids);
|
||||
|
||||
return array_values(array_unique($ids));
|
||||
return $entries;
|
||||
}
|
||||
|
||||
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 +337,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 +347,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 +361,7 @@ final class PieceAuditSubscriber implements EventSubscriber
|
||||
/**
|
||||
* @param array<string, array{from:mixed, to:mixed}> $base
|
||||
* @param array<string, array{from:mixed, to:mixed}> $extra
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
private function mergeDiffs(array $base, array $extra): array
|
||||
@@ -284,17 +375,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Product> $pendingProducts
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Product> $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<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Product> $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<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||
* @param array<string, Product> $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<string, array{0:mixed, 1:mixed}> $changeSet
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
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<string, array{from:mixed, to:mixed}> $base
|
||||
* @param array<string, array{from:mixed, to:mixed}> $extra
|
||||
*
|
||||
* @return array<string, array{from:mixed, to:mixed}>
|
||||
*/
|
||||
private function mergeDiffs(array $base, array $extra): array
|
||||
@@ -229,38 +314,44 @@ final class ProductAuditSubscriber implements EventSubscriber
|
||||
|
||||
/**
|
||||
* @param iterable<mixed> $items
|
||||
* @return list<string>
|
||||
*
|
||||
* @return list<array{id: string, name: string}|string>
|
||||
*/
|
||||
private function normalizeCollection(iterable $items): array
|
||||
{
|
||||
$ids = [];
|
||||
$entries = [];
|
||||
$seen = [];
|
||||
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 !== '') {
|
||||
$ids[] = (string) $id;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort($ids);
|
||||
|
||||
return array_values(array_unique($ids));
|
||||
return $entries;
|
||||
}
|
||||
|
||||
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 +361,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 +374,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,46 @@ final class AuditLogRepository extends ServiceEntityRepository
|
||||
->orderBy('a.createdAt', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{entityType?: string, action?: string} $filters
|
||||
*
|
||||
* @return array{items: list<AuditLog>, total: int}
|
||||
*/
|
||||
public function findAllPaginated(int $page = 1, int $itemsPerPage = 30, array $filters = []): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('a')
|
||||
->orderBy('a.createdAt', 'DESC')
|
||||
;
|
||||
|
||||
if (!empty($filters['entityType'])) {
|
||||
$qb->andWhere('a.entityType = :entityType')
|
||||
->setParameter('entityType', $filters['entityType'])
|
||||
;
|
||||
}
|
||||
|
||||
if (!empty($filters['action'])) {
|
||||
$qb->andWhere('a.action = :action')
|
||||
->setParameter('action', $filters['action'])
|
||||
;
|
||||
}
|
||||
|
||||
$countQb = clone $qb;
|
||||
$countQb->select('COUNT(a.id)')
|
||||
->resetDQLPart('orderBy')
|
||||
;
|
||||
$total = (int) $countQb->getQuery()->getSingleScalarResult();
|
||||
|
||||
$qb->setFirstResult(($page - 1) * $itemsPerPage)
|
||||
->setMaxResults($itemsPerPage)
|
||||
;
|
||||
|
||||
return [
|
||||
'items' => $qb->getQuery()->getResult(),
|
||||
'total' => $total,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user