feat(reference-auto) : add automatic reference generation for pieces

ModelType defines a formula with placeholders ({serie}{diametre}{type}).
ReferenceAutoGenerator resolves it from CustomFieldValues with trim+uppercase normalisation.
ReferenceAutoSubscriber (onFlush) recalculates on Piece/CFV insert/update/delete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-26 17:58:53 +01:00
parent d568961eb3
commit 3f6ce153bb
7 changed files with 532 additions and 0 deletions

View File

@@ -73,6 +73,14 @@ class ModelType
#[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
private ?string $description = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
#[Groups(['model_type:read', 'model_type:write'])]
private ?string $referenceFormula = null;
#[ORM\Column(type: Types::JSON, nullable: true)]
#[Groups(['model_type:read', 'model_type:write'])]
private ?array $requiredFieldsForReference = null;
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
#[Groups(['model_type:read'])]
private DateTimeImmutable $createdAt;
@@ -215,6 +223,30 @@ class ModelType
return $this;
}
public function getReferenceFormula(): ?string
{
return $this->referenceFormula;
}
public function setReferenceFormula(?string $referenceFormula): static
{
$this->referenceFormula = $referenceFormula;
return $this;
}
public function getRequiredFieldsForReference(): ?array
{
return $this->requiredFieldsForReference;
}
public function setRequiredFieldsForReference(?array $requiredFieldsForReference): static
{
$this->requiredFieldsForReference = $requiredFieldsForReference;
return $this;
}
#[Groups(['model_type:read', 'product:read', 'composant:read', 'piece:read'])]
public function getStructure(): ?array
{

View File

@@ -63,6 +63,10 @@ class Piece
#[Groups(['piece:read'])]
private ?string $reference = null;
#[ORM\Column(type: Types::STRING, length: 255, nullable: true)]
#[Groups(['piece:read'])]
private ?string $referenceAuto = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
#[Groups(['piece:read'])]
private ?string $description = null;
@@ -179,6 +183,21 @@ class Piece
return $this;
}
public function getReferenceAuto(): ?string
{
return $this->referenceAuto;
}
/**
* @internal used by ReferenceAutoSubscriber only — not part of the public API
*/
public function setReferenceAuto(?string $referenceAuto): static
{
$this->referenceAuto = $referenceAuto;
return $this;
}
public function getDescription(): ?string
{
return $this->description;

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace App\EventSubscriber;
use App\Entity\CustomFieldValue;
use App\Entity\Piece;
use App\Service\ReferenceAutoGenerator;
use Doctrine\Bundle\DoctrineBundle\Attribute\AsDoctrineListener;
use Doctrine\ORM\Event\OnFlushEventArgs;
use Doctrine\ORM\Events;
#[AsDoctrineListener(event: Events::onFlush)]
final class ReferenceAutoSubscriber
{
public function __construct(private readonly ReferenceAutoGenerator $generator) {}
public function onFlush(OnFlushEventArgs $args): void
{
$em = $args->getObjectManager();
$uow = $em->getUnitOfWork();
$piecesToRecalculate = [];
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if ($entity instanceof Piece) {
$piecesToRecalculate[$entity->getId()] = $entity;
}
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof Piece) {
$piecesToRecalculate[$entity->getId()] = $entity;
}
}
// For CFV insertions: the new CFV is not yet in the DB, so Piece's lazy-loaded
// collection won't contain it. We must add it manually so the generator sees it.
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if ($entity instanceof CustomFieldValue && $entity->getPiece()) {
$piece = $entity->getPiece();
if (!$piece->getCustomFieldValues()->contains($entity)) {
$piece->getCustomFieldValues()->add($entity);
}
$piecesToRecalculate[$piece->getId()] = $piece;
}
}
foreach ($uow->getScheduledEntityUpdates() as $entity) {
if ($entity instanceof CustomFieldValue && $entity->getPiece()) {
$piece = $entity->getPiece();
$piecesToRecalculate[$piece->getId()] = $piece;
}
}
// For CFV deletions: remove from collection so the generator doesn't see stale values.
foreach ($uow->getScheduledEntityDeletions() as $entity) {
if ($entity instanceof CustomFieldValue && $entity->getPiece()) {
$piece = $entity->getPiece();
$piece->getCustomFieldValues()->removeElement($entity);
$piecesToRecalculate[$piece->getId()] = $piece;
}
}
$meta = $em->getClassMetadata(Piece::class);
foreach ($piecesToRecalculate as $piece) {
$newRef = $this->generator->generate($piece);
if ($piece->getReferenceAuto() !== $newRef) {
$piece->setReferenceAuto($newRef);
$uow->recomputeSingleEntityChangeSet($meta, $piece);
}
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\CustomFieldValue;
use App\Entity\Piece;
class ReferenceAutoGenerator
{
public function generate(Piece $piece): ?string
{
$modelType = $piece->getTypePiece();
if (!$modelType || !$modelType->getReferenceFormula()) {
return null;
}
$valueMap = $this->buildValueMap($piece);
$requiredFields = $modelType->getRequiredFieldsForReference();
if ($requiredFields) {
foreach ($requiredFields as $fieldName) {
if (!isset($valueMap[$fieldName]) || '' === $valueMap[$fieldName]) {
return null;
}
}
}
return preg_replace_callback('/\{(\w+)\}/', static function (array $matches) use ($valueMap): string {
return $valueMap[$matches[1]] ?? '';
}, $modelType->getReferenceFormula());
}
/**
* Build a map of fieldName → normalized value from the Piece's CustomFieldValues.
*
* @return array<string, string>
*/
private function buildValueMap(Piece $piece): array
{
$map = [];
/** @var CustomFieldValue $cfv */
foreach ($piece->getCustomFieldValues() as $cfv) {
$normalized = mb_strtoupper(trim($cfv->getValue()));
$map[$cfv->getCustomField()->getName()] = $normalized;
}
return $map;
}
}