- Add selectedPieceId support to ComposantPieceSlotController - Create ComposantProductSlotController and ComposantSubcomponentSlotController - Add updateCustomFields() to SkeletonStructureService for managing CustomField entities - Fix position/orderIndex fallback to array index in all 3 sync strategies - Fix type comparison in ProductSyncStrategy for dual format support - Update CLAUDE.md with new entities, controllers, and fixtures documentation - Update frontend submodule with interactive slot selectors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
5.3 KiB
PHP
154 lines
5.3 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\CustomField;
|
|
use App\Entity\CustomFieldValue;
|
|
use App\Entity\ModelType;
|
|
use App\Entity\Product;
|
|
use App\Enum\ModelCategory;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
|
|
|
|
#[AutoconfigureTag('app.sync_strategy')]
|
|
class ProductSyncStrategy implements SyncStrategyInterface
|
|
{
|
|
public function __construct(
|
|
private readonly EntityManagerInterface $em,
|
|
) {}
|
|
|
|
public function supports(ModelType $modelType): bool
|
|
{
|
|
return ModelCategory::PRODUCT === $modelType->getCategory();
|
|
}
|
|
|
|
public function preview(ModelType $modelType, array $newStructure): SyncPreviewResult
|
|
{
|
|
$products = $this->em->getRepository(Product::class)->findBy(['typeProduct' => $modelType]);
|
|
$existingFields = $this->em->getRepository(CustomField::class)->findBy(
|
|
['typeProduct' => $modelType],
|
|
['orderIndex' => 'ASC']
|
|
);
|
|
|
|
$proposedFields = $newStructure['customFields'] ?? [];
|
|
|
|
// Map existing fields by orderIndex
|
|
$existingByOrder = [];
|
|
foreach ($existingFields as $field) {
|
|
$existingByOrder[$field->getOrderIndex()] = $field;
|
|
}
|
|
|
|
// Map proposed fields by orderIndex (falls back to array index)
|
|
$proposedByOrder = [];
|
|
foreach ($proposedFields as $i => $pf) {
|
|
$order = $pf['orderIndex'] ?? $i;
|
|
$proposedByOrder[$order] = $pf;
|
|
}
|
|
|
|
$addedFields = 0;
|
|
$deletedFields = 0;
|
|
$modifiedFields = 0;
|
|
|
|
// New fields (in proposed but not in existing)
|
|
foreach ($proposedByOrder as $orderIndex => $pf) {
|
|
if (!isset($existingByOrder[$orderIndex])) {
|
|
++$addedFields;
|
|
} elseif ($existingByOrder[$orderIndex]->getType() !== ($pf['type'] ?? $pf['value']['type'] ?? null)) {
|
|
++$modifiedFields;
|
|
}
|
|
}
|
|
|
|
// Deleted fields (in existing but not in proposed)
|
|
foreach ($existingByOrder as $orderIndex => $ef) {
|
|
if (!isset($proposedByOrder[$orderIndex])) {
|
|
++$deletedFields;
|
|
}
|
|
}
|
|
|
|
$itemCount = count($products);
|
|
|
|
return new SyncPreviewResult(
|
|
modelTypeId: $modelType->getId(),
|
|
category: 'product',
|
|
itemCount: $itemCount,
|
|
additions: ['customFieldValues' => $addedFields * $itemCount],
|
|
deletions: ['customFieldValues' => $deletedFields * $itemCount],
|
|
modifications: ['customFieldValues' => $modifiedFields * $itemCount],
|
|
);
|
|
}
|
|
|
|
public function execute(ModelType $modelType, SyncConfirmation $confirmation): SyncExecutionResult
|
|
{
|
|
$products = $this->em->getRepository(Product::class)->findBy(['typeProduct' => $modelType]);
|
|
$customFields = $this->em->getRepository(CustomField::class)->findBy(
|
|
['typeProduct' => $modelType],
|
|
['orderIndex' => 'ASC']
|
|
);
|
|
|
|
$addedValues = 0;
|
|
$deletedValues = 0;
|
|
$modifiedValues = 0;
|
|
$itemsUpdated = 0;
|
|
|
|
foreach ($products as $product) {
|
|
$changed = false;
|
|
|
|
// Get existing custom field values for this product
|
|
$existingValues = $this->em->getRepository(CustomFieldValue::class)->findBy([
|
|
'product' => $product,
|
|
]);
|
|
|
|
// Map existing values by custom field ID
|
|
$existingByFieldId = [];
|
|
foreach ($existingValues as $cfv) {
|
|
$existingByFieldId[$cfv->getCustomField()->getId()] = $cfv;
|
|
}
|
|
|
|
// For each custom field defined on the model type, ensure a value exists
|
|
foreach ($customFields as $cf) {
|
|
if (!isset($existingByFieldId[$cf->getId()])) {
|
|
// Create missing custom field value
|
|
$cfv = new CustomFieldValue();
|
|
$cfv->setCustomField($cf);
|
|
$cfv->setProduct($product);
|
|
$cfv->setValue('');
|
|
$this->em->persist($cfv);
|
|
++$addedValues;
|
|
$changed = true;
|
|
}
|
|
}
|
|
|
|
// Delete orphaned values if confirmDeletions
|
|
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);
|
|
++$deletedValues;
|
|
$changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($changed) {
|
|
$product->incrementVersion();
|
|
++$itemsUpdated;
|
|
}
|
|
}
|
|
|
|
$this->em->flush();
|
|
|
|
return new SyncExecutionResult(
|
|
itemsUpdated: $itemsUpdated,
|
|
additions: ['customFieldValues' => $addedValues],
|
|
deletions: ['customFieldValues' => $deletedValues],
|
|
modifications: ['customFieldValues' => $modifiedValues],
|
|
);
|
|
}
|
|
}
|