fix(composant) : persist piece/product/subcomponent selections on creation
The ComposantProcessor now reads the structure payload from the frontend and applies selectedPieceId/selectedProductId/selectedComponentId to the scaffolded slots, so user selections are actually saved to the database. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,12 @@ class Composant
|
|||||||
#[ORM\OrderBy(['position' => 'ASC'])]
|
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||||
private Collection $productSlots;
|
private Collection $productSlots;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transient — holds the structure payload sent by the frontend during creation.
|
||||||
|
* Not mapped to any column; consumed by ComposantProcessor.
|
||||||
|
*/
|
||||||
|
private ?array $pendingStructure = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]
|
#[ORM\Column(type: Types::INTEGER, options: ['default' => 1])]
|
||||||
#[Groups(['composant:read'])]
|
#[Groups(['composant:read'])]
|
||||||
private int $version = 1;
|
private int $version = 1;
|
||||||
@@ -395,6 +401,27 @@ class Composant
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by API Platform during denormalization — stores the frontend
|
||||||
|
* structure payload so the ComposantProcessor can apply selections.
|
||||||
|
*/
|
||||||
|
public function setStructure(?array $structure): static
|
||||||
|
{
|
||||||
|
$this->pendingStructure = $structure;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPendingStructure(): ?array
|
||||||
|
{
|
||||||
|
return $this->pendingStructure;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearPendingStructure(): void
|
||||||
|
{
|
||||||
|
$this->pendingStructure = null;
|
||||||
|
}
|
||||||
|
|
||||||
public function addProductSlot(ComposantProductSlot $productSlot): static
|
public function addProductSlot(ComposantProductSlot $productSlot): static
|
||||||
{
|
{
|
||||||
if (!$this->productSlots->contains($productSlot)) {
|
if (!$this->productSlots->contains($productSlot)) {
|
||||||
|
|||||||
@@ -12,15 +12,14 @@ use App\Entity\ComposantPieceSlot;
|
|||||||
use App\Entity\ComposantProductSlot;
|
use App\Entity\ComposantProductSlot;
|
||||||
use App\Entity\ComposantSubcomponentSlot;
|
use App\Entity\ComposantSubcomponentSlot;
|
||||||
use App\Entity\ModelType;
|
use App\Entity\ModelType;
|
||||||
|
use App\Entity\Piece;
|
||||||
|
use App\Entity\Product;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes skeleton slots when a Composant is created with a typeComposant.
|
* Initializes skeleton slots when a Composant is created with a typeComposant,
|
||||||
*
|
* and applies the piece/product/subcomponent selections from the frontend payload.
|
||||||
* Reads the SkeletonPieceRequirements, SkeletonProductRequirements and
|
|
||||||
* SkeletonSubcomponentRequirements from the ModelType and creates matching
|
|
||||||
* ComposantPieceSlot / ComposantProductSlot / ComposantSubcomponentSlot rows.
|
|
||||||
*/
|
*/
|
||||||
final class ComposantProcessor implements ProcessorInterface
|
final class ComposantProcessor implements ProcessorInterface
|
||||||
{
|
{
|
||||||
@@ -40,20 +39,21 @@ final class ComposantProcessor implements ProcessorInterface
|
|||||||
return $this->decorated->process($data, $operation, $uriVariables, $context);
|
return $this->decorated->process($data, $operation, $uriVariables, $context);
|
||||||
}
|
}
|
||||||
|
|
||||||
$isCreation = $operation instanceof Post;
|
$pendingStructure = $data->getPendingStructure();
|
||||||
|
|
||||||
// Persist the entity first
|
// Persist the entity first
|
||||||
$result = $this->decorated->process($data, $operation, $uriVariables, $context);
|
$result = $this->decorated->process($data, $operation, $uriVariables, $context);
|
||||||
|
|
||||||
// On creation, scaffold slots from the ModelType skeleton
|
// On creation, scaffold slots from the ModelType skeleton + apply selections
|
||||||
if ($isCreation) {
|
if ($operation instanceof Post) {
|
||||||
$this->initializeSlotsFromSkeleton($data);
|
$this->initializeSlotsFromSkeleton($data, $pendingStructure);
|
||||||
|
$data->clearPendingStructure();
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function initializeSlotsFromSkeleton(Composant $composant): void
|
private function initializeSlotsFromSkeleton(Composant $composant, ?array $structure): void
|
||||||
{
|
{
|
||||||
$modelType = $composant->getTypeComposant();
|
$modelType = $composant->getTypeComposant();
|
||||||
|
|
||||||
@@ -61,44 +61,69 @@ final class ComposantProcessor implements ProcessorInterface
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the ModelType's skeleton collections are loaded
|
|
||||||
$modelType = $this->entityManager->getRepository(ModelType::class)->find($modelType->getId());
|
$modelType = $this->entityManager->getRepository(ModelType::class)->find($modelType->getId());
|
||||||
|
|
||||||
if (!$modelType) {
|
if (!$modelType) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Index selections from the frontend payload by typePieceId/typeProductId/typeComposantId
|
||||||
|
$pieceSelections = $this->indexSelections($structure['pieces'] ?? [], 'typePieceId', 'selectedPieceId');
|
||||||
|
$productSelections = $this->indexSelections($structure['products'] ?? [], 'typeProductId', 'selectedProductId');
|
||||||
|
$subSelections = $this->indexSelections($structure['subcomponents'] ?? [], 'typeComposantId', 'selectedComponentId');
|
||||||
|
|
||||||
$hasNewSlots = false;
|
$hasNewSlots = false;
|
||||||
|
|
||||||
// Piece slots
|
// Piece slots
|
||||||
foreach ($modelType->getSkeletonPieceRequirements() as $req) {
|
foreach ($modelType->getSkeletonPieceRequirements() as $req) {
|
||||||
$slot = new ComposantPieceSlot();
|
$typePieceId = $req->getTypePiece()->getId();
|
||||||
|
$slot = new ComposantPieceSlot();
|
||||||
$slot->setComposant($composant);
|
$slot->setComposant($composant);
|
||||||
$slot->setTypePiece($req->getTypePiece());
|
$slot->setTypePiece($req->getTypePiece());
|
||||||
$slot->setPosition($req->getPosition());
|
$slot->setPosition($req->getPosition());
|
||||||
|
|
||||||
|
$selectedId = $this->shiftSelection($pieceSelections, $typePieceId);
|
||||||
|
if ($selectedId) {
|
||||||
|
$slot->setSelectedPiece($this->entityManager->getReference(Piece::class, $selectedId));
|
||||||
|
}
|
||||||
|
|
||||||
$this->entityManager->persist($slot);
|
$this->entityManager->persist($slot);
|
||||||
$hasNewSlots = true;
|
$hasNewSlots = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Product slots
|
// Product slots
|
||||||
foreach ($modelType->getSkeletonProductRequirements() as $req) {
|
foreach ($modelType->getSkeletonProductRequirements() as $req) {
|
||||||
$slot = new ComposantProductSlot();
|
$typeProductId = $req->getTypeProduct()->getId();
|
||||||
|
$slot = new ComposantProductSlot();
|
||||||
$slot->setComposant($composant);
|
$slot->setComposant($composant);
|
||||||
$slot->setTypeProduct($req->getTypeProduct());
|
$slot->setTypeProduct($req->getTypeProduct());
|
||||||
$slot->setFamilyCode($req->getFamilyCode());
|
$slot->setFamilyCode($req->getFamilyCode());
|
||||||
$slot->setPosition($req->getPosition());
|
$slot->setPosition($req->getPosition());
|
||||||
|
|
||||||
|
$selectedId = $this->shiftSelection($productSelections, $typeProductId);
|
||||||
|
if ($selectedId) {
|
||||||
|
$slot->setSelectedProduct($this->entityManager->getReference(Product::class, $selectedId));
|
||||||
|
}
|
||||||
|
|
||||||
$this->entityManager->persist($slot);
|
$this->entityManager->persist($slot);
|
||||||
$hasNewSlots = true;
|
$hasNewSlots = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subcomponent slots
|
// Subcomponent slots
|
||||||
foreach ($modelType->getSkeletonSubcomponentRequirements() as $req) {
|
foreach ($modelType->getSkeletonSubcomponentRequirements() as $req) {
|
||||||
$slot = new ComposantSubcomponentSlot();
|
$typeComposantId = $req->getTypeComposant()?->getId() ?? '';
|
||||||
|
$slot = new ComposantSubcomponentSlot();
|
||||||
$slot->setComposant($composant);
|
$slot->setComposant($composant);
|
||||||
$slot->setAlias($req->getAlias());
|
$slot->setAlias($req->getAlias());
|
||||||
$slot->setFamilyCode($req->getFamilyCode());
|
$slot->setFamilyCode($req->getFamilyCode());
|
||||||
$slot->setTypeComposant($req->getTypeComposant());
|
$slot->setTypeComposant($req->getTypeComposant());
|
||||||
$slot->setPosition($req->getPosition());
|
$slot->setPosition($req->getPosition());
|
||||||
|
|
||||||
|
$selectedId = $this->shiftSelection($subSelections, $typeComposantId);
|
||||||
|
if ($selectedId) {
|
||||||
|
$slot->setSelectedComposant($this->entityManager->getReference(Composant::class, $selectedId));
|
||||||
|
}
|
||||||
|
|
||||||
$this->entityManager->persist($slot);
|
$this->entityManager->persist($slot);
|
||||||
$hasNewSlots = true;
|
$hasNewSlots = true;
|
||||||
}
|
}
|
||||||
@@ -107,4 +132,49 @@ final class ComposantProcessor implements ProcessorInterface
|
|||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an indexed map of typeId → [selectedId, selectedId, ...] from the payload.
|
||||||
|
* Supports both flat format and nested definition format from the frontend.
|
||||||
|
*
|
||||||
|
* @return array<string, list<string>>
|
||||||
|
*/
|
||||||
|
private function indexSelections(array $items, string $typeKey, string $selectionKey): array
|
||||||
|
{
|
||||||
|
$map = [];
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The typeId can be at top level or inside definition
|
||||||
|
$typeId = $item['definition'][$typeKey]
|
||||||
|
?? $item[$typeKey]
|
||||||
|
?? null;
|
||||||
|
$selectedId = $item[$selectionKey] ?? null;
|
||||||
|
|
||||||
|
if (!is_string($typeId) || '' === $typeId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($selectedId) && '' !== $selectedId) {
|
||||||
|
$map[$typeId][] = $selectedId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pop the first selection for a given typeId (handles multiple slots of the same type).
|
||||||
|
*/
|
||||||
|
private function shiftSelection(array &$selections, string $typeId): ?string
|
||||||
|
{
|
||||||
|
if (empty($selections[$typeId])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_shift($selections[$typeId]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user