getCategory(); } public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult { $composants = $this->em->getRepository(Composant::class)->findBy(['typeComposant' => $modelType]); $proposedPieces = $newStructure['pieces'] ?? []; $proposedProducts = $newStructure['products'] ?? []; $proposedSubcomponents = $newStructure['subcomponents'] ?? []; $proposedCustomFields = $newStructure['customFields'] ?? []; $addedPieceSlots = 0; $deletedPieceSlots = 0; $addedProductSlots = 0; $deletedProductSlots = 0; $addedSubSlots = 0; $deletedSubSlots = 0; $addedCfValues = 0; $deletedCfValues = 0; // Map proposed by (typeId, position) keys — position defaults to array index $proposedPieceKeys = []; foreach ($proposedPieces as $i => $pp) { $pos = $pp['position'] ?? $i; $proposedPieceKeys[$pp['typePieceId'].'|'.$pos] = true; } $proposedProductKeys = []; foreach ($proposedProducts as $i => $pp) { $pos = $pp['position'] ?? $i; $proposedProductKeys[$pp['typeProductId'].'|'.$pos] = true; } $proposedSubKeys = []; foreach ($proposedSubcomponents as $i => $ps) { $pos = $ps['position'] ?? $i; $proposedSubKeys[$ps['typeComposantId'].'|'.$pos] = true; } // Map proposed custom fields by orderIndex (falls back to array index) $proposedCfByOrder = []; foreach ($proposedCustomFields as $i => $pcf) { $order = $pcf['orderIndex'] ?? $i; $proposedCfByOrder[$order] = $pcf; } // Get existing custom fields for this model type $existingFields = $this->em->getRepository(CustomField::class)->findBy( ['typeComposant' => $modelType], ['orderIndex' => 'ASC'] ); $existingCfByOrder = []; foreach ($existingFields as $field) { $existingCfByOrder[$field->getOrderIndex()] = $field; } // Count custom field additions/deletions (definition-level, affects all composants) $cfAdded = 0; $cfDeleted = 0; foreach ($proposedCfByOrder as $orderIndex => $pcf) { if (!isset($existingCfByOrder[$orderIndex])) { ++$cfAdded; } } foreach ($existingCfByOrder as $orderIndex => $ef) { if (!isset($proposedCfByOrder[$orderIndex])) { ++$cfDeleted; } } foreach ($composants as $composant) { // Piece slots — query from repository to avoid stale collection $pieceSlots = $this->em->getRepository(ComposantPieceSlot::class)->findBy(['composant' => $composant]); $existingPieceKeys = []; foreach ($pieceSlots as $slot) { $key = ($slot->getTypePiece()?->getId() ?? '').'|'.$slot->getPosition(); $existingPieceKeys[$key] = true; } foreach ($proposedPieceKeys as $key => $_) { if (!isset($existingPieceKeys[$key])) { ++$addedPieceSlots; } } foreach ($existingPieceKeys as $key => $_) { if (!isset($proposedPieceKeys[$key])) { ++$deletedPieceSlots; } } // Product slots $productSlots = $this->em->getRepository(ComposantProductSlot::class)->findBy(['composant' => $composant]); $existingProductKeys = []; foreach ($productSlots as $slot) { $key = ($slot->getTypeProduct()?->getId() ?? '').'|'.$slot->getPosition(); $existingProductKeys[$key] = true; } foreach ($proposedProductKeys as $key => $_) { if (!isset($existingProductKeys[$key])) { ++$addedProductSlots; } } foreach ($existingProductKeys as $key => $_) { if (!isset($proposedProductKeys[$key])) { ++$deletedProductSlots; } } // Subcomponent slots $subSlots = $this->em->getRepository(ComposantSubcomponentSlot::class)->findBy(['composant' => $composant]); $existingSubKeys = []; foreach ($subSlots as $slot) { $key = ($slot->getTypeComposant()?->getId() ?? '').'|'.$slot->getPosition(); $existingSubKeys[$key] = true; } foreach ($proposedSubKeys as $key => $_) { if (!isset($existingSubKeys[$key])) { ++$addedSubSlots; } } foreach ($existingSubKeys as $key => $_) { if (!isset($proposedSubKeys[$key])) { ++$deletedSubSlots; } } // Custom field values $addedCfValues += $cfAdded; $deletedCfValues += $cfDeleted; } $itemCount = count($composants); return new SyncPreviewResult( modelTypeId: $modelType->getId(), category: 'component', itemCount: $itemCount, additions: [ 'pieceSlots' => $addedPieceSlots, 'productSlots' => $addedProductSlots, 'subcomponentSlots' => $addedSubSlots, 'customFieldValues' => $addedCfValues, ], deletions: [ 'pieceSlots' => $deletedPieceSlots, 'productSlots' => $deletedProductSlots, 'subcomponentSlots' => $deletedSubSlots, 'customFieldValues' => $deletedCfValues, ], ); } public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult { $composants = $this->em->getRepository(Composant::class)->findBy(['typeComposant' => $modelType]); // Load skeleton requirements $pieceReqs = $this->em->getRepository(SkeletonPieceRequirement::class)->findBy(['modelType' => $modelType]); $productReqs = $this->em->getRepository(SkeletonProductRequirement::class)->findBy(['modelType' => $modelType]); $subReqs = $this->em->getRepository(SkeletonSubcomponentRequirement::class)->findBy(['modelType' => $modelType]); $customFields = $this->em->getRepository(CustomField::class)->findBy( ['typeComposant' => $modelType], ['orderIndex' => 'ASC'] ); // Map requirements by (typeId, position) $pieceReqKeys = []; foreach ($pieceReqs as $req) { $pieceReqKeys[$req->getTypePiece()->getId().'|'.$req->getPosition()] = $req; } $productReqKeys = []; foreach ($productReqs as $req) { $productReqKeys[$req->getTypeProduct()->getId().'|'.$req->getPosition()] = $req; } $subReqKeys = []; foreach ($subReqs as $req) { $key = ($req->getTypeComposant()?->getId() ?? '').'|'.$req->getPosition(); $subReqKeys[$key] = $req; } $addedPieceSlots = 0; $deletedPieceSlots = 0; $addedProductSlots = 0; $deletedProductSlots = 0; $addedSubSlots = 0; $deletedSubSlots = 0; $addedCfValues = 0; $deletedCfValues = 0; $itemsUpdated = 0; foreach ($composants as $composant) { $changed = false; // --- Piece slots — query from repository to avoid stale collection --- $pieceSlotEntities = $this->em->getRepository(ComposantPieceSlot::class)->findBy(['composant' => $composant]); $existingPieceSlots = []; foreach ($pieceSlotEntities as $slot) { $key = ($slot->getTypePiece()?->getId() ?? '').'|'.$slot->getPosition(); $existingPieceSlots[$key] = $slot; } // Add missing piece slots foreach ($pieceReqKeys as $key => $req) { if (!isset($existingPieceSlots[$key])) { $slot = new ComposantPieceSlot(); $slot->setComposant($composant); $slot->setTypePiece($req->getTypePiece()); $slot->setPosition($req->getPosition()); // Default quantity = 1, selectedPiece = null (already defaults) $this->em->persist($slot); ++$addedPieceSlots; $changed = true; } } // Delete orphaned piece slots if ($confirmation->confirmDeletions) { foreach ($existingPieceSlots as $key => $slot) { if (!isset($pieceReqKeys[$key])) { $composant->removePieceSlot($slot); $this->em->remove($slot); ++$deletedPieceSlots; $changed = true; } } } // --- Product slots --- $productSlotEntities = $this->em->getRepository(ComposantProductSlot::class)->findBy(['composant' => $composant]); $existingProductSlots = []; foreach ($productSlotEntities as $slot) { $key = ($slot->getTypeProduct()?->getId() ?? '').'|'.$slot->getPosition(); $existingProductSlots[$key] = $slot; } // Add missing product slots foreach ($productReqKeys as $key => $req) { if (!isset($existingProductSlots[$key])) { $slot = new ComposantProductSlot(); $slot->setComposant($composant); $slot->setTypeProduct($req->getTypeProduct()); $slot->setPosition($req->getPosition()); if (null !== $req->getFamilyCode()) { $slot->setFamilyCode($req->getFamilyCode()); } $this->em->persist($slot); ++$addedProductSlots; $changed = true; } } // Delete orphaned product slots if ($confirmation->confirmDeletions) { foreach ($existingProductSlots as $key => $slot) { if (!isset($productReqKeys[$key])) { $composant->removeProductSlot($slot); $this->em->remove($slot); ++$deletedProductSlots; $changed = true; } } } // --- Subcomponent slots --- $subSlotEntities = $this->em->getRepository(ComposantSubcomponentSlot::class)->findBy(['composant' => $composant]); $existingSubSlots = []; foreach ($subSlotEntities as $slot) { $key = ($slot->getTypeComposant()?->getId() ?? '').'|'.$slot->getPosition(); $existingSubSlots[$key] = $slot; } // Add missing subcomponent slots foreach ($subReqKeys as $key => $req) { if (!isset($existingSubSlots[$key])) { $slot = new ComposantSubcomponentSlot(); $slot->setComposant($composant); $slot->setTypeComposant($req->getTypeComposant()); $slot->setPosition($req->getPosition()); $slot->setAlias($req->getAlias()); $slot->setFamilyCode($req->getFamilyCode()); $this->em->persist($slot); ++$addedSubSlots; $changed = true; } } // Delete orphaned subcomponent slots if ($confirmation->confirmDeletions) { foreach ($existingSubSlots as $key => $slot) { if (!isset($subReqKeys[$key])) { $composant->removeSubcomponentSlot($slot); $this->em->remove($slot); ++$deletedSubSlots; $changed = true; } } } // --- Custom field values --- $existingValues = $this->em->getRepository(CustomFieldValue::class)->findBy([ 'composant' => $composant, ]); $existingByFieldId = []; foreach ($existingValues as $cfv) { $existingByFieldId[$cfv->getCustomField()->getId()] = $cfv; } // Add missing custom field values foreach ($customFields as $cf) { if (!isset($existingByFieldId[$cf->getId()])) { $cfv = new CustomFieldValue(); $cfv->setCustomField($cf); $cfv->setComposant($composant); $cfv->setValue(''); $this->em->persist($cfv); ++$addedCfValues; $changed = true; } } // Delete orphaned custom field values if ($confirmation->confirmDeletions) { $fieldIds = array_map(fn (CustomField $cf) => $cf->getId(), $customFields); foreach ($existingValues as $cfv) { if (!in_array($cfv->getCustomField()->getId(), $fieldIds, true)) { $this->em->remove($cfv); ++$deletedCfValues; $changed = true; } } } if ($changed) { $composant->incrementVersion(); ++$itemsUpdated; } } $this->em->flush(); return new SyncExecutionResult( itemsUpdated: $itemsUpdated, additions: [ 'pieceSlots' => $addedPieceSlots, 'productSlots' => $addedProductSlots, 'subcomponentSlots' => $addedSubSlots, 'customFieldValues' => $addedCfValues, ], deletions: [ 'pieceSlots' => $deletedPieceSlots, 'productSlots' => $deletedProductSlots, 'subcomponentSlots' => $deletedSubSlots, 'customFieldValues' => $deletedCfValues, ], ); } }