389 lines
15 KiB
PHP
389 lines
15 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Service\Sync;
|
|
|
|
use App\DTO\SyncConfirmation;
|
|
use App\DTO\SyncExecutionResult;
|
|
use App\DTO\SyncPreviewResult;
|
|
use App\Entity\Composant;
|
|
use App\Entity\ComposantPieceSlot;
|
|
use App\Entity\ComposantProductSlot;
|
|
use App\Entity\ComposantSubcomponentSlot;
|
|
use App\Entity\CustomField;
|
|
use App\Entity\CustomFieldValue;
|
|
use App\Entity\ModelType;
|
|
use App\Entity\SkeletonPieceRequirement;
|
|
use App\Entity\SkeletonProductRequirement;
|
|
use App\Entity\SkeletonSubcomponentRequirement;
|
|
use App\Enum\ModelCategory;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
|
|
|
#[AutoconfigureTag('app.sync_strategy')]
|
|
class ComposantSyncStrategy implements SyncStrategyInterface
|
|
{
|
|
public function __construct(
|
|
private readonly EntityManagerInterface $em,
|
|
) {}
|
|
|
|
public function supports(ModelType $modelType): bool
|
|
{
|
|
return ModelCategory::COMPONENT === $modelType->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
|
|
$proposedPieceKeys = [];
|
|
foreach ($proposedPieces as $pp) {
|
|
$proposedPieceKeys[$pp['typePieceId'].'|'.$pp['position']] = true;
|
|
}
|
|
|
|
$proposedProductKeys = [];
|
|
foreach ($proposedProducts as $pp) {
|
|
$proposedProductKeys[$pp['typeProductId'].'|'.$pp['position']] = true;
|
|
}
|
|
|
|
$proposedSubKeys = [];
|
|
foreach ($proposedSubcomponents as $ps) {
|
|
$proposedSubKeys[$ps['typeComposantId'].'|'.$ps['position']] = true;
|
|
}
|
|
|
|
// Map proposed custom fields by orderIndex
|
|
$proposedCfByOrder = [];
|
|
foreach ($proposedCustomFields as $pcf) {
|
|
$proposedCfByOrder[$pcf['orderIndex']] = $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,
|
|
],
|
|
);
|
|
}
|
|
}
|