fix(documents) : add serialization groups to prevent OOM on collection endpoint

The path field (base64 data URIs) is now excluded from GetCollection
via document:list group. Individual GET returns path via document:detail
group. Related entities expose id+name in document:list for attachment
display. Frontend lazy-loads path on download/preview click.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-02-11 17:16:27 +01:00
parent db16d26103
commit 91b8b424d6
7 changed files with 55 additions and 30 deletions

View File

@@ -30,11 +30,11 @@ class Composant
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)] #[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['composant:read'])] #[Groups(['composant:read', 'document:list'])]
private ?string $id = null; private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)] #[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['composant:read'])] #[Groups(['composant:read', 'document:list'])]
private string $name; private string $name;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)] #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]

View File

@@ -5,6 +5,11 @@ declare(strict_types=1);
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use App\Repository\DocumentRepository; use App\Repository\DocumentRepository;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
@@ -15,6 +20,13 @@ use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Table(name: 'documents')] #[ORM\Table(name: 'documents')]
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ApiResource( #[ApiResource(
operations: [
new GetCollection(normalizationContext: ['groups' => ['document:list']]),
new Get(normalizationContext: ['groups' => ['document:list', 'document:detail']]),
new Post(),
new Put(),
new Delete(),
],
paginationClientItemsPerPage: true, paginationClientItemsPerPage: true,
paginationMaximumItemsPerPage: 200 paginationMaximumItemsPerPage: 200
)] )]
@@ -22,50 +34,56 @@ class Document
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)] #[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])] #[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private ?string $id = null; private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255)] #[ORM\Column(type: Types::STRING, length: 255)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])] #[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $name; private string $name;
#[ORM\Column(type: Types::STRING, length: 255)] #[ORM\Column(type: Types::STRING, length: 255)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])] #[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $filename; private string $filename;
#[ORM\Column(type: Types::TEXT)] #[ORM\Column(type: Types::TEXT)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])] #[Groups(['document:detail', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $path; private string $path;
#[ORM\Column(type: Types::STRING, length: 100, name: 'mimeType')] #[ORM\Column(type: Types::STRING, length: 100, name: 'mimeType')]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])] #[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private string $mimeType; private string $mimeType;
#[ORM\Column(type: Types::INTEGER)] #[ORM\Column(type: Types::INTEGER)]
#[Groups(['document:read', 'composant:read', 'piece:read', 'product:read'])] #[Groups(['document:list', 'document:read', 'composant:read', 'piece:read', 'product:read'])]
private int $size; private int $size;
#[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'documents')] #[ORM\ManyToOne(targetEntity: Machine::class, inversedBy: 'documents')]
#[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] #[ORM\JoinColumn(name: 'machineId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
#[Groups(['document:list'])]
private ?Machine $machine = null; private ?Machine $machine = null;
#[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'documents')] #[ORM\ManyToOne(targetEntity: Composant::class, inversedBy: 'documents')]
#[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] #[ORM\JoinColumn(name: 'composantId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
#[Groups(['document:list'])]
private ?Composant $composant = null; private ?Composant $composant = null;
#[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'documents')] #[ORM\ManyToOne(targetEntity: Piece::class, inversedBy: 'documents')]
#[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] #[ORM\JoinColumn(name: 'pieceId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
#[Groups(['document:list'])]
private ?Piece $piece = null; private ?Piece $piece = null;
#[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'documents')] #[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'documents')]
#[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] #[ORM\JoinColumn(name: 'productId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
#[Groups(['document:list'])]
private ?Product $product = null; private ?Product $product = null;
#[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'documents')] #[ORM\ManyToOne(targetEntity: Site::class, inversedBy: 'documents')]
#[ORM\JoinColumn(name: 'siteId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')] #[ORM\JoinColumn(name: 'siteId', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
#[Groups(['document:list'])]
private ?Site $site = null; private ?Site $site = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
#[Groups(['document:list'])]
private DateTimeImmutable $createdAt; private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]

View File

@@ -6,10 +6,12 @@ namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use App\Repository\MachineRepository; use App\Repository\MachineRepository;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity(repositoryClass: MachineRepository::class)] #[ORM\Entity(repositoryClass: MachineRepository::class)]
#[ORM\Table(name: 'machines')] #[ORM\Table(name: 'machines')]
@@ -19,9 +21,11 @@ class Machine
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)] #[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['document:list'])]
private ?string $id = null; private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)] #[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['document:list'])]
private string $name; private string $name;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)] #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
@@ -80,29 +84,29 @@ class Machine
private Collection $customFieldValues; private Collection $customFieldValues;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
private \DateTimeImmutable $createdAt; private DateTimeImmutable $createdAt;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')] #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
private \DateTimeImmutable $updatedAt; private DateTimeImmutable $updatedAt;
public function __construct() public function __construct()
{ {
$this->constructeurs = new ArrayCollection(); $this->constructeurs = new ArrayCollection();
$this->componentLinks = new ArrayCollection(); $this->componentLinks = new ArrayCollection();
$this->pieceLinks = new ArrayCollection(); $this->pieceLinks = new ArrayCollection();
$this->productLinks = new ArrayCollection(); $this->productLinks = new ArrayCollection();
$this->documents = new ArrayCollection(); $this->documents = new ArrayCollection();
$this->customFieldValues = new ArrayCollection(); $this->customFieldValues = new ArrayCollection();
} }
#[ORM\PrePersist] #[ORM\PrePersist]
public function setCreatedAtValue(): void public function setCreatedAtValue(): void
{ {
$now = new \DateTimeImmutable(); $now = new DateTimeImmutable();
$this->createdAt = $now; $this->createdAt = $now;
$this->updatedAt = $now; $this->updatedAt = $now;
if ($this->id === null) { if (null === $this->id) {
$this->id = $this->generateCuid(); $this->id = $this->generateCuid();
} }
} }
@@ -110,12 +114,7 @@ class Machine
#[ORM\PreUpdate] #[ORM\PreUpdate]
public function setUpdatedAtValue(): void public function setUpdatedAtValue(): void
{ {
$this->updatedAt = new \DateTimeImmutable(); $this->updatedAt = new DateTimeImmutable();
}
private function generateCuid(): string
{
return 'cl' . bin2hex(random_bytes(12));
} }
public function getId(): ?string public function getId(): ?string
@@ -238,13 +237,18 @@ class Machine
return $this->customFieldValues; return $this->customFieldValues;
} }
public function getCreatedAt(): \DateTimeImmutable public function getCreatedAt(): DateTimeImmutable
{ {
return $this->createdAt; return $this->createdAt;
} }
public function getUpdatedAt(): \DateTimeImmutable public function getUpdatedAt(): DateTimeImmutable
{ {
return $this->updatedAt; return $this->updatedAt;
} }
private function generateCuid(): string
{
return 'cl'.bin2hex(random_bytes(12));
}
} }

View File

@@ -30,11 +30,11 @@ class Piece
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)] #[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['piece:read'])] #[Groups(['piece:read', 'document:list'])]
private ?string $id = null; private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)] #[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['piece:read'])] #[Groups(['piece:read', 'document:list'])]
private string $name; private string $name;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)] #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]

View File

@@ -30,11 +30,11 @@ class Product
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)] #[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['product:read'])] #[Groups(['product:read', 'document:list'])]
private ?string $id = null; private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255, unique: true)] #[ORM\Column(type: Types::STRING, length: 255, unique: true)]
#[Groups(['product:read'])] #[Groups(['product:read', 'document:list'])]
private string $name; private string $name;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)] #[ORM\Column(type: Types::STRING, length: 255, nullable: true)]

View File

@@ -16,6 +16,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types; use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: SiteRepository::class)] #[ORM\Entity(repositoryClass: SiteRepository::class)]
@@ -36,10 +37,12 @@ class Site
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\Column(type: Types::STRING, length: 36)] #[ORM\Column(type: Types::STRING, length: 36)]
#[Groups(['document:list'])]
private ?string $id = null; private ?string $id = null;
#[ORM\Column(type: Types::STRING, length: 255)] #[ORM\Column(type: Types::STRING, length: 255)]
#[Assert\NotBlank] #[Assert\NotBlank]
#[Groups(['document:list'])]
private string $name; private string $name;
#[ORM\Column(type: Types::STRING, length: 255, options: ['default' => ''], name: 'contactName')] #[ORM\Column(type: Types::STRING, length: 255, options: ['default' => ''], name: 'contactName')]