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'])]
|
||||
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])]
|
||||
#[Groups(['composant:read'])]
|
||||
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
|
||||
{
|
||||
if (!$this->productSlots->contains($productSlot)) {
|
||||
|
||||
@@ -12,15 +12,14 @@ use App\Entity\ComposantPieceSlot;
|
||||
use App\Entity\ComposantProductSlot;
|
||||
use App\Entity\ComposantSubcomponentSlot;
|
||||
use App\Entity\ModelType;
|
||||
use App\Entity\Piece;
|
||||
use App\Entity\Product;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
/**
|
||||
* Initializes skeleton slots when a Composant is created with a typeComposant.
|
||||
*
|
||||
* Reads the SkeletonPieceRequirements, SkeletonProductRequirements and
|
||||
* SkeletonSubcomponentRequirements from the ModelType and creates matching
|
||||
* ComposantPieceSlot / ComposantProductSlot / ComposantSubcomponentSlot rows.
|
||||
* Initializes skeleton slots when a Composant is created with a typeComposant,
|
||||
* and applies the piece/product/subcomponent selections from the frontend payload.
|
||||
*/
|
||||
final class ComposantProcessor implements ProcessorInterface
|
||||
{
|
||||
@@ -40,20 +39,21 @@ final class ComposantProcessor implements ProcessorInterface
|
||||
return $this->decorated->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
|
||||
$isCreation = $operation instanceof Post;
|
||||
$pendingStructure = $data->getPendingStructure();
|
||||
|
||||
// Persist the entity first
|
||||
$result = $this->decorated->process($data, $operation, $uriVariables, $context);
|
||||
|
||||
// On creation, scaffold slots from the ModelType skeleton
|
||||
if ($isCreation) {
|
||||
$this->initializeSlotsFromSkeleton($data);
|
||||
// On creation, scaffold slots from the ModelType skeleton + apply selections
|
||||
if ($operation instanceof Post) {
|
||||
$this->initializeSlotsFromSkeleton($data, $pendingStructure);
|
||||
$data->clearPendingStructure();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function initializeSlotsFromSkeleton(Composant $composant): void
|
||||
private function initializeSlotsFromSkeleton(Composant $composant, ?array $structure): void
|
||||
{
|
||||
$modelType = $composant->getTypeComposant();
|
||||
|
||||
@@ -61,44 +61,69 @@ final class ComposantProcessor implements ProcessorInterface
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the ModelType's skeleton collections are loaded
|
||||
$modelType = $this->entityManager->getRepository(ModelType::class)->find($modelType->getId());
|
||||
|
||||
if (!$modelType) {
|
||||
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;
|
||||
|
||||
// Piece slots
|
||||
foreach ($modelType->getSkeletonPieceRequirements() as $req) {
|
||||
$slot = new ComposantPieceSlot();
|
||||
$typePieceId = $req->getTypePiece()->getId();
|
||||
$slot = new ComposantPieceSlot();
|
||||
$slot->setComposant($composant);
|
||||
$slot->setTypePiece($req->getTypePiece());
|
||||
$slot->setPosition($req->getPosition());
|
||||
|
||||
$selectedId = $this->shiftSelection($pieceSelections, $typePieceId);
|
||||
if ($selectedId) {
|
||||
$slot->setSelectedPiece($this->entityManager->getReference(Piece::class, $selectedId));
|
||||
}
|
||||
|
||||
$this->entityManager->persist($slot);
|
||||
$hasNewSlots = true;
|
||||
}
|
||||
|
||||
// Product slots
|
||||
foreach ($modelType->getSkeletonProductRequirements() as $req) {
|
||||
$slot = new ComposantProductSlot();
|
||||
$typeProductId = $req->getTypeProduct()->getId();
|
||||
$slot = new ComposantProductSlot();
|
||||
$slot->setComposant($composant);
|
||||
$slot->setTypeProduct($req->getTypeProduct());
|
||||
$slot->setFamilyCode($req->getFamilyCode());
|
||||
$slot->setPosition($req->getPosition());
|
||||
|
||||
$selectedId = $this->shiftSelection($productSelections, $typeProductId);
|
||||
if ($selectedId) {
|
||||
$slot->setSelectedProduct($this->entityManager->getReference(Product::class, $selectedId));
|
||||
}
|
||||
|
||||
$this->entityManager->persist($slot);
|
||||
$hasNewSlots = true;
|
||||
}
|
||||
|
||||
// Subcomponent slots
|
||||
foreach ($modelType->getSkeletonSubcomponentRequirements() as $req) {
|
||||
$slot = new ComposantSubcomponentSlot();
|
||||
$typeComposantId = $req->getTypeComposant()?->getId() ?? '';
|
||||
$slot = new ComposantSubcomponentSlot();
|
||||
$slot->setComposant($composant);
|
||||
$slot->setAlias($req->getAlias());
|
||||
$slot->setFamilyCode($req->getFamilyCode());
|
||||
$slot->setTypeComposant($req->getTypeComposant());
|
||||
$slot->setPosition($req->getPosition());
|
||||
|
||||
$selectedId = $this->shiftSelection($subSelections, $typeComposantId);
|
||||
if ($selectedId) {
|
||||
$slot->setSelectedComposant($this->entityManager->getReference(Composant::class, $selectedId));
|
||||
}
|
||||
|
||||
$this->entityManager->persist($slot);
|
||||
$hasNewSlots = true;
|
||||
}
|
||||
@@ -107,4 +132,49 @@ final class ComposantProcessor implements ProcessorInterface
|
||||
$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