Files
Inventory/src/Service/Sync/ProductSyncStrategy.php
Matthieu b2aff0e414 feat(sync) : add slot selection controllers, custom field sync, and position fallbacks
- 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>
2026-03-13 16:40:44 +01:00

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],
);
}
}