Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adc44b99d3 | ||
|
|
60afeb4cfd | ||
|
|
02ff8b1a96 |
Submodule Inventory_frontend updated: 2fffe4a368...6bed715b7f
79
src/Controller/MachineHistoryController.php
Normal file
79
src/Controller/MachineHistoryController.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Repository\AuditLogRepository;
|
||||||
|
use App\Repository\MachineRepository;
|
||||||
|
use App\Repository\ProfileRepository;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
|
||||||
|
final class MachineHistoryController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly MachineRepository $machines,
|
||||||
|
private readonly AuditLogRepository $auditLogs,
|
||||||
|
private readonly ProfileRepository $profiles,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/machines/{id}/history', name: 'api_machine_history', methods: ['GET'])]
|
||||||
|
public function __invoke(string $id): JsonResponse
|
||||||
|
{
|
||||||
|
$machine = $this->machines->find($id);
|
||||||
|
if (!$machine) {
|
||||||
|
return new JsonResponse(
|
||||||
|
['message' => 'Machine introuvable.'],
|
||||||
|
Response::HTTP_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs = $this->auditLogs->findEntityHistory('machine', $id, 200);
|
||||||
|
|
||||||
|
$actorIds = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn ($log) => $log->getActorProfileId(),
|
||||||
|
$logs,
|
||||||
|
))));
|
||||||
|
|
||||||
|
$actorMap = [];
|
||||||
|
if ([] !== $actorIds) {
|
||||||
|
$profiles = $this->profiles->findBy(['id' => $actorIds]);
|
||||||
|
foreach ($profiles as $profile) {
|
||||||
|
$label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
|
||||||
|
if ('' === $label) {
|
||||||
|
$label = $profile->getEmail() ?? $profile->getId();
|
||||||
|
}
|
||||||
|
$actorMap[$profile->getId()] = $label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = array_map(
|
||||||
|
static function ($log) use ($actorMap) {
|
||||||
|
$actorId = $log->getActorProfileId();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => $log->getId(),
|
||||||
|
'action' => $log->getAction(),
|
||||||
|
'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
|
||||||
|
'actor' => $actorId
|
||||||
|
? [
|
||||||
|
'id' => $actorId,
|
||||||
|
'label' => $actorMap[$actorId] ?? $actorId,
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
'diff' => $log->getDiff(),
|
||||||
|
'snapshot' => $log->getSnapshot(),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
$logs,
|
||||||
|
);
|
||||||
|
|
||||||
|
return new JsonResponse([
|
||||||
|
'items' => array_values($items),
|
||||||
|
'total' => count($items),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\Composant;
|
use App\Entity\Composant;
|
||||||
|
use App\Entity\CustomField;
|
||||||
use App\Entity\Machine;
|
use App\Entity\Machine;
|
||||||
use App\Entity\MachineComponentLink;
|
use App\Entity\MachineComponentLink;
|
||||||
use App\Entity\MachinePieceLink;
|
use App\Entity\MachinePieceLink;
|
||||||
@@ -341,21 +342,28 @@ class MachineSkeletonController extends AbstractController
|
|||||||
$componentIndex = $this->indexNormalizedLinks($normalizedComponentLinks);
|
$componentIndex = $this->indexNormalizedLinks($normalizedComponentLinks);
|
||||||
$normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks);
|
$normalizedPieceLinks = $this->normalizePieceLinks($pieceLinks);
|
||||||
|
|
||||||
// Build component hierarchy
|
// Build component hierarchy – track which IDs are children
|
||||||
foreach ($normalizedComponentLinks as &$link) {
|
$childIds = [];
|
||||||
|
foreach ($normalizedComponentLinks as $link) {
|
||||||
$parentId = $link['parentComponentLinkId'] ?? null;
|
$parentId = $link['parentComponentLinkId'] ?? null;
|
||||||
if ($parentId && isset($componentIndex[$parentId])) {
|
if ($parentId && isset($componentIndex[$parentId])) {
|
||||||
$componentIndex[$parentId]['childLinks'][] = &$link;
|
$componentIndex[$parentId]['childLinks'][] = $link;
|
||||||
|
$childIds[$link['id']] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
unset($link);
|
|
||||||
|
|
||||||
// Add pieces to components recursively
|
// Add pieces to components recursively
|
||||||
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
|
$this->attachPiecesToComponents($componentIndex, $normalizedPieceLinks);
|
||||||
|
|
||||||
|
// Only return root-level components (exclude children already nested)
|
||||||
|
$rootComponents = array_filter(
|
||||||
|
$componentIndex,
|
||||||
|
static fn (array $link) => !isset($childIds[$link['id']]),
|
||||||
|
);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'machine' => $this->normalizeMachine($machine),
|
'machine' => $this->normalizeMachine($machine),
|
||||||
'componentLinks' => array_values($componentIndex),
|
'componentLinks' => array_values($rootComponents),
|
||||||
'pieceLinks' => $normalizedPieceLinks,
|
'pieceLinks' => $normalizedPieceLinks,
|
||||||
'productLinks' => $this->normalizeProductLinks($productLinks),
|
'productLinks' => $this->normalizeProductLinks($productLinks),
|
||||||
];
|
];
|
||||||
|
|||||||
168
src/EventSubscriber/ConstructeurAuditSubscriber.php
Normal file
168
src/EventSubscriber/ConstructeurAuditSubscriber.php
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\AuditLog;
|
||||||
|
use App\Entity\Constructeur;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
|
use Doctrine\Common\EventSubscriber;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function is_scalar;
|
||||||
|
|
||||||
|
#[AsDoctrineListener(event: Events::onFlush)]
|
||||||
|
final class ConstructeurAuditSubscriber implements EventSubscriber
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Events::onFlush,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onFlush(OnFlushEventArgs $args): void
|
||||||
|
{
|
||||||
|
$em = $args->getObjectManager();
|
||||||
|
if (!$em instanceof EntityManagerInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if (!$entity instanceof Constructeur) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
$snapshot = $this->snapshotConstructeur($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('constructeur', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof Constructeur) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (string) $entity->getId();
|
||||||
|
if ('' === $id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
if ([] !== $diff) {
|
||||||
|
$snapshot = $this->snapshotConstructeur($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('constructeur', $id, 'update', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if (!$entity instanceof Constructeur) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $this->snapshotConstructeur($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('constructeur', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
|
{
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$log->initializeAuditLog();
|
||||||
|
$em->persist($log);
|
||||||
|
|
||||||
|
$meta = $em->getClassMetadata(AuditLog::class);
|
||||||
|
$uow->computeChangeSet($meta, $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<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 ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedOld = $this->normalizeValue($oldValue);
|
||||||
|
$normalizedNew = $this->normalizeValue($newValue);
|
||||||
|
|
||||||
|
if ($normalizedOld === $normalizedNew) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff[$field] = [
|
||||||
|
'from' => $normalizedOld,
|
||||||
|
'to' => $normalizedNew,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function snapshotConstructeur(Constructeur $constructeur): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $constructeur->getId(),
|
||||||
|
'name' => $constructeur->getName(),
|
||||||
|
'email' => $constructeur->getEmail(),
|
||||||
|
'phone' => $constructeur->getPhone(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (null === $value || is_scalar($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof Profile) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
192
src/EventSubscriber/DocumentAuditSubscriber.php
Normal file
192
src/EventSubscriber/DocumentAuditSubscriber.php
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\AuditLog;
|
||||||
|
use App\Entity\Composant;
|
||||||
|
use App\Entity\Document;
|
||||||
|
use App\Entity\Machine;
|
||||||
|
use App\Entity\Piece;
|
||||||
|
use App\Entity\Product;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use App\Entity\Site;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
|
use Doctrine\Common\EventSubscriber;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
use function is_object;
|
||||||
|
use function is_scalar;
|
||||||
|
use function method_exists;
|
||||||
|
|
||||||
|
#[AsDoctrineListener(event: Events::onFlush)]
|
||||||
|
final class DocumentAuditSubscriber implements EventSubscriber
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Events::onFlush,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onFlush(OnFlushEventArgs $args): void
|
||||||
|
{
|
||||||
|
$em = $args->getObjectManager();
|
||||||
|
if (!$em instanceof EntityManagerInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if (!$entity instanceof Document) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
$snapshot = $this->snapshotDocument($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('document', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof Document) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (string) $entity->getId();
|
||||||
|
if ('' === $id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
if ([] !== $diff) {
|
||||||
|
$snapshot = $this->snapshotDocument($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('document', $id, 'update', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if (!$entity instanceof Document) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $this->snapshotDocument($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('document', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
|
{
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$log->initializeAuditLog();
|
||||||
|
$em->persist($log);
|
||||||
|
|
||||||
|
$meta = $em->getClassMetadata(AuditLog::class);
|
||||||
|
$uow->computeChangeSet($meta, $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<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 ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedOld = $this->normalizeValue($oldValue);
|
||||||
|
$normalizedNew = $this->normalizeValue($newValue);
|
||||||
|
|
||||||
|
if ($normalizedOld === $normalizedNew) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff[$field] = [
|
||||||
|
'from' => $normalizedOld,
|
||||||
|
'to' => $normalizedNew,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function snapshotDocument(Document $document): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $document->getId(),
|
||||||
|
'name' => $document->getName(),
|
||||||
|
'filename' => $document->getFilename(),
|
||||||
|
'mimeType' => $document->getMimeType(),
|
||||||
|
'size' => $document->getSize(),
|
||||||
|
'machine' => $this->normalizeValue($document->getMachine()),
|
||||||
|
'composant' => $this->normalizeValue($document->getComposant()),
|
||||||
|
'piece' => $this->normalizeValue($document->getPiece()),
|
||||||
|
'product' => $this->normalizeValue($document->getProduct()),
|
||||||
|
'site' => $this->normalizeValue($document->getSite()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (null === $value || is_scalar($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Machine || $value instanceof Composant || $value instanceof Piece || $value instanceof Product || $value instanceof Site) {
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'name' => $value->getName(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_object($value) && method_exists($value, 'getId')) {
|
||||||
|
return (string) $value->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof Profile) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
412
src/EventSubscriber/MachineAuditSubscriber.php
Normal file
412
src/EventSubscriber/MachineAuditSubscriber.php
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\AuditLog;
|
||||||
|
use App\Entity\CustomFieldValue;
|
||||||
|
use App\Entity\Machine;
|
||||||
|
use App\Entity\ModelType;
|
||||||
|
use App\Entity\Product;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use App\Entity\Site;
|
||||||
|
use App\Entity\TypeMachine;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\Common\EventSubscriber;
|
||||||
|
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 MachineAuditSubscriber implements EventSubscriber
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Events::onFlush,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onFlush(OnFlushEventArgs $args): void
|
||||||
|
{
|
||||||
|
$em = $args->getObjectManager();
|
||||||
|
if (!$em instanceof EntityManagerInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
|
$pendingUpdates = [];
|
||||||
|
$pendingSnapshots = [];
|
||||||
|
$pendingMachines = [];
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if (!$entity instanceof Machine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
$snapshot = $this->snapshotMachine($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('machine', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof Machine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$machineId = (string) $entity->getId();
|
||||||
|
if ('' === $machineId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
if ([] !== $diff) {
|
||||||
|
$pendingUpdates[$machineId] = $this->mergeDiffs($pendingUpdates[$machineId] ?? [], $diff);
|
||||||
|
$pendingSnapshots[$machineId] = $this->snapshotMachine($entity);
|
||||||
|
$pendingMachines[$machineId] = $entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if (!$entity instanceof Machine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $this->snapshotMachine($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('machine', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledCollectionUpdates() as $collection) {
|
||||||
|
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
}
|
||||||
|
foreach ($uow->getScheduledCollectionDeletions() as $collection) {
|
||||||
|
$this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
|
||||||
|
foreach ($pendingUpdates as $machineId => $diff) {
|
||||||
|
if ([] === $diff) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$machine = $pendingMachines[$machineId] ?? null;
|
||||||
|
if (!$machine instanceof Machine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $pendingSnapshots[$machineId] ?? $this->snapshotMachine($machine);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('machine', $machineId, 'update', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Machine> $pendingMachines
|
||||||
|
*/
|
||||||
|
private function collectCollectionUpdate(
|
||||||
|
object $collection,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingMachines,
|
||||||
|
): void {
|
||||||
|
if (!$collection instanceof PersistentCollection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$owner = $collection->getOwner();
|
||||||
|
if (!$owner instanceof Machine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$machineId = (string) $owner->getId();
|
||||||
|
if ('' === $machineId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapping = $collection->getMapping();
|
||||||
|
$fieldName = $mapping['fieldName'] ?? null;
|
||||||
|
if ('constructeurs' !== $fieldName) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = $this->normalizeCollection($collection->getSnapshot());
|
||||||
|
$after = $this->normalizeCollection($collection->toArray());
|
||||||
|
|
||||||
|
if ($before === $after) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = [
|
||||||
|
'constructeurIds' => [
|
||||||
|
'from' => $before,
|
||||||
|
'to' => $after,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$pendingUpdates[$machineId] = $this->mergeDiffs($pendingUpdates[$machineId] ?? [], $diff);
|
||||||
|
$pendingSnapshots[$machineId] = $this->snapshotMachine($owner);
|
||||||
|
$pendingMachines[$machineId] = $owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Machine> $pendingMachines
|
||||||
|
*/
|
||||||
|
private function collectCustomFieldValueChanges(
|
||||||
|
UnitOfWork $uow,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingMachines,
|
||||||
|
): void {
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof CustomFieldValue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$changeSet = $uow->getEntityChangeSet($entity);
|
||||||
|
if (!isset($changeSet['value'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[$oldVal, $newVal] = $changeSet['value'];
|
||||||
|
if ($oldVal !== $newVal) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if ($entity instanceof CustomFieldValue) {
|
||||||
|
$this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingMachines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, array<string, array{from:mixed, to:mixed}>> $pendingUpdates
|
||||||
|
* @param array<string, array<string, mixed>> $pendingSnapshots
|
||||||
|
* @param array<string, Machine> $pendingMachines
|
||||||
|
*/
|
||||||
|
private function trackCustomFieldValueChange(
|
||||||
|
CustomFieldValue $cfv,
|
||||||
|
mixed $from,
|
||||||
|
mixed $to,
|
||||||
|
array &$pendingUpdates,
|
||||||
|
array &$pendingSnapshots,
|
||||||
|
array &$pendingMachines,
|
||||||
|
): void {
|
||||||
|
$owner = $cfv->getMachine();
|
||||||
|
if (!$owner instanceof Machine) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ownerId = (string) $owner->getId();
|
||||||
|
if ('' === $ownerId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fieldName = 'customField:'.$cfv->getCustomField()->getName();
|
||||||
|
$diff = [$fieldName => ['from' => $from, 'to' => $to]];
|
||||||
|
|
||||||
|
$pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
|
||||||
|
$pendingSnapshots[$ownerId] = $this->snapshotMachine($owner);
|
||||||
|
$pendingMachines[$ownerId] = $owner;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
|
{
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$log->initializeAuditLog();
|
||||||
|
$em->persist($log);
|
||||||
|
|
||||||
|
$meta = $em->getClassMetadata(AuditLog::class);
|
||||||
|
$uow->computeChangeSet($meta, $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<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 ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedOld = $this->normalizeValue($oldValue);
|
||||||
|
$normalizedNew = $this->normalizeValue($newValue);
|
||||||
|
|
||||||
|
if ($normalizedOld === $normalizedNew) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff[$field] = [
|
||||||
|
'from' => $normalizedOld,
|
||||||
|
'to' => $normalizedNew,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function snapshotMachine(Machine $machine): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $machine->getId(),
|
||||||
|
'name' => $machine->getName(),
|
||||||
|
'reference' => $machine->getReference(),
|
||||||
|
'prix' => $machine->getPrix(),
|
||||||
|
'site' => $this->normalizeValue($machine->getSite()),
|
||||||
|
'typeMachine' => $this->normalizeValue($machine->getTypeMachine()),
|
||||||
|
'constructeurIds' => $this->normalizeCollection($machine->getConstructeurs()),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param iterable<mixed> $items
|
||||||
|
*
|
||||||
|
* @return list<array{id: string, name: string}|string>
|
||||||
|
*/
|
||||||
|
private function normalizeCollection(iterable $items): array
|
||||||
|
{
|
||||||
|
$entries = [];
|
||||||
|
$seen = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (is_object($item) && method_exists($item, 'getId')) {
|
||||||
|
$id = $item->getId();
|
||||||
|
if (null === $id || '' === $id || isset($seen[(string) $id])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$seen[(string) $id] = true;
|
||||||
|
if (method_exists($item, 'getName')) {
|
||||||
|
$entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
|
||||||
|
} else {
|
||||||
|
$entries[] = (string) $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (null === $value || is_scalar($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Site) {
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'name' => $value->getName(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof TypeMachine) {
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'name' => $value->getName(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof ModelType) {
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'name' => $value->getName(),
|
||||||
|
'code' => $value->getCode(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Product) {
|
||||||
|
return [
|
||||||
|
'id' => $value->getId(),
|
||||||
|
'name' => $value->getName(),
|
||||||
|
'reference' => $value->getReference(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof Collection) {
|
||||||
|
return $this->normalizeCollection($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_object($value) && method_exists($value, 'getId')) {
|
||||||
|
return (string) $value->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<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
|
||||||
|
{
|
||||||
|
foreach ($extra as $field => $change) {
|
||||||
|
$base[$field] = $change;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $base;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof Profile) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
180
src/EventSubscriber/ModelTypeAuditSubscriber.php
Normal file
180
src/EventSubscriber/ModelTypeAuditSubscriber.php
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\EventSubscriber;
|
||||||
|
|
||||||
|
use App\Entity\AuditLog;
|
||||||
|
use App\Entity\ModelType;
|
||||||
|
use App\Entity\Profile;
|
||||||
|
use App\Enum\ModelCategory;
|
||||||
|
use DateTimeInterface;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
|
||||||
|
use Doctrine\Common\EventSubscriber;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\Event\OnFlushEventArgs;
|
||||||
|
use Doctrine\ORM\Events;
|
||||||
|
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_scalar;
|
||||||
|
|
||||||
|
#[AsDoctrineListener(event: Events::onFlush)]
|
||||||
|
final class ModelTypeAuditSubscriber implements EventSubscriber
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly Security $security,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Events::onFlush,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onFlush(OnFlushEventArgs $args): void
|
||||||
|
{
|
||||||
|
$em = $args->getObjectManager();
|
||||||
|
if (!$em instanceof EntityManagerInterface) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$actorProfileId = $this->resolveActorProfileId();
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityInsertions() as $entity) {
|
||||||
|
if (!$entity instanceof ModelType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
$snapshot = $this->snapshotModelType($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('model_type', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityUpdates() as $entity) {
|
||||||
|
if (!$entity instanceof ModelType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = (string) $entity->getId();
|
||||||
|
if ('' === $id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
|
||||||
|
if ([] !== $diff) {
|
||||||
|
$snapshot = $this->snapshotModelType($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('model_type', $id, 'update', $diff, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($uow->getScheduledEntityDeletions() as $entity) {
|
||||||
|
if (!$entity instanceof ModelType) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $this->snapshotModelType($entity);
|
||||||
|
$this->persistAuditLog($em, new AuditLog('model_type', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
|
||||||
|
{
|
||||||
|
$uow = $em->getUnitOfWork();
|
||||||
|
$log->initializeAuditLog();
|
||||||
|
$em->persist($log);
|
||||||
|
|
||||||
|
$meta = $em->getClassMetadata(AuditLog::class);
|
||||||
|
$uow->computeChangeSet($meta, $log);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<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 ('updatedAt' === $field || 'createdAt' === $field) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalizedOld = $this->normalizeValue($oldValue);
|
||||||
|
$normalizedNew = $this->normalizeValue($newValue);
|
||||||
|
|
||||||
|
if ($normalizedOld === $normalizedNew) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$diff[$field] = [
|
||||||
|
'from' => $normalizedOld,
|
||||||
|
'to' => $normalizedNew,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function snapshotModelType(ModelType $modelType): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $modelType->getId(),
|
||||||
|
'name' => $modelType->getName(),
|
||||||
|
'code' => $modelType->getCode(),
|
||||||
|
'category' => $modelType->getCategory()->value,
|
||||||
|
'notes' => $modelType->getNotes(),
|
||||||
|
'description' => $modelType->getDescription(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (null === $value || is_scalar($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof DateTimeInterface) {
|
||||||
|
return $value->format(DateTimeInterface::ATOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value instanceof ModelCategory) {
|
||||||
|
return $value->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
// No session available (CLI context, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof Profile) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,14 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Entity\Profile;
|
||||||
use App\Enum\ModelCategory;
|
use App\Enum\ModelCategory;
|
||||||
use App\Repository\ModelTypeRepository;
|
use App\Repository\ModelTypeRepository;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
use Doctrine\DBAL\Connection;
|
use Doctrine\DBAL\Connection;
|
||||||
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
final class ModelTypeCategoryConversionService
|
final class ModelTypeCategoryConversionService
|
||||||
@@ -15,6 +19,8 @@ final class ModelTypeCategoryConversionService
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Connection $connection,
|
private readonly Connection $connection,
|
||||||
private readonly ModelTypeRepository $modelTypes,
|
private readonly ModelTypeRepository $modelTypes,
|
||||||
|
private readonly RequestStack $requestStack,
|
||||||
|
private readonly Security $security,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,6 +82,11 @@ final class ModelTypeCategoryConversionService
|
|||||||
|
|
||||||
$category = $modelType->getCategory();
|
$category = $modelType->getCategory();
|
||||||
|
|
||||||
|
$direction = ModelCategory::PIECE === $category ? 'piece_to_component' : 'component_to_piece';
|
||||||
|
$names = $check['names'];
|
||||||
|
$modelName = $modelType->getName();
|
||||||
|
$modelCode = $modelType->getCode();
|
||||||
|
|
||||||
$this->connection->beginTransaction();
|
$this->connection->beginTransaction();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -85,6 +96,8 @@ final class ModelTypeCategoryConversionService
|
|||||||
$count = $this->convertComponentToPiece($modelTypeId);
|
$count = $this->convertComponentToPiece($modelTypeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->logConversionAudit($modelTypeId, $modelName, $modelCode, $direction, $count, $names);
|
||||||
|
|
||||||
$this->connection->commit();
|
$this->connection->commit();
|
||||||
|
|
||||||
return ['success' => true, 'convertedCount' => $count, 'error' => null];
|
return ['success' => true, 'convertedCount' => $count, 'error' => null];
|
||||||
@@ -415,4 +428,67 @@ final class ModelTypeCategoryConversionService
|
|||||||
|
|
||||||
return $count;
|
return $count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $names
|
||||||
|
*/
|
||||||
|
private function logConversionAudit(
|
||||||
|
string $modelTypeId,
|
||||||
|
string $modelName,
|
||||||
|
string $modelCode,
|
||||||
|
string $direction,
|
||||||
|
int $convertedCount,
|
||||||
|
array $names,
|
||||||
|
): void {
|
||||||
|
$now = new DateTimeImmutable()->format('Y-m-d H:i:s');
|
||||||
|
$id = 'cl'.bin2hex(random_bytes(12));
|
||||||
|
|
||||||
|
$snapshot = [
|
||||||
|
'id' => $modelTypeId,
|
||||||
|
'name' => $modelName,
|
||||||
|
'code' => $modelCode,
|
||||||
|
];
|
||||||
|
|
||||||
|
$diff = [
|
||||||
|
'direction' => ['from' => null, 'to' => $direction],
|
||||||
|
'convertedCount' => ['from' => null, 'to' => $convertedCount],
|
||||||
|
'convertedNames' => ['from' => null, 'to' => $names],
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->connection->executeStatement(
|
||||||
|
'INSERT INTO audit_logs (id, entitytype, entityid, action, diff, snapshot, actorprofileid, createdat)
|
||||||
|
VALUES (:id, :entityType, :entityId, :action, :diff, :snapshot, :actor, :now)',
|
||||||
|
[
|
||||||
|
'id' => $id,
|
||||||
|
'entityType' => 'model_type',
|
||||||
|
'entityId' => $modelTypeId,
|
||||||
|
'action' => 'convert',
|
||||||
|
'diff' => json_encode($diff),
|
||||||
|
'snapshot' => json_encode($snapshot),
|
||||||
|
'actor' => $this->resolveActorProfileId(),
|
||||||
|
'now' => $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveActorProfileId(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$session = $this->requestStack->getSession();
|
||||||
|
if ($session instanceof SessionInterface) {
|
||||||
|
$profileId = $session->get('profileId');
|
||||||
|
if ($profileId) {
|
||||||
|
return (string) $profileId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->security->getUser();
|
||||||
|
if ($user instanceof Profile) {
|
||||||
|
return $user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user