diff --git a/phpunit.dist.xml b/phpunit.dist.xml
index 22bd879..294e6d1 100644
--- a/phpunit.dist.xml
+++ b/phpunit.dist.xml
@@ -4,7 +4,7 @@
+
diff --git a/src/Controller/ComposantHistoryController.php b/src/Controller/EntityHistoryController.php
similarity index 54%
rename from src/Controller/ComposantHistoryController.php
rename to src/Controller/EntityHistoryController.php
index 7610771..51554a3 100644
--- a/src/Controller/ComposantHistoryController.php
+++ b/src/Controller/EntityHistoryController.php
@@ -6,35 +6,76 @@ namespace App\Controller;
use App\Repository\AuditLogRepository;
use App\Repository\ComposantRepository;
+use App\Repository\MachineRepository;
+use App\Repository\PieceRepository;
+use App\Repository\ProductRepository;
use App\Repository\ProfileRepository;
use DateTimeInterface;
+use Doctrine\ORM\EntityRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
-final class ComposantHistoryController extends AbstractController
+final class EntityHistoryController extends AbstractController
{
+ /** @var array, label: string}> */
+ private readonly array $entityConfig;
+
public function __construct(
- private readonly ComposantRepository $components,
+ MachineRepository $machines,
+ PieceRepository $pieces,
+ ComposantRepository $composants,
+ ProductRepository $products,
private readonly AuditLogRepository $auditLogs,
private readonly ProfileRepository $profiles,
- ) {}
+ ) {
+ $this->entityConfig = [
+ 'machine' => ['repo' => $machines, 'label' => 'Machine introuvable.'],
+ 'piece' => ['repo' => $pieces, 'label' => 'Pièce introuvable.'],
+ 'composant' => ['repo' => $composants, 'label' => 'Composant introuvable.'],
+ 'product' => ['repo' => $products, 'label' => 'Produit introuvable.'],
+ ];
+ }
+
+ #[Route('/api/machines/{id}/history', name: 'api_machine_history', methods: ['GET'])]
+ public function machineHistory(string $id): JsonResponse
+ {
+ return $this->entityHistory('machine', $id);
+ }
+
+ #[Route('/api/pieces/{id}/history', name: 'api_piece_history', methods: ['GET'])]
+ public function pieceHistory(string $id): JsonResponse
+ {
+ return $this->entityHistory('piece', $id);
+ }
#[Route('/api/composants/{id}/history', name: 'api_composant_history', methods: ['GET'])]
- public function __invoke(string $id): JsonResponse
+ public function composantHistory(string $id): JsonResponse
+ {
+ return $this->entityHistory('composant', $id);
+ }
+
+ #[Route('/api/products/{id}/history', name: 'api_product_history', methods: ['GET'])]
+ public function productHistory(string $id): JsonResponse
+ {
+ return $this->entityHistory('product', $id);
+ }
+
+ private function entityHistory(string $type, string $id): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_VIEWER');
- $component = $this->components->find($id);
- if (!$component) {
+ $config = $this->entityConfig[$type];
+ $entity = $config['repo']->find($id);
+ if (!$entity) {
return new JsonResponse(
- ['message' => 'Composant introuvable.'],
+ ['message' => $config['label']],
Response::HTTP_NOT_FOUND,
);
}
- $logs = $this->auditLogs->findEntityHistory('composant', $id, 200);
+ $logs = $this->auditLogs->findEntityHistory($type, $id, 200);
$actorIds = array_values(array_unique(array_filter(array_map(
static fn ($log) => $log->getActorProfileId(),
diff --git a/src/Controller/MachineHistoryController.php b/src/Controller/MachineHistoryController.php
deleted file mode 100644
index 9d340b7..0000000
--- a/src/Controller/MachineHistoryController.php
+++ /dev/null
@@ -1,82 +0,0 @@
-denyAccessUnlessGranted('ROLE_VIEWER');
-
- $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),
- ]);
- }
-}
diff --git a/src/Controller/MachineStructureController.php b/src/Controller/MachineStructureController.php
index d046a06..50361d4 100644
--- a/src/Controller/MachineStructureController.php
+++ b/src/Controller/MachineStructureController.php
@@ -14,6 +14,7 @@ use App\Entity\MachineProductLink;
use App\Entity\ModelType;
use App\Entity\Piece;
use App\Entity\Product;
+use App\Entity\Site;
use App\Repository\ComposantRepository;
use App\Repository\MachineComponentLinkRepository;
use App\Repository\MachinePieceLinkRepository;
@@ -123,7 +124,7 @@ class MachineStructureController extends AbstractController
return $this->json(['success' => false, 'error' => 'name et siteId sont requis.'], 400);
}
- $site = $this->entityManager->getRepository(\App\Entity\Site::class)->find($payload['siteId']);
+ $site = $this->entityManager->getRepository(Site::class)->find($payload['siteId']);
if (!$site) {
return $this->json(['success' => false, 'error' => 'Site introuvable.'], 404);
}
@@ -772,7 +773,7 @@ class MachineStructureController extends AbstractController
if (!$cfv instanceof CustomFieldValue) {
continue;
}
- $cf = $cfv->getCustomField();
+ $cf = $cfv->getCustomField();
$items[] = [
'id' => $cfv->getId(),
'value' => $cfv->getValue(),
diff --git a/src/Controller/PieceHistoryController.php b/src/Controller/PieceHistoryController.php
deleted file mode 100644
index 36e7aee..0000000
--- a/src/Controller/PieceHistoryController.php
+++ /dev/null
@@ -1,82 +0,0 @@
-denyAccessUnlessGranted('ROLE_VIEWER');
-
- $piece = $this->pieces->find($id);
- if (!$piece) {
- return new JsonResponse(
- ['message' => 'Pièce introuvable.'],
- Response::HTTP_NOT_FOUND,
- );
- }
-
- $logs = $this->auditLogs->findEntityHistory('piece', $id, 200);
-
- $actorIds = array_values(array_unique(array_filter(array_map(
- static fn ($log) => $log->getActorProfileId(),
- $logs,
- ))));
-
- $actorMap = [];
- if ([] !== $actorIds) {
- $profiles = $this->profiles->findBy(['id' => $actorIds]);
- foreach ($profiles as $profile) {
- $label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
- if ('' === $label) {
- $label = $profile->getEmail() ?? $profile->getId();
- }
- $actorMap[$profile->getId()] = $label;
- }
- }
-
- $items = array_map(
- static function ($log) use ($actorMap) {
- $actorId = $log->getActorProfileId();
-
- return [
- 'id' => $log->getId(),
- 'action' => $log->getAction(),
- 'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
- 'actor' => $actorId
- ? [
- 'id' => $actorId,
- 'label' => $actorMap[$actorId] ?? $actorId,
- ]
- : null,
- 'diff' => $log->getDiff(),
- 'snapshot' => $log->getSnapshot(),
- ];
- },
- $logs,
- );
-
- return new JsonResponse([
- 'items' => array_values($items),
- 'total' => count($items),
- ]);
- }
-}
diff --git a/src/Controller/ProductHistoryController.php b/src/Controller/ProductHistoryController.php
deleted file mode 100644
index 7d7b6d6..0000000
--- a/src/Controller/ProductHistoryController.php
+++ /dev/null
@@ -1,82 +0,0 @@
-denyAccessUnlessGranted('ROLE_VIEWER');
-
- $product = $this->products->find($id);
- if (!$product) {
- return new JsonResponse(
- ['message' => 'Produit introuvable.'],
- Response::HTTP_NOT_FOUND,
- );
- }
-
- $logs = $this->auditLogs->findEntityHistory('product', $id, 200);
-
- $actorIds = array_values(array_unique(array_filter(array_map(
- static fn ($log) => $log->getActorProfileId(),
- $logs,
- ))));
-
- $actorMap = [];
- if ([] !== $actorIds) {
- $profiles = $this->profiles->findBy(['id' => $actorIds]);
- foreach ($profiles as $profile) {
- $label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName()));
- if ('' === $label) {
- $label = $profile->getEmail() ?? $profile->getId();
- }
- $actorMap[$profile->getId()] = $label;
- }
- }
-
- $items = array_map(
- static function ($log) use ($actorMap) {
- $actorId = $log->getActorProfileId();
-
- return [
- 'id' => $log->getId(),
- 'action' => $log->getAction(),
- 'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM),
- 'actor' => $actorId
- ? [
- 'id' => $actorId,
- 'label' => $actorMap[$actorId] ?? $actorId,
- ]
- : null,
- 'diff' => $log->getDiff(),
- 'snapshot' => $log->getSnapshot(),
- ];
- },
- $logs,
- );
-
- return new JsonResponse([
- 'items' => array_values($items),
- 'total' => count($items),
- ]);
- }
-}
diff --git a/src/Entity/Comment.php b/src/Entity/Comment.php
index 547c805..378c12d 100644
--- a/src/Entity/Comment.php
+++ b/src/Entity/Comment.php
@@ -12,6 +12,7 @@ use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
+use App\Entity\Trait\CuidEntityTrait;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
@@ -23,6 +24,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ApiFilter(SearchFilter::class, properties: ['entityType' => 'exact', 'entityId' => 'exact', 'status' => 'exact', 'entityName' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['createdAt', 'authorName', 'status'])]
#[ApiResource(
+ description: 'Commentaires et annotations. Permet aux utilisateurs de commenter les machines, pièces, composants, produits et catégories. Les commentaires peuvent être marqués comme résolus.',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -35,6 +37,8 @@ use Doctrine\ORM\Mapping as ORM;
)]
class Comment
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
@@ -75,29 +79,12 @@ class Comment
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updated_at')]
private DateTimeImmutable $updatedAt;
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
+ public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
}
- public function getId(): ?string
- {
- return $this->id;
- }
-
public function getContent(): string
{
return $this->content;
@@ -217,19 +204,4 @@ class Comment
return $this;
}
-
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getUpdatedAt(): DateTimeImmutable
- {
- return $this->updatedAt;
- }
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/Composant.php b/src/Entity/Composant.php
index fcb9f2f..d7033e2 100644
--- a/src/Entity/Composant.php
+++ b/src/Entity/Composant.php
@@ -14,6 +14,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\ComposantRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -28,6 +29,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeComposant' => 'exact', 'typeComposant.name' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiResource(
+ description: 'Composants du catalogue. Un composant représente un élément fonctionnel rattaché à une machine, avec un type, des fournisseurs et des documents.',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -42,6 +44,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
)]
class Composant
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['composant:read', 'document:list'])]
@@ -119,42 +123,14 @@ class Composant
public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = new DateTimeImmutable();
$this->constructeurs = new ArrayCollection();
$this->documents = new ArrayCollection();
$this->customFieldValues = new ArrayCollection();
$this->machineLinks = new ArrayCollection();
}
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
public function getName(): string
{
return $this->name;
@@ -294,19 +270,4 @@ class Composant
{
return $this->customFieldValues;
}
-
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getUpdatedAt(): DateTimeImmutable
- {
- return $this->updatedAt;
- }
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/Constructeur.php b/src/Entity/Constructeur.php
index 2bc9d1f..c710496 100644
--- a/src/Entity/Constructeur.php
+++ b/src/Entity/Constructeur.php
@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\ConstructeurRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -18,12 +19,14 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
+use Symfony\Component\Validator\Constraints as Assert;
#[UniqueEntity(fields: ['name'], message: 'Un fournisseur avec ce nom existe déjà.')]
#[ORM\Entity(repositoryClass: ConstructeurRepository::class)]
#[ORM\Table(name: 'constructeurs')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
+ description: 'Fournisseurs et constructeurs. Référentiel partagé entre les machines, pièces, composants et produits pour identifier les fabricants et distributeurs.',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -37,12 +40,15 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
)]
class Constructeur
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)]
- private string $name;
+ #[Assert\NotBlank(message: 'Le nom est obligatoire.')]
+ private ?string $name = null;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
private ?string $email = null;
@@ -82,43 +88,15 @@ class Constructeur
public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = new DateTimeImmutable();
$this->machines = new ArrayCollection();
$this->composants = new ArrayCollection();
$this->pieces = new ArrayCollection();
$this->products = new ArrayCollection();
}
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
- public function getName(): string
+ public function getName(): ?string
{
return $this->name;
}
@@ -153,19 +131,4 @@ class Constructeur
return $this;
}
-
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getUpdatedAt(): DateTimeImmutable
- {
- return $this->updatedAt;
- }
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/CustomField.php b/src/Entity/CustomField.php
index e721e6d..2f12d52 100644
--- a/src/Entity/CustomField.php
+++ b/src/Entity/CustomField.php
@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\CustomFieldRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -23,6 +24,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Table(name: 'custom_fields')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
+ description: 'Définitions de champs personnalisés. Permet de créer des champs dynamiques (texte, nombre, date, etc.) applicables aux machines, pièces, composants et produits.',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -34,6 +36,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
)]
class CustomField
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
@@ -92,39 +96,11 @@ class CustomField
public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = new DateTimeImmutable();
$this->customFieldValues = new ArrayCollection();
}
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
public function getName(): string
{
return $this->name;
@@ -208,19 +184,4 @@ class CustomField
return $this;
}
-
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getUpdatedAt(): DateTimeImmutable
- {
- return $this->updatedAt;
- }
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/CustomFieldValue.php b/src/Entity/CustomFieldValue.php
index 1be1177..507d028 100644
--- a/src/Entity/CustomFieldValue.php
+++ b/src/Entity/CustomFieldValue.php
@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\CustomFieldValueRepository;
use DateTimeImmutable;
use Doctrine\DBAL\Types\Types;
@@ -21,6 +22,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Table(name: 'custom_field_values')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
+ description: 'Valeurs des champs personnalisés. Stocke la valeur concrète d\'un champ personnalisé pour une entité donnée (machine, pièce, composant ou produit).',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -32,6 +34,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
)]
class CustomFieldValue
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
@@ -70,36 +74,12 @@ class CustomFieldValue
#[Groups(['composant:read', 'piece:read', 'product:read', 'machine:read'])]
private DateTimeImmutable $updatedAt;
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
+ public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
}
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
public function getValue(): string
{
return $this->value;
@@ -171,19 +151,4 @@ class CustomFieldValue
return $this;
}
-
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getUpdatedAt(): DateTimeImmutable
- {
- return $this->updatedAt;
- }
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/Machine.php b/src/Entity/Machine.php
index 8210980..fe0901d 100644
--- a/src/Entity/Machine.php
+++ b/src/Entity/Machine.php
@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\MachineRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -18,11 +19,13 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
+use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: MachineRepository::class)]
#[ORM\Table(name: 'machines')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
+ description: 'Machines industrielles rattachées à un site. Chaque machine possède une structure hiérarchique de composants, pièces et produits, ainsi que des champs personnalisés et des documents.',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -34,6 +37,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
)]
class Machine
{
+ use CuidEntityTrait;
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['document:list'])]
@@ -51,7 +55,8 @@ class Machine
#[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'machines')]
#[ORM\JoinColumn(name: 'siteId', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
- private Site $site;
+ #[Assert\NotNull(message: 'Le site est obligatoire.')]
+ private ?Site $site = null;
/**
* @var Collection
@@ -108,6 +113,9 @@ class Machine
public function __construct()
{
+ $now = new DateTimeImmutable();
+ $this->createdAt = $now;
+ $this->updatedAt = $now;
$this->constructeurs = new ArrayCollection();
$this->componentLinks = new ArrayCollection();
$this->pieceLinks = new ArrayCollection();
@@ -117,36 +125,6 @@ class Machine
$this->customFieldValues = new ArrayCollection();
}
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
public function getName(): string
{
return $this->name;
@@ -183,12 +161,12 @@ class Machine
return $this;
}
- public function getSite(): Site
+ public function getSite(): ?Site
{
return $this->site;
}
- public function setSite(Site $site): static
+ public function setSite(?Site $site): static
{
$this->site = $site;
@@ -271,19 +249,4 @@ class Machine
{
return $this->customFieldValues;
}
-
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getUpdatedAt(): DateTimeImmutable
- {
- return $this->updatedAt;
- }
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/MachineComponentLink.php b/src/Entity/MachineComponentLink.php
index f907850..a815a41 100644
--- a/src/Entity/MachineComponentLink.php
+++ b/src/Entity/MachineComponentLink.php
@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\MachineComponentLinkRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -22,6 +23,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Table(name: 'machine_component_links')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
+ description: 'Liaisons machine–composant. Représente le rattachement d\'un composant à une machine, avec quantité, position dans la hiérarchie et surcharges éventuelles (nom, référence).',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -33,6 +35,8 @@ use Doctrine\ORM\Mapping as ORM;
)]
class MachineComponentLink
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
@@ -84,41 +88,13 @@ class MachineComponentLink
public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = new DateTimeImmutable();
$this->childLinks = new ArrayCollection();
$this->pieceLinks = new ArrayCollection();
$this->productLinks = new ArrayCollection();
}
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
public function getMachine(): Machine
{
return $this->machine;
@@ -190,9 +166,4 @@ class MachineComponentLink
return $this;
}
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/MachinePieceLink.php b/src/Entity/MachinePieceLink.php
index 19b3b47..54c8e39 100644
--- a/src/Entity/MachinePieceLink.php
+++ b/src/Entity/MachinePieceLink.php
@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\MachinePieceLinkRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -22,6 +23,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Table(name: 'machine_piece_links')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
+ description: 'Liaisons machine–pièce. Représente le rattachement d\'une pièce à une machine, avec quantité, position dans la hiérarchie et surcharges éventuelles (nom, référence, prix).',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -33,6 +35,8 @@ use Doctrine\ORM\Mapping as ORM;
)]
class MachinePieceLink
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
@@ -72,39 +76,11 @@ class MachinePieceLink
public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = new DateTimeImmutable();
$this->productLinks = new ArrayCollection();
}
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
public function getMachine(): Machine
{
return $this->machine;
@@ -176,9 +152,4 @@ class MachinePieceLink
return $this;
}
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/MachineProductLink.php b/src/Entity/MachineProductLink.php
index db11867..677e5db 100644
--- a/src/Entity/MachineProductLink.php
+++ b/src/Entity/MachineProductLink.php
@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\MachineProductLinkRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -22,6 +23,7 @@ use Doctrine\ORM\Mapping as ORM;
#[ORM\Table(name: 'machine_product_links')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
+ description: 'Liaisons machine–produit. Représente le rattachement d\'un produit à une machine, avec quantité, position dans la hiérarchie et surcharges éventuelles (nom, référence, prix).',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -33,6 +35,8 @@ use Doctrine\ORM\Mapping as ORM;
)]
class MachineProductLink
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
private ?string $id = null;
@@ -71,39 +75,11 @@ class MachineProductLink
public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = new DateTimeImmutable();
$this->childLinks = new ArrayCollection();
}
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
public function getMachine(): Machine
{
return $this->machine;
@@ -163,9 +139,4 @@ class MachineProductLink
return $this;
}
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/ModelType.php b/src/Entity/ModelType.php
index ac54467..832501d 100644
--- a/src/Entity/ModelType.php
+++ b/src/Entity/ModelType.php
@@ -14,6 +14,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Enum\ModelCategory;
use App\Repository\ModelTypeRepository;
use DateTimeImmutable;
@@ -30,6 +31,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiResource(
+ description: 'Types et catégories. Référentiel de classification pour les machines, pièces, composants et produits. Chaque type appartient à une catégorie (machine, piece, composant, product) et peut être converti.',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -43,6 +45,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
)]
class ModelType
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['type_machine:read', 'model_type:read', 'product:read', 'composant:read', 'piece:read'])]
@@ -128,6 +132,8 @@ class ModelType
public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = new DateTimeImmutable();
$this->composants = new ArrayCollection();
$this->pieces = new ArrayCollection();
$this->products = new ArrayCollection();
@@ -136,36 +142,6 @@ class ModelType
$this->productCustomFields = new ArrayCollection();
}
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
public function getName(): string
{
return $this->name;
@@ -315,21 +291,6 @@ class ModelType
return $this->productCustomFields;
}
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getUpdatedAt(): DateTimeImmutable
- {
- return $this->updatedAt;
- }
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
-
private function applyStructureForCategory(?array $structure, ModelCategory $category): void
{
if (ModelCategory::COMPONENT === $category) {
diff --git a/src/Entity/Piece.php b/src/Entity/Piece.php
index 74f3902..e8ca2dd 100644
--- a/src/Entity/Piece.php
+++ b/src/Entity/Piece.php
@@ -14,6 +14,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\PieceRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -30,6 +31,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typePiece' => 'exact', 'typePiece.name' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiResource(
+ description: 'Pièces détachées du catalogue. Une pièce peut être rattachée à plusieurs machines et possède un type, des fournisseurs, des documents et un produit associé.',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -44,6 +46,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
)]
class Piece
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['piece:read', 'document:list'])]
@@ -121,42 +125,14 @@ class Piece
public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = new DateTimeImmutable();
$this->constructeurs = new ArrayCollection();
$this->documents = new ArrayCollection();
$this->customFieldValues = new ArrayCollection();
$this->machineLinks = new ArrayCollection();
}
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
public function getName(): string
{
return $this->name;
@@ -322,19 +298,4 @@ class Piece
{
return $this->customFieldValues;
}
-
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getUpdatedAt(): DateTimeImmutable
- {
- return $this->updatedAt;
- }
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/Product.php b/src/Entity/Product.php
index ca0f0f8..5e0acc9 100644
--- a/src/Entity/Product.php
+++ b/src/Entity/Product.php
@@ -14,6 +14,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\ProductRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -28,6 +29,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ApiFilter(SearchFilter::class, properties: ['name' => 'ipartial', 'reference' => 'ipartial', 'typeProduct' => 'exact', 'typeProduct.name' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt', 'supplierPrice'])]
#[ApiResource(
+ description: 'Produits du catalogue fournisseur. Un produit possède une référence, un prix indicatif, un type, des fournisseurs et des documents. Il peut être lié à des machines.',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -42,6 +44,8 @@ use Symfony\Component\Serializer\Attribute\Groups;
)]
class Product
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['product:read', 'document:list'])]
@@ -118,6 +122,8 @@ class Product
public function __construct()
{
+ $this->createdAt = new DateTimeImmutable();
+ $this->updatedAt = new DateTimeImmutable();
$this->constructeurs = new ArrayCollection();
$this->documents = new ArrayCollection();
$this->customFieldValues = new ArrayCollection();
@@ -126,36 +132,6 @@ class Product
$this->machineLinks = new ArrayCollection();
}
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
- {
- $now = new DateTimeImmutable();
- $this->createdAt = $now;
- $this->updatedAt = $now;
-
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
- }
-
public function getName(): string
{
return $this->name;
@@ -243,19 +219,4 @@ class Product
{
return $this->customFieldValues;
}
-
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getUpdatedAt(): DateTimeImmutable
- {
- return $this->updatedAt;
- }
-
- private function generateCuid(): string
- {
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/Site.php b/src/Entity/Site.php
index 816d55c..689ef2e 100644
--- a/src/Entity/Site.php
+++ b/src/Entity/Site.php
@@ -11,6 +11,7 @@ use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
+use App\Entity\Trait\CuidEntityTrait;
use App\Repository\SiteRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
@@ -24,6 +25,7 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Table(name: 'sites')]
#[ORM\HasLifecycleCallbacks]
#[ApiResource(
+ description: 'Sites industriels. Chaque site regroupe des machines et peut avoir ses propres documents. Un site possède un nom, une adresse et des coordonnées de contact.',
operations: [
new Get(security: "is_granted('ROLE_VIEWER')"),
new GetCollection(security: "is_granted('ROLE_VIEWER')"),
@@ -37,6 +39,8 @@ use Symfony\Component\Validator\Constraints as Assert;
)]
class Site
{
+ use CuidEntityTrait;
+
#[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['document:list'])]
@@ -81,41 +85,11 @@ class Site
private Collection $documents;
public function __construct()
- {
- $this->machines = new ArrayCollection();
- $this->documents = new ArrayCollection();
- }
-
- #[ORM\PrePersist]
- public function setCreatedAtValue(): void
{
$this->createdAt = new DateTimeImmutable();
$this->updatedAt = new DateTimeImmutable();
-
- // Générer un ID CUID-compatible si nécessaire
- if (null === $this->id) {
- $this->id = $this->generateCuid();
- }
- }
-
- #[ORM\PreUpdate]
- public function setUpdatedAtValue(): void
- {
- $this->updatedAt = new DateTimeImmutable();
- }
-
- // Getters et Setters
-
- public function getId(): ?string
- {
- return $this->id;
- }
-
- public function setId(string $id): static
- {
- $this->id = $id;
-
- return $this;
+ $this->machines = new ArrayCollection();
+ $this->documents = new ArrayCollection();
}
public function getName(): string
@@ -190,16 +164,6 @@ class Site
return $this;
}
- public function getCreatedAt(): DateTimeImmutable
- {
- return $this->createdAt;
- }
-
- public function getUpdatedAt(): DateTimeImmutable
- {
- return $this->updatedAt;
- }
-
/**
* @return Collection
*/
@@ -259,10 +223,4 @@ class Site
return $this;
}
-
- private function generateCuid(): string
- {
- // Génération d'un ID compatible CUID (format: cl + 24 caractères)
- return 'cl'.bin2hex(random_bytes(12));
- }
}
diff --git a/src/Entity/Trait/CuidEntityTrait.php b/src/Entity/Trait/CuidEntityTrait.php
new file mode 100644
index 0000000..7d92878
--- /dev/null
+++ b/src/Entity/Trait/CuidEntityTrait.php
@@ -0,0 +1,56 @@
+createdAt = $now;
+ $this->updatedAt = $now;
+
+ if (null === $this->id) {
+ $this->id = $this->generateCuid();
+ }
+ }
+
+ #[ORM\PreUpdate]
+ public function setUpdatedAtValue(): void
+ {
+ $this->updatedAt = new DateTimeImmutable();
+ }
+
+ public function getId(): ?string
+ {
+ return $this->id;
+ }
+
+ public function setId(string $id): static
+ {
+ $this->id = $id;
+
+ return $this;
+ }
+
+ public function getCreatedAt(): DateTimeImmutable
+ {
+ return $this->createdAt;
+ }
+
+ public function getUpdatedAt(): DateTimeImmutable
+ {
+ return $this->updatedAt;
+ }
+
+ private function generateCuid(): string
+ {
+ return 'cl'.bin2hex(random_bytes(12));
+ }
+}
diff --git a/src/EventSubscriber/AbstractAuditSubscriber.php b/src/EventSubscriber/AbstractAuditSubscriber.php
new file mode 100644
index 0000000..d73ea23
--- /dev/null
+++ b/src/EventSubscriber/AbstractAuditSubscriber.php
@@ -0,0 +1,463 @@
+getObjectManager();
+ if (!$em instanceof EntityManagerInterface) {
+ return;
+ }
+
+ $uow = $em->getUnitOfWork();
+ $actorProfileId = $this->resolveActorProfileId();
+ $entityType = $this->entityType();
+
+ if ($this->hasCollectionTracking()) {
+ $this->onFlushComplex($em, $uow, $actorProfileId, $entityType);
+ } else {
+ $this->onFlushSimple($em, $uow, $actorProfileId, $entityType);
+ }
+ }
+
+ abstract protected function supports(object $entity): bool;
+
+ abstract protected function entityType(): string;
+
+ abstract protected function snapshotEntity(object $entity): array;
+
+ /**
+ * Override in subclasses that track custom field value changes.
+ * Return the owner entity if the CFV belongs to the tracked entity type.
+ */
+ protected function getOwnerFromCustomFieldValue(CustomFieldValue $cfv): ?object
+ {
+ return null;
+ }
+
+ /**
+ * Whether this subscriber tracks constructeur collection changes.
+ * Override to return true for entities with a constructeurs ManyToMany.
+ */
+ protected function hasCollectionTracking(): bool
+ {
+ return false;
+ }
+
+ protected function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
+ {
+ $uow = $em->getUnitOfWork();
+ $log->initializeAuditLog();
+ $em->persist($log);
+
+ $meta = $em->getClassMetadata(AuditLog::class);
+ $uow->computeChangeSet($meta, $log);
+ }
+
+ /**
+ * @param array $changeSet
+ *
+ * @return array
+ */
+ protected 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;
+ }
+
+ /**
+ * @param iterable $items
+ *
+ * @return list
+ */
+ protected 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;
+ }
+
+ protected function safeGet(object $entity, string $method): mixed
+ {
+ try {
+ return $entity->{$method}();
+ } catch (Error) {
+ return null;
+ }
+ }
+
+ protected 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 BackedEnum) {
+ return $value->value;
+ }
+
+ 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 Site || $value instanceof Machine || $value instanceof Composant || $value instanceof Piece) {
+ return [
+ 'id' => $value->getId(),
+ 'name' => $value->getName(),
+ ];
+ }
+
+ if ($value instanceof Collection) {
+ return $this->normalizeCollection($value);
+ }
+
+ if (is_object($value) && method_exists($value, 'getId')) {
+ return (string) $value->getId();
+ }
+
+ if (is_array($value)) {
+ return $value;
+ }
+
+ return (string) $value;
+ }
+
+ /**
+ * @param array $base
+ * @param array $extra
+ *
+ * @return array
+ */
+ protected function mergeDiffs(array $base, array $extra): array
+ {
+ foreach ($extra as $field => $change) {
+ $base[$field] = $change;
+ }
+
+ return $base;
+ }
+
+ protected 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;
+ }
+
+ private function onFlushSimple(EntityManagerInterface $em, UnitOfWork $uow, ?string $actorProfileId, string $entityType): void
+ {
+ foreach ($uow->getScheduledEntityInsertions() as $entity) {
+ if (!$this->supports($entity)) {
+ continue;
+ }
+
+ $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
+ $snapshot = $this->snapshotEntity($entity);
+ $this->persistAuditLog($em, new AuditLog($entityType, (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
+ }
+
+ foreach ($uow->getScheduledEntityUpdates() as $entity) {
+ if (!$this->supports($entity)) {
+ continue;
+ }
+
+ $id = (string) $entity->getId();
+ if ('' === $id) {
+ continue;
+ }
+
+ $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
+ if ([] !== $diff) {
+ $snapshot = $this->snapshotEntity($entity);
+ $this->persistAuditLog($em, new AuditLog($entityType, $id, 'update', $diff, $snapshot, $actorProfileId));
+ }
+ }
+
+ foreach ($uow->getScheduledEntityDeletions() as $entity) {
+ if (!$this->supports($entity)) {
+ continue;
+ }
+
+ $snapshot = $this->snapshotEntity($entity);
+ $this->persistAuditLog($em, new AuditLog($entityType, (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
+ }
+ }
+
+ private function onFlushComplex(EntityManagerInterface $em, UnitOfWork $uow, ?string $actorProfileId, string $entityType): void
+ {
+ $pendingUpdates = [];
+ $pendingSnapshots = [];
+ $pendingEntities = [];
+
+ foreach ($uow->getScheduledEntityInsertions() as $entity) {
+ if (!$this->supports($entity)) {
+ continue;
+ }
+
+ $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
+ $snapshot = $this->snapshotEntity($entity);
+ $this->persistAuditLog($em, new AuditLog($entityType, (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
+ }
+
+ foreach ($uow->getScheduledEntityUpdates() as $entity) {
+ if (!$this->supports($entity)) {
+ continue;
+ }
+
+ $entityId = (string) $entity->getId();
+ if ('' === $entityId) {
+ continue;
+ }
+
+ $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
+ if ([] !== $diff) {
+ $pendingUpdates[$entityId] = $this->mergeDiffs($pendingUpdates[$entityId] ?? [], $diff);
+ $pendingSnapshots[$entityId] = $this->snapshotEntity($entity);
+ $pendingEntities[$entityId] = $entity;
+ }
+ }
+
+ foreach ($uow->getScheduledEntityDeletions() as $entity) {
+ if (!$this->supports($entity)) {
+ continue;
+ }
+
+ $snapshot = $this->snapshotEntity($entity);
+ $this->persistAuditLog($em, new AuditLog($entityType, (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
+ }
+
+ foreach ($uow->getScheduledCollectionUpdates() as $collection) {
+ $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingEntities);
+ }
+ foreach ($uow->getScheduledCollectionDeletions() as $collection) {
+ $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingEntities);
+ }
+
+ $this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingEntities);
+
+ foreach ($pendingUpdates as $entityId => $diff) {
+ if ([] === $diff) {
+ continue;
+ }
+
+ $entity = $pendingEntities[$entityId] ?? null;
+ if (null === $entity || !$this->supports($entity)) {
+ continue;
+ }
+
+ $snapshot = $pendingSnapshots[$entityId] ?? $this->snapshotEntity($entity);
+ $this->persistAuditLog($em, new AuditLog($entityType, $entityId, 'update', $diff, $snapshot, $actorProfileId));
+ }
+ }
+
+ private function collectCollectionUpdate(
+ object $collection,
+ array &$pendingUpdates,
+ array &$pendingSnapshots,
+ array &$pendingEntities,
+ ): void {
+ if (!$collection instanceof PersistentCollection) {
+ return;
+ }
+
+ $owner = $collection->getOwner();
+ if (null === $owner || !$this->supports($owner)) {
+ return;
+ }
+
+ $ownerId = (string) $owner->getId();
+ if ('' === $ownerId) {
+ 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[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
+ $pendingSnapshots[$ownerId] = $this->snapshotEntity($owner);
+ $pendingEntities[$ownerId] = $owner;
+ }
+
+ private function collectCustomFieldValueChanges(
+ UnitOfWork $uow,
+ array &$pendingUpdates,
+ array &$pendingSnapshots,
+ array &$pendingEntities,
+ ): void {
+ foreach ($uow->getScheduledEntityInsertions() as $entity) {
+ if ($entity instanceof CustomFieldValue) {
+ $this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingEntities);
+ }
+ }
+
+ 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, $pendingEntities);
+ }
+ }
+
+ foreach ($uow->getScheduledEntityDeletions() as $entity) {
+ if ($entity instanceof CustomFieldValue) {
+ $this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingEntities);
+ }
+ }
+ }
+
+ private function trackCustomFieldValueChange(
+ CustomFieldValue $cfv,
+ mixed $from,
+ mixed $to,
+ array &$pendingUpdates,
+ array &$pendingSnapshots,
+ array &$pendingEntities,
+ ): void {
+ $owner = $this->getOwnerFromCustomFieldValue($cfv);
+ if (null === $owner) {
+ 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->snapshotEntity($owner);
+ $pendingEntities[$ownerId] = $owner;
+ }
+}
diff --git a/src/EventSubscriber/ComposantAuditSubscriber.php b/src/EventSubscriber/ComposantAuditSubscriber.php
index e7a90d7..1279a03 100644
--- a/src/EventSubscriber/ComposantAuditSubscriber.php
+++ b/src/EventSubscriber/ComposantAuditSubscriber.php
@@ -4,394 +4,45 @@ declare(strict_types=1);
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;
-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
+final class ComposantAuditSubscriber extends AbstractAuditSubscriber
{
- public function __construct(
- private readonly RequestStack $requestStack,
- private readonly Security $security,
- ) {}
+ protected function supports(object $entity): bool
+ {
+ return $entity instanceof Composant;
+ }
- public function getSubscribedEvents(): array
+ protected function entityType(): string
+ {
+ return 'composant';
+ }
+
+ protected function hasCollectionTracking(): bool
+ {
+ return true;
+ }
+
+ protected function getOwnerFromCustomFieldValue(CustomFieldValue $cfv): ?object
+ {
+ return $cfv->getComposant();
+ }
+
+ protected function snapshotEntity(object $entity): array
{
return [
- Events::onFlush,
+ 'id' => $entity->getId(),
+ 'name' => $this->safeGet($entity, 'getName'),
+ 'reference' => $this->safeGet($entity, 'getReference'),
+ 'prix' => $this->safeGet($entity, 'getPrix'),
+ 'structure' => $this->safeGet($entity, 'getStructure'),
+ 'typeComposant' => $this->normalizeValue($this->safeGet($entity, 'getTypeComposant')),
+ 'product' => $this->normalizeValue($this->safeGet($entity, 'getProduct')),
+ 'constructeurIds' => $this->normalizeCollection($entity->getConstructeurs()),
];
}
-
- public function onFlush(OnFlushEventArgs $args): void
- {
- $em = $args->getObjectManager();
- if (!$em instanceof EntityManagerInterface) {
- return;
- }
-
- $uow = $em->getUnitOfWork();
- $actorProfileId = $this->resolveActorProfileId();
- $pendingUpdates = [];
- $pendingSnapshots = [];
- $pendingComponents = [];
-
- foreach ($uow->getScheduledEntityInsertions() as $entity) {
- if (!$entity instanceof Composant) {
- continue;
- }
-
- $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
- $snapshot = $this->snapshotComposant($entity);
- $this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
- }
-
- foreach ($uow->getScheduledEntityUpdates() as $entity) {
- if (!$entity instanceof Composant) {
- continue;
- }
-
- $componentId = (string) $entity->getId();
- if ('' === $componentId) {
- continue;
- }
-
- $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
- if ([] !== $diff) {
- $pendingUpdates[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
- $pendingSnapshots[$componentId] = $this->snapshotComposant($entity);
- $pendingComponents[$componentId] = $entity;
- }
- }
-
- foreach ($uow->getScheduledEntityDeletions() as $entity) {
- if (!$entity instanceof Composant) {
- continue;
- }
-
- $snapshot = $this->snapshotComposant($entity);
- $this->persistAuditLog($em, new AuditLog('composant', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
- }
-
- foreach ($uow->getScheduledCollectionUpdates() as $collection) {
- $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents);
- }
- foreach ($uow->getScheduledCollectionDeletions() as $collection) {
- $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingComponents);
- }
-
- $this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingComponents);
-
- foreach ($pendingUpdates as $componentId => $diff) {
- if ([] === $diff) {
- continue;
- }
-
- $component = $pendingComponents[$componentId] ?? null;
- if (!$component instanceof Composant) {
- continue;
- }
-
- $snapshot = $pendingSnapshots[$componentId] ?? $this->snapshotComposant($component);
- $this->persistAuditLog($em, new AuditLog('composant', $componentId, 'update', $diff, $snapshot, $actorProfileId));
- }
- }
-
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingComponents
- */
- private function collectCollectionUpdate(
- object $collection,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingComponents,
- ): void {
- if (!$collection instanceof PersistentCollection) {
- return;
- }
-
- $owner = $collection->getOwner();
- if (!$owner instanceof Composant) {
- return;
- }
-
- $componentId = (string) $owner->getId();
- if ('' === $componentId) {
- 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[$componentId] = $this->mergeDiffs($pendingUpdates[$componentId] ?? [], $diff);
- $pendingSnapshots[$componentId] = $this->snapshotComposant($owner);
- $pendingComponents[$componentId] = $owner;
- }
-
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingComponents
- */
- private function collectCustomFieldValueChanges(
- UnitOfWork $uow,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingComponents,
- ): void {
- foreach ($uow->getScheduledEntityInsertions() as $entity) {
- if ($entity instanceof CustomFieldValue) {
- $this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingComponents);
- }
- }
-
- foreach ($uow->getScheduledEntityUpdates() as $entity) {
- if (!$entity instanceof CustomFieldValue) {
- continue;
- }
- $changeSet = $uow->getEntityChangeSet($entity);
- if (!isset($changeSet['value'])) {
- continue;
- }
- [$oldVal, $newVal] = $changeSet['value'];
- if ($oldVal !== $newVal) {
- $this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingComponents);
- }
- }
-
- foreach ($uow->getScheduledEntityDeletions() as $entity) {
- if ($entity instanceof CustomFieldValue) {
- $this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingComponents);
- }
- }
- }
-
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingComponents
- */
- private function trackCustomFieldValueChange(
- CustomFieldValue $cfv,
- mixed $from,
- mixed $to,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingComponents,
- ): void {
- $owner = $cfv->getComposant();
- if (!$owner instanceof Composant) {
- return;
- }
-
- $ownerId = (string) $owner->getId();
- if ('' === $ownerId) {
- return;
- }
-
- $fieldName = 'customField:'.$cfv->getCustomField()->getName();
- $diff = [$fieldName => ['from' => $from, 'to' => $to]];
-
- $pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
- $pendingSnapshots[$ownerId] = $this->snapshotComposant($owner);
- $pendingComponents[$ownerId] = $owner;
- }
-
- private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
- {
- $uow = $em->getUnitOfWork();
- $log->initializeAuditLog();
- $em->persist($log);
-
- $meta = $em->getClassMetadata(AuditLog::class);
- $uow->computeChangeSet($meta, $log);
- }
-
- /**
- * @param array $changeSet
- *
- * @return array
- */
- private function buildDiffFromChangeSet(array $changeSet): array
- {
- $diff = [];
- foreach ($changeSet as $field => [$oldValue, $newValue]) {
- if ('updatedAt' === $field || 'createdAt' === $field) {
- continue;
- }
-
- $normalizedOld = $this->normalizeValue($oldValue);
- $normalizedNew = $this->normalizeValue($newValue);
-
- if ($normalizedOld === $normalizedNew) {
- continue;
- }
-
- $diff[$field] = [
- 'from' => $normalizedOld,
- 'to' => $normalizedNew,
- ];
- }
-
- return $diff;
- }
-
- private function 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()),
- 'constructeurIds' => $this->normalizeCollection($component->getConstructeurs()),
- ];
- }
-
- /**
- * @param iterable $items
- *
- * @return list
- */
- private function normalizeCollection(iterable $items): array
- {
- $entries = [];
- $seen = [];
- foreach ($items as $item) {
- if (is_object($item) && method_exists($item, 'getId')) {
- $id = $item->getId();
- if (null === $id || '' === $id || isset($seen[(string) $id])) {
- continue;
- }
- $seen[(string) $id] = true;
- if (method_exists($item, 'getName')) {
- $entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
- } else {
- $entries[] = (string) $id;
- }
- }
- }
-
- return $entries;
- }
-
- private function normalizeValue(mixed $value): mixed
- {
- if (null === $value || is_scalar($value)) {
- return $value;
- }
-
- if ($value instanceof DateTimeInterface) {
- return $value->format(DateTimeInterface::ATOM);
- }
-
- if ($value instanceof ModelType) {
- return [
- 'id' => $value->getId(),
- 'name' => $value->getName(),
- 'code' => $value->getCode(),
- ];
- }
-
- if ($value instanceof Product) {
- return [
- 'id' => $value->getId(),
- 'name' => $value->getName(),
- 'reference' => $value->getReference(),
- ];
- }
-
- if ($value instanceof Collection) {
- return $this->normalizeCollection($value);
- }
-
- if (is_object($value) && method_exists($value, 'getId')) {
- return (string) $value->getId();
- }
-
- if (is_array($value)) {
- return $value;
- }
-
- return (string) $value;
- }
-
- /**
- * @param array $base
- * @param array $extra
- *
- * @return array
- */
- private function mergeDiffs(array $base, array $extra): array
- {
- foreach ($extra as $field => $change) {
- $base[$field] = $change;
- }
-
- return $base;
- }
-
- private function resolveActorProfileId(): ?string
- {
- try {
- $session = $this->requestStack->getSession();
- if ($session instanceof SessionInterface) {
- $profileId = $session->get('profileId');
- if ($profileId) {
- return (string) $profileId;
- }
- }
- } catch (Throwable) {
- // No session available (CLI context, etc.)
- }
-
- $user = $this->security->getUser();
- if ($user instanceof Profile) {
- return $user->getId();
- }
-
- return null;
- }
}
diff --git a/src/EventSubscriber/ConstructeurAuditSubscriber.php b/src/EventSubscriber/ConstructeurAuditSubscriber.php
index 428dead..b506085 100644
--- a/src/EventSubscriber/ConstructeurAuditSubscriber.php
+++ b/src/EventSubscriber/ConstructeurAuditSubscriber.php
@@ -4,165 +4,30 @@ 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
+final class ConstructeurAuditSubscriber extends AbstractAuditSubscriber
{
- public function __construct(
- private readonly RequestStack $requestStack,
- private readonly Security $security,
- ) {}
+ protected function supports(object $entity): bool
+ {
+ return $entity instanceof Constructeur;
+ }
- public function getSubscribedEvents(): array
+ protected function entityType(): string
+ {
+ return 'constructeur';
+ }
+
+ protected function snapshotEntity(object $entity): array
{
return [
- Events::onFlush,
+ 'id' => $entity->getId(),
+ 'name' => $this->safeGet($entity, 'getName'),
+ 'email' => $this->safeGet($entity, 'getEmail'),
+ 'phone' => $this->safeGet($entity, 'getPhone'),
];
}
-
- 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 $changeSet
- *
- * @return array
- */
- private function buildDiffFromChangeSet(array $changeSet): array
- {
- $diff = [];
- foreach ($changeSet as $field => [$oldValue, $newValue]) {
- if ('updatedAt' === $field || 'createdAt' === $field) {
- continue;
- }
-
- $normalizedOld = $this->normalizeValue($oldValue);
- $normalizedNew = $this->normalizeValue($newValue);
-
- if ($normalizedOld === $normalizedNew) {
- continue;
- }
-
- $diff[$field] = [
- 'from' => $normalizedOld,
- 'to' => $normalizedNew,
- ];
- }
-
- return $diff;
- }
-
- private function snapshotConstructeur(Constructeur $constructeur): array
- {
- return [
- 'id' => $constructeur->getId(),
- 'name' => $constructeur->getName(),
- 'email' => $constructeur->getEmail(),
- 'phone' => $constructeur->getPhone(),
- ];
- }
-
- private function normalizeValue(mixed $value): mixed
- {
- if (null === $value || is_scalar($value)) {
- return $value;
- }
-
- if ($value instanceof DateTimeInterface) {
- return $value->format(DateTimeInterface::ATOM);
- }
-
- return (string) $value;
- }
-
- private function resolveActorProfileId(): ?string
- {
- try {
- $session = $this->requestStack->getSession();
- if ($session instanceof SessionInterface) {
- $profileId = $session->get('profileId');
- if ($profileId) {
- return (string) $profileId;
- }
- }
- } catch (Throwable) {
- // No session available (CLI context, etc.)
- }
-
- $user = $this->security->getUser();
- if ($user instanceof Profile) {
- return $user->getId();
- }
-
- return null;
- }
}
diff --git a/src/EventSubscriber/DocumentAuditSubscriber.php b/src/EventSubscriber/DocumentAuditSubscriber.php
index f40cdeb..634a304 100644
--- a/src/EventSubscriber/DocumentAuditSubscriber.php
+++ b/src/EventSubscriber/DocumentAuditSubscriber.php
@@ -4,189 +4,36 @@ 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
+final class DocumentAuditSubscriber extends AbstractAuditSubscriber
{
- public function __construct(
- private readonly RequestStack $requestStack,
- private readonly Security $security,
- ) {}
+ protected function supports(object $entity): bool
+ {
+ return $entity instanceof Document;
+ }
- public function getSubscribedEvents(): array
+ protected function entityType(): string
+ {
+ return 'document';
+ }
+
+ protected function snapshotEntity(object $entity): array
{
return [
- Events::onFlush,
+ 'id' => $entity->getId(),
+ 'name' => $this->safeGet($entity, 'getName'),
+ 'filename' => $this->safeGet($entity, 'getFilename'),
+ 'mimeType' => $this->safeGet($entity, 'getMimeType'),
+ 'size' => $this->safeGet($entity, 'getSize'),
+ 'machine' => $this->normalizeValue($this->safeGet($entity, 'getMachine')),
+ 'composant' => $this->normalizeValue($this->safeGet($entity, 'getComposant')),
+ 'piece' => $this->normalizeValue($this->safeGet($entity, 'getPiece')),
+ 'product' => $this->normalizeValue($this->safeGet($entity, 'getProduct')),
+ 'site' => $this->normalizeValue($this->safeGet($entity, 'getSite')),
];
}
-
- 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 $changeSet
- *
- * @return array
- */
- private function buildDiffFromChangeSet(array $changeSet): array
- {
- $diff = [];
- foreach ($changeSet as $field => [$oldValue, $newValue]) {
- if ('updatedAt' === $field || 'createdAt' === $field) {
- continue;
- }
-
- $normalizedOld = $this->normalizeValue($oldValue);
- $normalizedNew = $this->normalizeValue($newValue);
-
- if ($normalizedOld === $normalizedNew) {
- continue;
- }
-
- $diff[$field] = [
- 'from' => $normalizedOld,
- 'to' => $normalizedNew,
- ];
- }
-
- return $diff;
- }
-
- private function snapshotDocument(Document $document): array
- {
- return [
- 'id' => $document->getId(),
- 'name' => $document->getName(),
- 'filename' => $document->getFilename(),
- 'mimeType' => $document->getMimeType(),
- 'size' => $document->getSize(),
- 'machine' => $this->normalizeValue($document->getMachine()),
- 'composant' => $this->normalizeValue($document->getComposant()),
- 'piece' => $this->normalizeValue($document->getPiece()),
- 'product' => $this->normalizeValue($document->getProduct()),
- 'site' => $this->normalizeValue($document->getSite()),
- ];
- }
-
- private function normalizeValue(mixed $value): mixed
- {
- if (null === $value || is_scalar($value)) {
- return $value;
- }
-
- if ($value instanceof DateTimeInterface) {
- return $value->format(DateTimeInterface::ATOM);
- }
-
- if ($value instanceof Machine || $value instanceof Composant || $value instanceof Piece || $value instanceof Product || $value instanceof Site) {
- return [
- 'id' => $value->getId(),
- 'name' => $value->getName(),
- ];
- }
-
- if (is_object($value) && method_exists($value, 'getId')) {
- return (string) $value->getId();
- }
-
- return (string) $value;
- }
-
- private function resolveActorProfileId(): ?string
- {
- try {
- $session = $this->requestStack->getSession();
- if ($session instanceof SessionInterface) {
- $profileId = $session->get('profileId');
- if ($profileId) {
- return (string) $profileId;
- }
- }
- } catch (Throwable) {
- // No session available (CLI context, etc.)
- }
-
- $user = $this->security->getUser();
- if ($user instanceof Profile) {
- return $user->getId();
- }
-
- return null;
- }
}
diff --git a/src/EventSubscriber/MachineAuditSubscriber.php b/src/EventSubscriber/MachineAuditSubscriber.php
index 42dbe9c..0cbeb90 100644
--- a/src/EventSubscriber/MachineAuditSubscriber.php
+++ b/src/EventSubscriber/MachineAuditSubscriber.php
@@ -4,400 +4,45 @@ 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 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
+final class MachineAuditSubscriber extends AbstractAuditSubscriber
{
- public function __construct(
- private readonly RequestStack $requestStack,
- private readonly Security $security,
- ) {}
-
- public function getSubscribedEvents(): array
+ protected function supports(object $entity): bool
{
- return [
- Events::onFlush,
- ];
+ return $entity instanceof Machine;
}
- public function onFlush(OnFlushEventArgs $args): void
+ protected function entityType(): string
{
- $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));
- }
+ return 'machine';
}
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingMachines
- */
- private function collectCollectionUpdate(
- object $collection,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingMachines,
- ): void {
- if (!$collection instanceof PersistentCollection) {
- return;
- }
-
- $owner = $collection->getOwner();
- if (!$owner instanceof Machine) {
- return;
- }
-
- $machineId = (string) $owner->getId();
- if ('' === $machineId) {
- return;
- }
-
- $mapping = $collection->getMapping();
- $fieldName = $mapping['fieldName'] ?? null;
- if ('constructeurs' !== $fieldName) {
- return;
- }
-
- $before = $this->normalizeCollection($collection->getSnapshot());
- $after = $this->normalizeCollection($collection->toArray());
-
- if ($before === $after) {
- return;
- }
-
- $diff = [
- 'constructeurIds' => [
- 'from' => $before,
- 'to' => $after,
- ],
- ];
-
- $pendingUpdates[$machineId] = $this->mergeDiffs($pendingUpdates[$machineId] ?? [], $diff);
- $pendingSnapshots[$machineId] = $this->snapshotMachine($owner);
- $pendingMachines[$machineId] = $owner;
+ protected function hasCollectionTracking(): bool
+ {
+ return true;
}
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingMachines
- */
- private function collectCustomFieldValueChanges(
- UnitOfWork $uow,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingMachines,
- ): void {
- foreach ($uow->getScheduledEntityInsertions() as $entity) {
- if ($entity instanceof CustomFieldValue) {
- $this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingMachines);
- }
- }
-
- foreach ($uow->getScheduledEntityUpdates() as $entity) {
- if (!$entity instanceof CustomFieldValue) {
- continue;
- }
- $changeSet = $uow->getEntityChangeSet($entity);
- if (!isset($changeSet['value'])) {
- continue;
- }
- [$oldVal, $newVal] = $changeSet['value'];
- if ($oldVal !== $newVal) {
- $this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingMachines);
- }
- }
-
- foreach ($uow->getScheduledEntityDeletions() as $entity) {
- if ($entity instanceof CustomFieldValue) {
- $this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingMachines);
- }
- }
- }
-
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingMachines
- */
- private function trackCustomFieldValueChange(
- CustomFieldValue $cfv,
- mixed $from,
- mixed $to,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingMachines,
- ): void {
+ protected function getOwnerFromCustomFieldValue(CustomFieldValue $cfv): ?object
+ {
$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;
+ return $owner instanceof Machine ? $owner : null;
}
- private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
- {
- $uow = $em->getUnitOfWork();
- $log->initializeAuditLog();
- $em->persist($log);
-
- $meta = $em->getClassMetadata(AuditLog::class);
- $uow->computeChangeSet($meta, $log);
- }
-
- /**
- * @param array $changeSet
- *
- * @return array
- */
- private function buildDiffFromChangeSet(array $changeSet): array
- {
- $diff = [];
- foreach ($changeSet as $field => [$oldValue, $newValue]) {
- if ('updatedAt' === $field || 'createdAt' === $field) {
- continue;
- }
-
- $normalizedOld = $this->normalizeValue($oldValue);
- $normalizedNew = $this->normalizeValue($newValue);
-
- if ($normalizedOld === $normalizedNew) {
- continue;
- }
-
- $diff[$field] = [
- 'from' => $normalizedOld,
- 'to' => $normalizedNew,
- ];
- }
-
- return $diff;
- }
-
- private function snapshotMachine(Machine $machine): array
+ protected function snapshotEntity(object $entity): array
{
return [
- 'id' => $machine->getId(),
- 'name' => $machine->getName(),
- 'reference' => $machine->getReference(),
- 'prix' => $machine->getPrix(),
- 'site' => $this->normalizeValue($machine->getSite()),
- 'constructeurIds' => $this->normalizeCollection($machine->getConstructeurs()),
+ 'id' => $entity->getId(),
+ 'name' => $this->safeGet($entity, 'getName'),
+ 'reference' => $this->safeGet($entity, 'getReference'),
+ 'prix' => $this->safeGet($entity, 'getPrix'),
+ 'site' => $this->normalizeValue($this->safeGet($entity, 'getSite')),
+ 'constructeurIds' => $this->normalizeCollection($entity->getConstructeurs()),
];
}
-
- /**
- * @param iterable $items
- *
- * @return list
- */
- private function normalizeCollection(iterable $items): array
- {
- $entries = [];
- $seen = [];
- foreach ($items as $item) {
- if (is_object($item) && method_exists($item, 'getId')) {
- $id = $item->getId();
- if (null === $id || '' === $id || isset($seen[(string) $id])) {
- continue;
- }
- $seen[(string) $id] = true;
- if (method_exists($item, 'getName')) {
- $entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
- } else {
- $entries[] = (string) $id;
- }
- }
- }
-
- return $entries;
- }
-
- private function normalizeValue(mixed $value): mixed
- {
- if (null === $value || is_scalar($value)) {
- return $value;
- }
-
- if ($value instanceof DateTimeInterface) {
- return $value->format(DateTimeInterface::ATOM);
- }
-
- if ($value instanceof Site) {
- return [
- 'id' => $value->getId(),
- 'name' => $value->getName(),
- ];
- }
-
- if ($value instanceof ModelType) {
- return [
- 'id' => $value->getId(),
- 'name' => $value->getName(),
- 'code' => $value->getCode(),
- ];
- }
-
- if ($value instanceof Product) {
- return [
- 'id' => $value->getId(),
- 'name' => $value->getName(),
- 'reference' => $value->getReference(),
- ];
- }
-
- if ($value instanceof Collection) {
- return $this->normalizeCollection($value);
- }
-
- if (is_object($value) && method_exists($value, 'getId')) {
- return (string) $value->getId();
- }
-
- if (is_array($value)) {
- return $value;
- }
-
- return (string) $value;
- }
-
- /**
- * @param array $base
- * @param array $extra
- *
- * @return array
- */
- private function mergeDiffs(array $base, array $extra): array
- {
- foreach ($extra as $field => $change) {
- $base[$field] = $change;
- }
-
- return $base;
- }
-
- private function resolveActorProfileId(): ?string
- {
- try {
- $session = $this->requestStack->getSession();
- if ($session instanceof SessionInterface) {
- $profileId = $session->get('profileId');
- if ($profileId) {
- return (string) $profileId;
- }
- }
- } catch (Throwable) {
- // No session available (CLI context, etc.)
- }
-
- $user = $this->security->getUser();
- if ($user instanceof Profile) {
- return $user->getId();
- }
-
- return null;
- }
}
diff --git a/src/EventSubscriber/ModelTypeAuditSubscriber.php b/src/EventSubscriber/ModelTypeAuditSubscriber.php
index aa1528e..e2fecb2 100644
--- a/src/EventSubscriber/ModelTypeAuditSubscriber.php
+++ b/src/EventSubscriber/ModelTypeAuditSubscriber.php
@@ -4,177 +4,32 @@ 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
+final class ModelTypeAuditSubscriber extends AbstractAuditSubscriber
{
- public function __construct(
- private readonly RequestStack $requestStack,
- private readonly Security $security,
- ) {}
+ protected function supports(object $entity): bool
+ {
+ return $entity instanceof ModelType;
+ }
- public function getSubscribedEvents(): array
+ protected function entityType(): string
+ {
+ return 'model_type';
+ }
+
+ protected function snapshotEntity(object $entity): array
{
return [
- Events::onFlush,
+ 'id' => $entity->getId(),
+ 'name' => $this->safeGet($entity, 'getName'),
+ 'code' => $this->safeGet($entity, 'getCode'),
+ 'category' => $this->safeGet($entity, 'getCategory')?->value,
+ 'notes' => $this->safeGet($entity, 'getNotes'),
+ 'description' => $this->safeGet($entity, 'getDescription'),
];
}
-
- 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 $changeSet
- *
- * @return array
- */
- private function buildDiffFromChangeSet(array $changeSet): array
- {
- $diff = [];
- foreach ($changeSet as $field => [$oldValue, $newValue]) {
- if ('updatedAt' === $field || 'createdAt' === $field) {
- continue;
- }
-
- $normalizedOld = $this->normalizeValue($oldValue);
- $normalizedNew = $this->normalizeValue($newValue);
-
- if ($normalizedOld === $normalizedNew) {
- continue;
- }
-
- $diff[$field] = [
- 'from' => $normalizedOld,
- 'to' => $normalizedNew,
- ];
- }
-
- return $diff;
- }
-
- private function snapshotModelType(ModelType $modelType): array
- {
- return [
- 'id' => $modelType->getId(),
- 'name' => $modelType->getName(),
- 'code' => $modelType->getCode(),
- 'category' => $modelType->getCategory()->value,
- 'notes' => $modelType->getNotes(),
- 'description' => $modelType->getDescription(),
- ];
- }
-
- private function normalizeValue(mixed $value): mixed
- {
- if (null === $value || is_scalar($value)) {
- return $value;
- }
-
- if ($value instanceof DateTimeInterface) {
- return $value->format(DateTimeInterface::ATOM);
- }
-
- if ($value instanceof ModelCategory) {
- return $value->value;
- }
-
- if (is_array($value)) {
- return $value;
- }
-
- return (string) $value;
- }
-
- private function resolveActorProfileId(): ?string
- {
- try {
- $session = $this->requestStack->getSession();
- if ($session instanceof SessionInterface) {
- $profileId = $session->get('profileId');
- if ($profileId) {
- return (string) $profileId;
- }
- }
- } catch (Throwable) {
- // No session available (CLI context, etc.)
- }
-
- $user = $this->security->getUser();
- if ($user instanceof Profile) {
- return $user->getId();
- }
-
- return null;
- }
}
diff --git a/src/EventSubscriber/PieceAuditSubscriber.php b/src/EventSubscriber/PieceAuditSubscriber.php
index c802da3..d60c9a1 100644
--- a/src/EventSubscriber/PieceAuditSubscriber.php
+++ b/src/EventSubscriber/PieceAuditSubscriber.php
@@ -4,394 +4,45 @@ 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;
-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
+final class PieceAuditSubscriber extends AbstractAuditSubscriber
{
- public function __construct(
- private readonly RequestStack $requestStack,
- private readonly Security $security,
- ) {}
+ protected function supports(object $entity): bool
+ {
+ return $entity instanceof Piece;
+ }
- public function getSubscribedEvents(): array
+ protected function entityType(): string
+ {
+ return 'piece';
+ }
+
+ protected function hasCollectionTracking(): bool
+ {
+ return true;
+ }
+
+ protected function getOwnerFromCustomFieldValue(CustomFieldValue $cfv): ?object
+ {
+ return $cfv->getPiece();
+ }
+
+ protected function snapshotEntity(object $entity): array
{
return [
- Events::onFlush,
+ 'id' => $entity->getId(),
+ 'name' => $this->safeGet($entity, 'getName'),
+ 'reference' => $this->safeGet($entity, 'getReference'),
+ 'prix' => $this->safeGet($entity, 'getPrix'),
+ 'typePiece' => $this->normalizeValue($this->safeGet($entity, 'getTypePiece')),
+ 'product' => $this->normalizeValue($this->safeGet($entity, 'getProduct')),
+ 'productIds' => $this->safeGet($entity, 'getProductIds') ?? [],
+ 'constructeurIds' => $this->normalizeCollection($entity->getConstructeurs()),
];
}
-
- public function onFlush(OnFlushEventArgs $args): void
- {
- $em = $args->getObjectManager();
- if (!$em instanceof EntityManagerInterface) {
- return;
- }
-
- $uow = $em->getUnitOfWork();
- $actorProfileId = $this->resolveActorProfileId();
- $pendingUpdates = [];
- $pendingSnapshots = [];
- $pendingPieces = [];
-
- foreach ($uow->getScheduledEntityInsertions() as $entity) {
- if (!$entity instanceof Piece) {
- continue;
- }
-
- $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
- $snapshot = $this->snapshotPiece($entity);
- $this->persistAuditLog($em, new AuditLog('piece', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
- }
-
- foreach ($uow->getScheduledEntityUpdates() as $entity) {
- if (!$entity instanceof Piece) {
- continue;
- }
-
- $pieceId = (string) $entity->getId();
- if ('' === $pieceId) {
- continue;
- }
-
- $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
- if ([] !== $diff) {
- $pendingUpdates[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff);
- $pendingSnapshots[$pieceId] = $this->snapshotPiece($entity);
- $pendingPieces[$pieceId] = $entity;
- }
- }
-
- foreach ($uow->getScheduledEntityDeletions() as $entity) {
- if (!$entity instanceof Piece) {
- continue;
- }
-
- $snapshot = $this->snapshotPiece($entity);
- $this->persistAuditLog($em, new AuditLog('piece', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
- }
-
- foreach ($uow->getScheduledCollectionUpdates() as $collection) {
- $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingPieces);
- }
- foreach ($uow->getScheduledCollectionDeletions() as $collection) {
- $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingPieces);
- }
-
- $this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingPieces);
-
- foreach ($pendingUpdates as $pieceId => $diff) {
- if ([] === $diff) {
- continue;
- }
-
- $piece = $pendingPieces[$pieceId] ?? null;
- if (!$piece instanceof Piece) {
- continue;
- }
-
- $snapshot = $pendingSnapshots[$pieceId] ?? $this->snapshotPiece($piece);
- $this->persistAuditLog($em, new AuditLog('piece', $pieceId, 'update', $diff, $snapshot, $actorProfileId));
- }
- }
-
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingPieces
- */
- private function collectCollectionUpdate(
- object $collection,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingPieces,
- ): void {
- if (!$collection instanceof PersistentCollection) {
- return;
- }
-
- $owner = $collection->getOwner();
- if (!$owner instanceof Piece) {
- return;
- }
-
- $pieceId = (string) $owner->getId();
- if ('' === $pieceId) {
- 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[$pieceId] = $this->mergeDiffs($pendingUpdates[$pieceId] ?? [], $diff);
- $pendingSnapshots[$pieceId] = $this->snapshotPiece($owner);
- $pendingPieces[$pieceId] = $owner;
- }
-
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingPieces
- */
- private function collectCustomFieldValueChanges(
- UnitOfWork $uow,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingPieces,
- ): void {
- foreach ($uow->getScheduledEntityInsertions() as $entity) {
- if ($entity instanceof CustomFieldValue) {
- $this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingPieces);
- }
- }
-
- foreach ($uow->getScheduledEntityUpdates() as $entity) {
- if (!$entity instanceof CustomFieldValue) {
- continue;
- }
- $changeSet = $uow->getEntityChangeSet($entity);
- if (!isset($changeSet['value'])) {
- continue;
- }
- [$oldVal, $newVal] = $changeSet['value'];
- if ($oldVal !== $newVal) {
- $this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingPieces);
- }
- }
-
- foreach ($uow->getScheduledEntityDeletions() as $entity) {
- if ($entity instanceof CustomFieldValue) {
- $this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingPieces);
- }
- }
- }
-
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingPieces
- */
- private function trackCustomFieldValueChange(
- CustomFieldValue $cfv,
- mixed $from,
- mixed $to,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingPieces,
- ): void {
- $owner = $cfv->getPiece();
- if (!$owner instanceof Piece) {
- return;
- }
-
- $ownerId = (string) $owner->getId();
- if ('' === $ownerId) {
- return;
- }
-
- $fieldName = 'customField:'.$cfv->getCustomField()->getName();
- $diff = [$fieldName => ['from' => $from, 'to' => $to]];
-
- $pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
- $pendingSnapshots[$ownerId] = $this->snapshotPiece($owner);
- $pendingPieces[$ownerId] = $owner;
- }
-
- private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
- {
- $uow = $em->getUnitOfWork();
- $log->initializeAuditLog();
- $em->persist($log);
-
- $meta = $em->getClassMetadata(AuditLog::class);
- $uow->computeChangeSet($meta, $log);
- }
-
- /**
- * @param array $changeSet
- *
- * @return array
- */
- private function buildDiffFromChangeSet(array $changeSet): array
- {
- $diff = [];
- foreach ($changeSet as $field => [$oldValue, $newValue]) {
- if ('updatedAt' === $field || 'createdAt' === $field) {
- continue;
- }
-
- $normalizedOld = $this->normalizeValue($oldValue);
- $normalizedNew = $this->normalizeValue($newValue);
-
- if ($normalizedOld === $normalizedNew) {
- continue;
- }
-
- $diff[$field] = [
- 'from' => $normalizedOld,
- 'to' => $normalizedNew,
- ];
- }
-
- return $diff;
- }
-
- private function 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(),
- 'constructeurIds' => $this->normalizeCollection($piece->getConstructeurs()),
- ];
- }
-
- /**
- * @param iterable $items
- *
- * @return list
- */
- private function normalizeCollection(iterable $items): array
- {
- $entries = [];
- $seen = [];
- foreach ($items as $item) {
- if (is_object($item) && method_exists($item, 'getId')) {
- $id = $item->getId();
- if (null === $id || '' === $id || isset($seen[(string) $id])) {
- continue;
- }
- $seen[(string) $id] = true;
- if (method_exists($item, 'getName')) {
- $entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
- } else {
- $entries[] = (string) $id;
- }
- }
- }
-
- return $entries;
- }
-
- private function normalizeValue(mixed $value): mixed
- {
- if (null === $value || is_scalar($value)) {
- return $value;
- }
-
- if ($value instanceof DateTimeInterface) {
- return $value->format(DateTimeInterface::ATOM);
- }
-
- if ($value instanceof ModelType) {
- return [
- 'id' => $value->getId(),
- 'name' => $value->getName(),
- 'code' => $value->getCode(),
- ];
- }
-
- if ($value instanceof Product) {
- return [
- 'id' => $value->getId(),
- 'name' => $value->getName(),
- 'reference' => $value->getReference(),
- ];
- }
-
- if ($value instanceof Collection) {
- return $this->normalizeCollection($value);
- }
-
- if (is_object($value) && method_exists($value, 'getId')) {
- return (string) $value->getId();
- }
-
- if (is_array($value)) {
- return $value;
- }
-
- return (string) $value;
- }
-
- /**
- * @param array $base
- * @param array $extra
- *
- * @return array
- */
- private function mergeDiffs(array $base, array $extra): array
- {
- foreach ($extra as $field => $change) {
- $base[$field] = $change;
- }
-
- return $base;
- }
-
- private function resolveActorProfileId(): ?string
- {
- try {
- $session = $this->requestStack->getSession();
- if ($session instanceof SessionInterface) {
- $profileId = $session->get('profileId');
- if ($profileId) {
- return (string) $profileId;
- }
- }
- } catch (Throwable) {
- // No session available (CLI context, etc.)
- }
-
- $user = $this->security->getUser();
- if ($user instanceof Profile) {
- return $user->getId();
- }
-
- return null;
- }
}
diff --git a/src/EventSubscriber/ProductAuditSubscriber.php b/src/EventSubscriber/ProductAuditSubscriber.php
index c620d92..b6ec6e2 100644
--- a/src/EventSubscriber/ProductAuditSubscriber.php
+++ b/src/EventSubscriber/ProductAuditSubscriber.php
@@ -4,393 +4,43 @@ 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;
-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.
- *
- * This MVP focuses on Product updates and captures:
- * - scalar field changes (from Doctrine change sets)
- * - constructeur collection changes (from collection updates)
- */
#[AsDoctrineListener(event: Events::onFlush)]
-final class ProductAuditSubscriber implements EventSubscriber
+final class ProductAuditSubscriber extends AbstractAuditSubscriber
{
- public function __construct(
- private readonly RequestStack $requestStack,
- private readonly Security $security,
- ) {}
+ protected function supports(object $entity): bool
+ {
+ return $entity instanceof Product;
+ }
- public function getSubscribedEvents(): array
+ protected function entityType(): string
+ {
+ return 'product';
+ }
+
+ protected function hasCollectionTracking(): bool
+ {
+ return true;
+ }
+
+ protected function getOwnerFromCustomFieldValue(CustomFieldValue $cfv): ?object
+ {
+ return $cfv->getProduct();
+ }
+
+ protected function snapshotEntity(object $entity): array
{
return [
- Events::onFlush,
+ 'id' => $entity->getId(),
+ 'name' => $this->safeGet($entity, 'getName'),
+ 'reference' => $this->safeGet($entity, 'getReference'),
+ 'supplierPrice' => $this->safeGet($entity, 'getSupplierPrice'),
+ 'typeProduct' => $this->normalizeValue($this->safeGet($entity, 'getTypeProduct')),
+ 'constructeurIds' => $this->normalizeCollection($entity->getConstructeurs()),
];
}
-
- public function onFlush(OnFlushEventArgs $args): void
- {
- $em = $args->getObjectManager();
- if (!$em instanceof EntityManagerInterface) {
- return;
- }
-
- $uow = $em->getUnitOfWork();
- $actorProfileId = $this->resolveActorProfileId();
- $pendingUpdates = [];
- $pendingSnapshots = [];
- $pendingProducts = [];
-
- foreach ($uow->getScheduledEntityInsertions() as $entity) {
- if (!$entity instanceof Product) {
- continue;
- }
-
- $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
- $snapshot = $this->snapshotProduct($entity);
- $this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'create', $diff, $snapshot, $actorProfileId));
- }
-
- foreach ($uow->getScheduledEntityUpdates() as $entity) {
- if (!$entity instanceof Product) {
- continue;
- }
-
- $productId = (string) $entity->getId();
- if ('' === $productId) {
- continue;
- }
-
- $diff = $this->buildDiffFromChangeSet($uow->getEntityChangeSet($entity));
- if ([] !== $diff) {
- $pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
- $pendingSnapshots[$productId] = $this->snapshotProduct($entity);
- $pendingProducts[$productId] = $entity;
- }
- }
-
- foreach ($uow->getScheduledEntityDeletions() as $entity) {
- if (!$entity instanceof Product) {
- continue;
- }
-
- $snapshot = $this->snapshotProduct($entity);
- $this->persistAuditLog($em, new AuditLog('product', (string) $entity->getId(), 'delete', null, $snapshot, $actorProfileId));
- }
-
- // Capture constructeur collection updates, which are not included in the change set.
- foreach ($uow->getScheduledCollectionUpdates() as $collection) {
- $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts);
- }
- foreach ($uow->getScheduledCollectionDeletions() as $collection) {
- $this->collectCollectionUpdate($collection, $pendingUpdates, $pendingSnapshots, $pendingProducts);
- }
-
- $this->collectCustomFieldValueChanges($uow, $pendingUpdates, $pendingSnapshots, $pendingProducts);
-
- foreach ($pendingUpdates as $productId => $diff) {
- if ([] === $diff) {
- continue;
- }
-
- $product = $pendingProducts[$productId] ?? null;
- if (!$product instanceof Product) {
- continue;
- }
-
- $snapshot = $pendingSnapshots[$productId] ?? $this->snapshotProduct($product);
- $this->persistAuditLog($em, new AuditLog('product', $productId, 'update', $diff, $snapshot, $actorProfileId));
- }
- }
-
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingProducts
- */
- private function collectCollectionUpdate(
- object $collection,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingProducts,
- ): void {
- if (!$collection instanceof PersistentCollection) {
- return;
- }
-
- $owner = $collection->getOwner();
- if (!$owner instanceof Product) {
- return;
- }
-
- $productId = (string) $owner->getId();
- if ('' === $productId) {
- return;
- }
-
- $mapping = $collection->getMapping();
- $fieldName = $mapping['fieldName'] ?? null;
- if ('constructeurs' !== $fieldName) {
- return;
- }
-
- $before = $this->normalizeCollection($collection->getSnapshot());
- $after = $this->normalizeCollection($collection->toArray());
-
- if ($before === $after) {
- return;
- }
-
- $diff = [
- 'constructeurIds' => [
- 'from' => $before,
- 'to' => $after,
- ],
- ];
-
- $pendingUpdates[$productId] = $this->mergeDiffs($pendingUpdates[$productId] ?? [], $diff);
- $pendingSnapshots[$productId] = $this->snapshotProduct($owner);
- $pendingProducts[$productId] = $owner;
- }
-
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingProducts
- */
- private function collectCustomFieldValueChanges(
- UnitOfWork $uow,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingProducts,
- ): void {
- foreach ($uow->getScheduledEntityInsertions() as $entity) {
- if ($entity instanceof CustomFieldValue) {
- $this->trackCustomFieldValueChange($entity, null, $entity->getValue(), $pendingUpdates, $pendingSnapshots, $pendingProducts);
- }
- }
-
- foreach ($uow->getScheduledEntityUpdates() as $entity) {
- if (!$entity instanceof CustomFieldValue) {
- continue;
- }
- $changeSet = $uow->getEntityChangeSet($entity);
- if (!isset($changeSet['value'])) {
- continue;
- }
- [$oldVal, $newVal] = $changeSet['value'];
- if ($oldVal !== $newVal) {
- $this->trackCustomFieldValueChange($entity, $oldVal, $newVal, $pendingUpdates, $pendingSnapshots, $pendingProducts);
- }
- }
-
- foreach ($uow->getScheduledEntityDeletions() as $entity) {
- if ($entity instanceof CustomFieldValue) {
- $this->trackCustomFieldValueChange($entity, $entity->getValue(), null, $pendingUpdates, $pendingSnapshots, $pendingProducts);
- }
- }
- }
-
- /**
- * @param array> $pendingUpdates
- * @param array> $pendingSnapshots
- * @param array $pendingProducts
- */
- private function trackCustomFieldValueChange(
- CustomFieldValue $cfv,
- mixed $from,
- mixed $to,
- array &$pendingUpdates,
- array &$pendingSnapshots,
- array &$pendingProducts,
- ): void {
- $owner = $cfv->getProduct();
- if (!$owner instanceof Product) {
- return;
- }
-
- $ownerId = (string) $owner->getId();
- if ('' === $ownerId) {
- return;
- }
-
- $fieldName = 'customField:'.$cfv->getCustomField()->getName();
- $diff = [$fieldName => ['from' => $from, 'to' => $to]];
-
- $pendingUpdates[$ownerId] = $this->mergeDiffs($pendingUpdates[$ownerId] ?? [], $diff);
- $pendingSnapshots[$ownerId] = $this->snapshotProduct($owner);
- $pendingProducts[$ownerId] = $owner;
- }
-
- private function persistAuditLog(EntityManagerInterface $em, AuditLog $log): void
- {
- $uow = $em->getUnitOfWork();
- // Ensure identifiers and timestamps are set even when persisting during onFlush.
- $log->initializeAuditLog();
- $em->persist($log);
-
- $meta = $em->getClassMetadata(AuditLog::class);
- $uow->computeChangeSet($meta, $log);
- }
-
- /**
- * @param array $changeSet
- *
- * @return array
- */
- private function buildDiffFromChangeSet(array $changeSet): array
- {
- $diff = [];
- foreach ($changeSet as $field => [$oldValue, $newValue]) {
- // Skip noisy timestamps managed automatically.
- if ('updatedAt' === $field || 'createdAt' === $field) {
- continue;
- }
-
- $normalizedOld = $this->normalizeValue($oldValue);
- $normalizedNew = $this->normalizeValue($newValue);
-
- if ($normalizedOld === $normalizedNew) {
- continue;
- }
-
- $diff[$field] = [
- 'from' => $normalizedOld,
- 'to' => $normalizedNew,
- ];
- }
-
- return $diff;
- }
-
- private function snapshotProduct(Product $product): array
- {
- return [
- 'id' => $product->getId(),
- 'name' => $product->getName(),
- 'reference' => $product->getReference(),
- 'supplierPrice' => $product->getSupplierPrice(),
- 'typeProduct' => $this->normalizeValue($product->getTypeProduct()),
- 'constructeurIds' => $this->normalizeCollection($product->getConstructeurs()),
- ];
- }
-
- /**
- * @param array $base
- * @param array $extra
- *
- * @return array
- */
- private function mergeDiffs(array $base, array $extra): array
- {
- foreach ($extra as $field => $change) {
- $base[$field] = $change;
- }
-
- return $base;
- }
-
- /**
- * @param iterable $items
- *
- * @return list
- */
- private function normalizeCollection(iterable $items): array
- {
- $entries = [];
- $seen = [];
- foreach ($items as $item) {
- if (is_object($item) && method_exists($item, 'getId')) {
- $id = $item->getId();
- if (null === $id || '' === $id || isset($seen[(string) $id])) {
- continue;
- }
- $seen[(string) $id] = true;
- if (method_exists($item, 'getName')) {
- $entries[] = ['id' => (string) $id, 'name' => (string) $item->getName()];
- } else {
- $entries[] = (string) $id;
- }
- }
- }
-
- return $entries;
- }
-
- private function normalizeValue(mixed $value): mixed
- {
- if (null === $value || is_scalar($value)) {
- return $value;
- }
-
- if ($value instanceof DateTimeInterface) {
- return $value->format(DateTimeInterface::ATOM);
- }
-
- if ($value instanceof ModelType) {
- return [
- 'id' => $value->getId(),
- 'name' => $value->getName(),
- 'code' => $value->getCode(),
- ];
- }
-
- if ($value instanceof Collection) {
- return $this->normalizeCollection($value);
- }
-
- if (is_object($value) && method_exists($value, 'getId')) {
- return (string) $value->getId();
- }
-
- if (is_array($value)) {
- return $value;
- }
-
- return (string) $value;
- }
-
- private function resolveActorProfileId(): ?string
- {
- try {
- $session = $this->requestStack->getSession();
- if ($session instanceof SessionInterface) {
- $profileId = $session->get('profileId');
- if ($profileId) {
- return (string) $profileId;
- }
- }
- } catch (Throwable) {
- // No session available (CLI context, etc.)
- }
-
- $user = $this->security->getUser();
- if ($user instanceof Profile) {
- return $user->getId();
- }
-
- return null;
- }
}
diff --git a/src/OpenApi/OpenApiDecorator.php b/src/OpenApi/OpenApiDecorator.php
new file mode 100644
index 0000000..deff2ef
--- /dev/null
+++ b/src/OpenApi/OpenApiDecorator.php
@@ -0,0 +1,520 @@
+decorated)($context);
+
+ $this->addHealthCheck($openApi);
+ $this->addSessionRoutes($openApi);
+ $this->addAdminProfileRoutes($openApi);
+ $this->addActivityLogs($openApi);
+ $this->addCommentRoutes($openApi);
+ $this->addCustomFieldValueRoutes($openApi);
+ $this->addDocumentQueryRoutes($openApi);
+ $this->addDocumentServeRoutes($openApi);
+ $this->addEntityHistoryRoutes($openApi);
+ $this->addMachineStructureRoutes($openApi);
+ $this->addMachineCustomFieldsRoutes($openApi);
+ $this->addModelTypeConversionRoutes($openApi);
+
+ return $this->addTagDescriptions($openApi);
+ }
+
+ private function addHealthCheck(OpenApi $openApi): void
+ {
+ $openApi->getPaths()->addPath('/api/health', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'getHealthCheck',
+ tags: ['Monitoring'],
+ summary: 'Vérification de santé du système',
+ description: 'Retourne le statut du système, la version, la latence BDD et la mémoire utilisée.',
+ responses: [
+ '200' => $this->jsonResponse('Système opérationnel.'),
+ '503' => $this->jsonResponse('Système dégradé.'),
+ ],
+ ),
+ ));
+ }
+
+ private function addSessionRoutes(OpenApi $openApi): void
+ {
+ $openApi->getPaths()->addPath('/api/session/profiles', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'getSessionProfiles',
+ tags: ['Session'],
+ summary: 'Lister les profils disponibles',
+ description: 'Retourne les profils actifs (id, prénom, nom, hasPassword). Aucune authentification requise.',
+ responses: ['200' => $this->jsonResponse('Liste des profils.')],
+ ),
+ ));
+
+ $openApi->getPaths()->addPath('/api/session/profile', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'getSessionProfile',
+ tags: ['Session'],
+ summary: 'Profil actif de la session',
+ description: 'Retourne le profil actuellement connecté via la session.',
+ responses: [
+ '200' => $this->jsonResponse('Profil actif.'),
+ '401' => $this->jsonResponse('Aucun profil actif.'),
+ ],
+ ),
+ post: new Model\Operation(
+ operationId: 'loginSessionProfile',
+ tags: ['Session'],
+ summary: 'Connexion — activer un profil',
+ description: 'Active un profil dans la session. Requiert profileId et password.',
+ requestBody: $this->jsonRequestBody('Identifiants de connexion.'),
+ responses: [
+ '200' => $this->jsonResponse('Connexion réussie.'),
+ '401' => $this->jsonResponse('Mot de passe incorrect.'),
+ '404' => $this->jsonResponse('Profil introuvable.'),
+ ],
+ ),
+ delete: new Model\Operation(
+ operationId: 'logoutSessionProfile',
+ tags: ['Session'],
+ summary: 'Déconnexion — invalider la session',
+ responses: ['200' => $this->jsonResponse('Session invalidée.')],
+ ),
+ ));
+ }
+
+ private function addAdminProfileRoutes(OpenApi $openApi): void
+ {
+ $openApi->getPaths()->addPath('/api/admin/profiles', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'adminListProfiles',
+ tags: ['Admin — Profils'],
+ summary: 'Lister tous les profils',
+ description: 'Liste complète des profils triés par prénom. Requiert ROLE_ADMIN.',
+ responses: ['200' => $this->jsonResponse('Liste des profils.')],
+ ),
+ post: new Model\Operation(
+ operationId: 'adminCreateProfile',
+ tags: ['Admin — Profils'],
+ summary: 'Créer un profil',
+ description: 'Crée un nouveau profil avec rôle. Requiert ROLE_ADMIN.',
+ requestBody: $this->jsonRequestBody('Données du profil (firstName, lastName, email, password, role).'),
+ responses: [
+ '201' => $this->jsonResponse('Profil créé.'),
+ '400' => $this->jsonResponse('Données invalides.'),
+ '409' => $this->jsonResponse('Email déjà utilisé.'),
+ ],
+ ),
+ ));
+
+ $idParam = $this->pathParam('id', 'Identifiant du profil');
+
+ $openApi->getPaths()->addPath('/api/admin/profiles/{id}/role', new Model\PathItem(
+ put: new Model\Operation(
+ operationId: 'adminUpdateProfileRole',
+ tags: ['Admin — Profils'],
+ summary: 'Modifier le rôle d\'un profil',
+ description: 'Change le rôle d\'un profil. Empêche la suppression du dernier admin. Requiert ROLE_ADMIN.',
+ parameters: [$idParam],
+ requestBody: $this->jsonRequestBody('Nouveau rôle.'),
+ responses: [
+ '200' => $this->jsonResponse('Rôle mis à jour.'),
+ '400' => $this->jsonResponse('Rôle invalide ou dernier admin.'),
+ ],
+ ),
+ ));
+
+ $openApi->getPaths()->addPath('/api/admin/profiles/{id}/password', new Model\PathItem(
+ put: new Model\Operation(
+ operationId: 'adminUpdateProfilePassword',
+ tags: ['Admin — Profils'],
+ summary: 'Modifier le mot de passe d\'un profil',
+ description: 'Requiert ROLE_ADMIN.',
+ parameters: [$idParam],
+ requestBody: $this->jsonRequestBody('Nouveau mot de passe.'),
+ responses: ['200' => $this->jsonResponse('Mot de passe mis à jour.')],
+ ),
+ ));
+
+ $openApi->getPaths()->addPath('/api/admin/profiles/{id}/deactivate', new Model\PathItem(
+ put: new Model\Operation(
+ operationId: 'adminDeactivateProfile',
+ tags: ['Admin — Profils'],
+ summary: 'Désactiver un profil',
+ description: 'Désactive un profil. Empêche la désactivation du dernier admin. Requiert ROLE_ADMIN.',
+ parameters: [$idParam],
+ responses: [
+ '200' => $this->jsonResponse('Profil désactivé.'),
+ '400' => $this->jsonResponse('Dernier admin, impossible de désactiver.'),
+ ],
+ ),
+ ));
+ }
+
+ private function addActivityLogs(OpenApi $openApi): void
+ {
+ $openApi->getPaths()->addPath('/api/activity-logs', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'getActivityLogs',
+ tags: ['Audit'],
+ summary: 'Journal d\'activité paginé',
+ description: 'Retourne les logs d\'audit avec filtrage optionnel par type d\'entité et action. Requiert ROLE_VIEWER.',
+ parameters: [
+ $this->queryParam('page', 'Numéro de page'),
+ $this->queryParam('itemsPerPage', 'Éléments par page'),
+ $this->queryParam('entityType', 'Filtrer par type d\'entité'),
+ $this->queryParam('action', 'Filtrer par action'),
+ ],
+ responses: ['200' => $this->jsonResponse('Logs paginés.')],
+ ),
+ ));
+ }
+
+ private function addCommentRoutes(OpenApi $openApi): void
+ {
+ $openApi->getPaths()->addPath('/api/comments', new Model\PathItem(
+ post: new Model\Operation(
+ operationId: 'createComment',
+ tags: ['Commentaires'],
+ summary: 'Créer un commentaire',
+ description: 'Ajoute un commentaire à une entité (machine, pièce, composant, produit, catégorie, squelette). Requiert ROLE_VIEWER.',
+ requestBody: $this->jsonRequestBody('Données du commentaire (content, entityType, entityId, entityName).'),
+ responses: [
+ '201' => $this->jsonResponse('Commentaire créé.'),
+ '400' => $this->jsonResponse('Données invalides.'),
+ ],
+ ),
+ ));
+
+ $openApi->getPaths()->addPath('/api/comments/{id}/resolve', new Model\PathItem(
+ patch: new Model\Operation(
+ operationId: 'resolveComment',
+ tags: ['Commentaires'],
+ summary: 'Résoudre un commentaire',
+ description: 'Marque un commentaire comme résolu. Requiert ROLE_GESTIONNAIRE.',
+ parameters: [$this->pathParam('id', 'Identifiant du commentaire')],
+ responses: [
+ '200' => $this->jsonResponse('Commentaire résolu.'),
+ '404' => $this->jsonResponse('Commentaire introuvable.'),
+ ],
+ ),
+ ));
+
+ $openApi->getPaths()->addPath('/api/comments/stats/unresolved-count', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'getUnresolvedCommentCount',
+ tags: ['Commentaires'],
+ summary: 'Nombre de commentaires non résolus',
+ description: 'Requiert ROLE_VIEWER.',
+ responses: ['200' => $this->jsonResponse('Compteur.')],
+ ),
+ ));
+ }
+
+ private function addCustomFieldValueRoutes(OpenApi $openApi): void
+ {
+ $openApi->getPaths()->addPath('/api/custom-fields/values', new Model\PathItem(
+ post: new Model\Operation(
+ operationId: 'createCustomFieldValue',
+ tags: ['Champs personnalisés'],
+ summary: 'Créer une valeur de champ personnalisé',
+ description: 'Crée une valeur pour un champ personnalisé sur une entité. Auto-crée le champ si nécessaire. Requiert ROLE_GESTIONNAIRE.',
+ requestBody: $this->jsonRequestBody('Données (customFieldId/customFieldName, value, entité cible).'),
+ responses: [
+ '201' => $this->jsonResponse('Valeur créée.'),
+ '400' => $this->jsonResponse('Données invalides.'),
+ ],
+ ),
+ ));
+
+ $openApi->getPaths()->addPath('/api/custom-fields/values/upsert', new Model\PathItem(
+ post: new Model\Operation(
+ operationId: 'upsertCustomFieldValue',
+ tags: ['Champs personnalisés'],
+ summary: 'Créer ou mettre à jour une valeur de champ personnalisé',
+ description: 'Requiert ROLE_GESTIONNAIRE.',
+ requestBody: $this->jsonRequestBody('Données du champ.'),
+ responses: ['200' => $this->jsonResponse('Valeur créée ou mise à jour.')],
+ ),
+ ));
+
+ $openApi->getPaths()->addPath('/api/custom-fields/values/{entityType}/{entityId}', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'listCustomFieldValues',
+ tags: ['Champs personnalisés'],
+ summary: 'Lister les valeurs de champs personnalisés d\'une entité',
+ description: 'Requiert ROLE_VIEWER.',
+ parameters: [
+ $this->pathParam('entityType', 'Type d\'entité (machine, composant, piece, product)'),
+ $this->pathParam('entityId', 'Identifiant de l\'entité'),
+ ],
+ responses: ['200' => $this->jsonResponse('Liste des valeurs.')],
+ ),
+ ));
+
+ $idParam = $this->pathParam('id', 'Identifiant de la valeur');
+
+ $openApi->getPaths()->addPath('/api/custom-fields/values/{id}', new Model\PathItem(
+ patch: new Model\Operation(
+ operationId: 'updateCustomFieldValue',
+ tags: ['Champs personnalisés'],
+ summary: 'Modifier une valeur de champ personnalisé',
+ description: 'Requiert ROLE_GESTIONNAIRE.',
+ parameters: [$idParam],
+ requestBody: $this->jsonRequestBody('Nouvelle valeur.'),
+ responses: ['200' => $this->jsonResponse('Valeur mise à jour.')],
+ ),
+ delete: new Model\Operation(
+ operationId: 'deleteCustomFieldValue',
+ tags: ['Champs personnalisés'],
+ summary: 'Supprimer une valeur de champ personnalisé',
+ description: 'Requiert ROLE_GESTIONNAIRE.',
+ parameters: [$idParam],
+ responses: ['204' => new Model\Response(description: 'Valeur supprimée.')],
+ ),
+ ));
+ }
+
+ private function addDocumentQueryRoutes(OpenApi $openApi): void
+ {
+ $entities = [
+ 'site' => 'un site',
+ 'machine' => 'une machine',
+ 'composant' => 'un composant',
+ 'piece' => 'une pièce',
+ 'product' => 'un produit',
+ ];
+
+ foreach ($entities as $entity => $label) {
+ $openApi->getPaths()->addPath("/api/documents/{$entity}/{id}", new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'getDocumentsBy'.ucfirst($entity),
+ tags: ['Documents'],
+ summary: "Documents rattachés à {$label}",
+ description: 'Requiert ROLE_VIEWER.',
+ parameters: [$this->pathParam('id', "Identifiant de l'entité")],
+ responses: ['200' => $this->jsonResponse('Liste des documents.')],
+ ),
+ ));
+ }
+ }
+
+ private function addDocumentServeRoutes(OpenApi $openApi): void
+ {
+ $idParam = $this->pathParam('id', 'Identifiant du document');
+
+ $openApi->getPaths()->addPath('/api/documents/{id}/file', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'serveDocumentFile',
+ tags: ['Documents'],
+ summary: 'Afficher un fichier document (inline)',
+ description: 'Sert le fichier pour affichage dans le navigateur. Requiert ROLE_VIEWER.',
+ parameters: [$idParam],
+ responses: ['200' => new Model\Response(description: 'Contenu du fichier.')],
+ ),
+ ));
+
+ $openApi->getPaths()->addPath('/api/documents/{id}/download', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'downloadDocumentFile',
+ tags: ['Documents'],
+ summary: 'Télécharger un fichier document',
+ description: 'Sert le fichier en téléchargement (attachment). Requiert ROLE_VIEWER.',
+ parameters: [$idParam],
+ responses: ['200' => new Model\Response(description: 'Fichier en téléchargement.')],
+ ),
+ ));
+ }
+
+ private function addEntityHistoryRoutes(OpenApi $openApi): void
+ {
+ $entities = [
+ 'machines' => 'une machine',
+ 'pieces' => 'une pièce',
+ 'composants' => 'un composant',
+ 'products' => 'un produit',
+ ];
+
+ foreach ($entities as $plural => $label) {
+ $openApi->getPaths()->addPath("/api/{$plural}/{id}/history", new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'get'.ucfirst(rtrim($plural, 's')).'History',
+ tags: ['Audit'],
+ summary: "Historique d'audit de {$label}",
+ description: "Retourne les 200 derniers événements d'audit. Requiert ROLE_VIEWER.",
+ parameters: [$this->pathParam('id', "Identifiant de l'entité")],
+ responses: ['200' => $this->jsonResponse('Historique paginé.')],
+ ),
+ ));
+ }
+ }
+
+ private function addMachineStructureRoutes(OpenApi $openApi): void
+ {
+ $idParam = $this->pathParam('id', 'Identifiant de la machine');
+
+ $openApi->getPaths()->addPath('/api/machines/{id}/structure', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'getMachineStructure',
+ tags: ['Machines — Structure'],
+ summary: 'Structure complète d\'une machine',
+ description: 'Retourne les composants, pièces et produits avec hiérarchie. Requiert ROLE_VIEWER.',
+ parameters: [$idParam],
+ responses: ['200' => $this->jsonResponse('Structure de la machine.')],
+ ),
+ patch: new Model\Operation(
+ operationId: 'updateMachineStructure',
+ tags: ['Machines — Structure'],
+ summary: 'Modifier la structure d\'une machine',
+ description: 'Crée, met à jour ou supprime les liens composants/pièces/produits. Requiert ROLE_GESTIONNAIRE.',
+ parameters: [$idParam],
+ requestBody: $this->jsonRequestBody('Liens à créer/modifier/supprimer.'),
+ responses: ['200' => $this->jsonResponse('Structure mise à jour.')],
+ ),
+ ));
+
+ $openApi->getPaths()->addPath('/api/machines/{id}/clone', new Model\PathItem(
+ post: new Model\Operation(
+ operationId: 'cloneMachine',
+ tags: ['Machines — Structure'],
+ summary: 'Cloner une machine',
+ description: 'Clone une machine avec tous ses composants, pièces, produits, champs personnalisés et constructeurs. Requiert ROLE_GESTIONNAIRE.',
+ parameters: [$idParam],
+ requestBody: $this->jsonRequestBody('Données de la copie (name, siteId, reference).'),
+ responses: [
+ '201' => $this->jsonResponse('Machine clonée.'),
+ '404' => $this->jsonResponse('Machine source introuvable.'),
+ ],
+ ),
+ ));
+ }
+
+ private function addMachineCustomFieldsRoutes(OpenApi $openApi): void
+ {
+ $openApi->getPaths()->addPath('/api/machines/{id}/add-custom-fields', new Model\PathItem(
+ post: new Model\Operation(
+ operationId: 'addMachineCustomFields',
+ tags: ['Machines — Structure'],
+ summary: 'Initialiser les champs personnalisés manquants',
+ description: 'Crée les entrées de valeur manquantes pour les champs personnalisés définis. Requiert ROLE_GESTIONNAIRE.',
+ parameters: [$this->pathParam('id', 'Identifiant de la machine')],
+ responses: ['200' => $this->jsonResponse('Champs ajoutés.')],
+ ),
+ ));
+ }
+
+ private function addModelTypeConversionRoutes(OpenApi $openApi): void
+ {
+ $idParam = $this->pathParam('id', 'Identifiant du ModelType');
+
+ $openApi->getPaths()->addPath('/api/model_types/{id}/conversion-check', new Model\PathItem(
+ get: new Model\Operation(
+ operationId: 'checkModelTypeConversion',
+ tags: ['ModelType'],
+ summary: 'Vérifier la convertibilité d\'un ModelType',
+ description: 'Vérifie si la catégorie du ModelType peut être convertie. Requiert ROLE_VIEWER.',
+ parameters: [$idParam],
+ responses: ['200' => $this->jsonResponse('Résultat de la vérification.')],
+ ),
+ ));
+
+ $openApi->getPaths()->addPath('/api/model_types/{id}/convert', new Model\PathItem(
+ post: new Model\Operation(
+ operationId: 'convertModelType',
+ tags: ['ModelType'],
+ summary: 'Convertir la catégorie d\'un ModelType',
+ description: 'Convertit la catégorie. Retourne 409 en cas de conflit. Requiert ROLE_GESTIONNAIRE.',
+ parameters: [$idParam],
+ responses: [
+ '200' => $this->jsonResponse('Conversion effectuée.'),
+ '409' => $this->jsonResponse('Conflit — conversion impossible.'),
+ ],
+ ),
+ ));
+ }
+
+ private function addTagDescriptions(OpenApi $openApi): OpenApi
+ {
+ $customTags = [
+ 'Monitoring' => 'Supervision et vérification de l\'état de santé du système (statut, version, latence BDD, mémoire).',
+ 'Session' => 'Authentification par session. Permet de lister les profils disponibles, se connecter (activer un profil) et se déconnecter.',
+ 'Admin — Profils' => 'Administration des profils utilisateurs. Création, modification des rôles et mots de passe, désactivation. Réservé aux administrateurs.',
+ 'Audit' => 'Journal d\'activité et historique d\'audit. Consultation des modifications apportées aux entités avec détail des changements (diff et snapshot).',
+ 'Commentaires' => 'Système de commentaires et annotations sur les entités. Permet de créer des commentaires, les résoudre et suivre le nombre de commentaires ouverts.',
+ 'Champs personnalisés' => 'Gestion des valeurs de champs personnalisés. Permet d\'ajouter, modifier et supprimer des données dynamiques sur les machines, pièces, composants et produits.',
+ 'Documents' => 'Gestion des fichiers joints. Consultation des documents par entité, affichage inline et téléchargement.',
+ 'Machines — Structure' => 'Structure hiérarchique des machines. Consultation et modification des liaisons composants/pièces/produits, clonage de machines et initialisation des champs personnalisés.',
+ 'ModelType' => 'Conversion de catégories de types. Vérification de compatibilité et conversion effective des catégories de ModelType.',
+ ];
+
+ $existingTags = $openApi->getTags();
+ $existingNames = array_map(static fn (Model\Tag $tag) => $tag->getName(), $existingTags);
+
+ foreach ($customTags as $name => $description) {
+ if (!in_array($name, $existingNames, true)) {
+ $existingTags[] = new Model\Tag(name: $name, description: $description);
+ }
+ }
+
+ return $openApi->withTags($existingTags);
+ }
+
+ private function jsonResponse(string $description): Model\Response
+ {
+ return new Model\Response(
+ description: $description,
+ content: new ArrayObject([
+ 'application/json' => new Model\MediaType(
+ schema: new ArrayObject(['type' => 'object']),
+ ),
+ ]),
+ );
+ }
+
+ private function jsonRequestBody(string $description): Model\RequestBody
+ {
+ return new Model\RequestBody(
+ description: $description,
+ content: new ArrayObject([
+ 'application/json' => new Model\MediaType(
+ schema: new ArrayObject(['type' => 'object']),
+ ),
+ ]),
+ required: true,
+ );
+ }
+
+ private function pathParam(string $name, string $description): Model\Parameter
+ {
+ return new Model\Parameter(
+ name: $name,
+ in: 'path',
+ description: $description,
+ required: true,
+ schema: ['type' => 'string'],
+ );
+ }
+
+ private function queryParam(string $name, string $description): Model\Parameter
+ {
+ return new Model\Parameter(
+ name: $name,
+ in: 'query',
+ description: $description,
+ required: false,
+ schema: ['type' => 'string'],
+ );
+ }
+}
diff --git a/src/Service/PdfCompressorService.php b/src/Service/PdfCompressorService.php
index cf1df3a..aeba15c 100644
--- a/src/Service/PdfCompressorService.php
+++ b/src/Service/PdfCompressorService.php
@@ -6,6 +6,8 @@ namespace App\Service;
class PdfCompressorService
{
+ private ?bool $qpdfAvailable = null;
+
/**
* Compress an actual PDF file on disk. Returns metadata or null if no gain.
*
@@ -13,8 +15,7 @@ class PdfCompressorService
*/
public function compressFile(string $absolutePath): ?array
{
- exec('which qpdf', $qpdfPath, $returnCode);
- if (0 !== $returnCode) {
+ if (!$this->isQpdfAvailable()) {
return null;
}
@@ -65,9 +66,7 @@ class PdfCompressorService
public function compressBase64Pdf(string $base64Data): ?array
{
- // Check if qpdf is available
- exec('which qpdf', $qpdfPath, $returnCode);
- if (0 !== $returnCode) {
+ if (!$this->isQpdfAvailable()) {
return null;
}
@@ -127,4 +126,14 @@ class PdfCompressorService
'saved' => $originalSize - $compressedSize,
];
}
+
+ private function isQpdfAvailable(): bool
+ {
+ if (null === $this->qpdfAvailable) {
+ exec('which qpdf', $qpdfPath, $returnCode);
+ $this->qpdfAvailable = 0 === $returnCode;
+ }
+
+ return $this->qpdfAvailable;
+ }
}