Machine::class, 'composant' => Composant::class, 'piece' => Piece::class, 'product' => Product::class, ]; private const BASE_FIELDS = [ 'machine' => ['name', 'reference', 'prix'], 'composant' => ['name', 'reference', 'description', 'prix'], 'piece' => ['name', 'reference', 'description', 'prix'], 'product' => ['name', 'reference', 'supplierPrice'], ]; public function __construct( private readonly AuditLogRepository $auditLogs, private readonly EntityManagerInterface $em, private readonly ActorProfileResolver $actorProfileResolver, private readonly MachineRepository $machines, private readonly ComposantRepository $composants, private readonly PieceRepository $pieces, private readonly ProductRepository $products, private readonly ConstructeurRepository $constructeurs, private readonly SiteRepository $sites, private readonly ModelTypeRepository $modelTypes, private readonly CustomFieldValueRepository $customFieldValues, private readonly ProfileRepository $profiles, ) {} /** * @return array{items: list, total: int} */ public function getVersions(string $entityType, string $entityId): array { $logs = $this->auditLogs->findVersionHistory($entityType, $entityId); $actorIds = array_values(array_unique(array_filter(array_map( static fn (AuditLog $log) => $log->getActorProfileId(), $logs, )))); $actorMap = []; if ([] !== $actorIds) { $profileEntities = $this->profiles->findBy(['id' => $actorIds]); foreach ($profileEntities as $profile) { $label = trim(sprintf('%s %s', $profile->getFirstName(), $profile->getLastName())); if ('' === $label) { $label = $profile->getEmail() ?? $profile->getId(); } $actorMap[$profile->getId()] = $label; } } $items = array_map( static function (AuditLog $log) use ($actorMap) { $actorId = $log->getActorProfileId(); return [ 'version' => $log->getVersion(), 'action' => $log->getAction(), 'createdAt' => $log->getCreatedAt()->format(DateTimeInterface::ATOM), 'actor' => $actorId ? ['id' => $actorId, 'label' => $actorMap[$actorId] ?? $actorId] : null, 'diff' => $log->getDiff(), ]; }, $logs, ); return ['items' => array_values($items), 'total' => count($items)]; } /** * @return array{version: int, restoreMode: string, diff: array, warnings: list, snapshot: array} */ public function getRestorePreview(string $entityType, string $entityId, int $version): array { $entity = $this->findEntity($entityType, $entityId); if (null === $entity) { throw new InvalidArgumentException('Entité introuvable.'); } $auditLog = $this->auditLogs->findByVersion($entityType, $entityId, $version); if (null === $auditLog) { throw new InvalidArgumentException('Version introuvable.'); } $snapshot = $auditLog->getSnapshot() ?? []; $restoreMode = $this->checkSkeletonCompatibility($entityType, $entity, $snapshot); $warnings = $this->checkIntegrity($entityType, $snapshot, $restoreMode); $diff = $this->buildRestoreDiff($entityType, $entity, $snapshot, $restoreMode); return [ 'version' => $version, 'restoreMode' => $restoreMode, 'diff' => $diff, 'warnings' => $warnings, 'snapshot' => $snapshot, ]; } /** * @return array{success: true, newVersion: int, restoredFromVersion: int, restoreMode: string, warnings: list} */ public function restore(string $entityType, string $entityId, int $version): array { $entity = $this->findEntity($entityType, $entityId); if (null === $entity) { throw new InvalidArgumentException('Entité introuvable.'); } $auditLog = $this->auditLogs->findByVersion($entityType, $entityId, $version); if (null === $auditLog) { throw new InvalidArgumentException('Version introuvable.'); } $snapshot = $auditLog->getSnapshot() ?? []; $restoreMode = $this->checkSkeletonCompatibility($entityType, $entity, $snapshot); $warnings = $this->checkIntegrity($entityType, $snapshot, $restoreMode); $connection = $this->em->getConnection(); $connection->beginTransaction(); try { // Mark entity to skip audit subscriber (we create the AuditLog manually) if (method_exists($entity, 'setSkipAudit')) { $entity->setSkipAudit(true); } $this->applyRestore($entityType, $entity, $snapshot, $restoreMode); // Increment version manually (since subscriber is skipped) if (method_exists($entity, 'incrementVersion')) { $entity->incrementVersion(); } $this->em->flush(); $newVersion = method_exists($entity, 'getVersion') ? $entity->getVersion() : null; // Create the restore AuditLog manually with action = "restore" $restoreAuditLog = new AuditLog( $entityType, $entityId, 'restore', ['restoredFromVersion' => $version, 'restoreMode' => $restoreMode], $this->buildCurrentSnapshot($entityType, $entity), $this->actorProfileResolver->resolve(), $newVersion, ); $this->em->persist($restoreAuditLog); $this->em->flush(); $connection->commit(); } catch (Throwable $e) { $connection->rollBack(); throw $e; } finally { // Clear skip flag if (method_exists($entity, 'setSkipAudit')) { $entity->setSkipAudit(false); } } return [ 'success' => true, 'newVersion' => $newVersion, 'restoredFromVersion' => $version, 'restoreMode' => $restoreMode, 'warnings' => $warnings, ]; } private function findEntity(string $entityType, string $entityId): ?object { return match ($entityType) { 'machine' => $this->machines->find($entityId), 'composant' => $this->composants->find($entityId), 'piece' => $this->pieces->find($entityId), 'product' => $this->products->find($entityId), default => null, }; } private function checkSkeletonCompatibility(string $entityType, object $entity, array $snapshot): string { if ('machine' === $entityType) { return 'full'; } $currentTypeId = match ($entityType) { 'composant' => $entity->getTypeComposant()?->getId(), 'piece' => $entity->getTypePiece()?->getId(), 'product' => $entity->getTypeProduct()?->getId(), default => null, }; $typeKey = match ($entityType) { 'composant' => 'typeComposant', 'piece' => 'typePiece', 'product' => 'typeProduct', default => null, }; $snapshotTypeId = null; if ($typeKey && isset($snapshot[$typeKey])) { $snapshotTypeId = is_array($snapshot[$typeKey]) ? ($snapshot[$typeKey]['id'] ?? null) : $snapshot[$typeKey]; } return $currentTypeId === $snapshotTypeId ? 'full' : 'partial'; } private function checkIntegrity(string $entityType, array $snapshot, string $restoreMode): array { $warnings = []; // Check constructeurs (batch query) if (!empty($snapshot['constructeurIds'])) { $constructeurEntries = []; foreach ($snapshot['constructeurIds'] as $entry) { $id = is_array($entry) ? ($entry['id'] ?? null) : $entry; $name = is_array($entry) ? ($entry['name'] ?? $id) : $id; if ($id) { $constructeurEntries[$id] = $name; } } if ([] !== $constructeurEntries) { $foundIds = array_map( fn ($c) => $c->getId(), $this->constructeurs->findBy(['id' => array_keys($constructeurEntries)]), ); foreach ($constructeurEntries as $id => $name) { if (!in_array($id, $foundIds, true)) { $warnings[] = [ 'field' => 'constructeurIds', 'message' => sprintf("Le fournisseur '%s' n'existe plus. Il ne sera pas restauré.", $name), 'missingEntityId' => $id, 'missingEntityName' => $name, ]; } } } } // Machine: check site if ('machine' === $entityType && !empty($snapshot['site'])) { $siteId = is_array($snapshot['site']) ? ($snapshot['site']['id'] ?? null) : $snapshot['site']; if ($siteId && null === $this->sites->find($siteId)) { $siteName = is_array($snapshot['site']) ? ($snapshot['site']['name'] ?? $siteId) : $siteId; $warnings[] = [ 'field' => 'site', 'message' => sprintf("Le site '%s' n'existe plus. Le site actuel sera conservé.", $siteName), 'missingEntityId' => $siteId, 'missingEntityName' => $siteName, ]; } } if ('partial' === $restoreMode) { return $warnings; } // Machine: check link references if ('machine' === $entityType) { $linkChecks = [ ['key' => 'componentLinks', 'refKey' => 'composantId', 'nameKey' => 'composantName', 'label' => 'composant', 'repo' => $this->composants], ['key' => 'pieceLinks', 'refKey' => 'pieceId', 'nameKey' => 'pieceName', 'label' => 'pièce', 'repo' => $this->pieces], ['key' => 'productLinks', 'refKey' => 'productId', 'nameKey' => 'productName', 'label' => 'produit', 'repo' => $this->products], ]; foreach ($linkChecks as $check) { $links = $snapshot[$check['key']] ?? []; $refIds = []; foreach ($links as $link) { $refId = $link[$check['refKey']] ?? null; if (null !== $refId) { $refIds[$refId] = $link[$check['nameKey']] ?? $refId; } } if ([] === $refIds) { continue; } $foundIds = array_map(fn ($e) => $e->getId(), $check['repo']->findBy(['id' => array_keys($refIds)])); foreach ($refIds as $id => $name) { if (!in_array($id, $foundIds, true)) { $warnings[] = [ 'field' => $check['key'], 'message' => sprintf("Le %s '%s' n'existe plus. Le lien ne sera pas restauré.", $check['label'], $name), 'missingEntityId' => $id, 'missingEntityName' => $name, ]; } } } } // Full mode: check slot references (batch queries per slot type) $slotChecks = match ($entityType) { 'composant' => [ ['key' => 'pieceSlots', 'refKey' => 'selectedPieceId', 'label' => 'pièce', 'repo' => $this->pieces], ['key' => 'subcomponentSlots', 'refKey' => 'selectedComposantId', 'label' => 'sous-composant', 'repo' => $this->composants], ['key' => 'productSlots', 'refKey' => 'selectedProductId', 'label' => 'produit', 'repo' => $this->products], ], 'piece' => [ ['key' => 'productSlots', 'refKey' => 'selectedProductId', 'label' => 'produit', 'repo' => $this->products], ], default => [], }; foreach ($slotChecks as $check) { $slots = $snapshot[$check['key']] ?? []; // Collect all referenced IDs for batch lookup $refIds = []; foreach ($slots as $i => $slot) { $refId = $slot[$check['refKey']] ?? null; if (null !== $refId) { $refIds[$i] = $refId; } } if ([] === $refIds) { continue; } $foundEntities = $check['repo']->findBy(['id' => array_values(array_unique($refIds))]); $foundIds = array_map(fn ($e) => $e->getId(), $foundEntities); foreach ($refIds as $i => $refId) { if (!in_array($refId, $foundIds, true)) { $warnings[] = [ 'field' => sprintf('%s[%d].%s', $check['key'], $i, $check['refKey']), 'message' => sprintf("Le %s sélectionné dans le slot n'existe plus. Le slot sera restauré vide.", $check['label']), 'missingEntityId' => $refId, 'missingEntityName' => null, ]; } } } return $warnings; } private function buildRestoreDiff(string $entityType, object $entity, array $snapshot, string $restoreMode): array { $diff = []; $baseFields = self::BASE_FIELDS[$entityType] ?? []; foreach ($baseFields as $field) { $getter = 'get'.ucfirst($field); if (!method_exists($entity, $getter)) { continue; } $current = $entity->{$getter}(); $restored = $snapshot[$field] ?? null; if ((string) ($current ?? '') !== (string) ($restored ?? '')) { $diff[$field] = ['current' => $current, 'restored' => $restored]; } } // Constructeurs $currentConstructeurIds = []; if (method_exists($entity, 'getConstructeurLinks')) { foreach ($entity->getConstructeurLinks() as $link) { $currentConstructeurIds[] = $link->getConstructeur()->getId(); } } $snapshotConstructeurIds = []; foreach ($snapshot['constructeurIds'] ?? [] as $entry) { $snapshotConstructeurIds[] = is_array($entry) ? ($entry['id'] ?? null) : $entry; } sort($currentConstructeurIds); sort($snapshotConstructeurIds); if ($currentConstructeurIds !== $snapshotConstructeurIds) { $diff['constructeurIds'] = ['current' => $currentConstructeurIds, 'restored' => $snapshotConstructeurIds]; } if ('full' === $restoreMode) { // We signal slot restore as a single diff entry $slotKeys = match ($entityType) { 'composant' => ['pieceSlots', 'subcomponentSlots', 'productSlots'], 'piece' => ['productSlots'], default => [], }; foreach ($slotKeys as $key) { if (!empty($snapshot[$key])) { $diff[$key] = ['current' => '(structure actuelle)', 'restored' => sprintf('%d slot(s)', count($snapshot[$key]))]; } } // Machine: link diffs if ('machine' === $entityType) { $linkTypes = [ 'componentLinks' => ['idKey' => 'composantId', 'nameKey' => 'composantName', 'getter' => 'getComponentLinks', 'entityGetter' => 'getComposant'], 'pieceLinks' => ['idKey' => 'pieceId', 'nameKey' => 'pieceName', 'getter' => 'getPieceLinks', 'entityGetter' => 'getPiece'], 'productLinks' => ['idKey' => 'productId', 'nameKey' => 'productName', 'getter' => 'getProductLinks', 'entityGetter' => 'getProduct'], ]; foreach ($linkTypes as $key => $config) { $currentIds = []; $currentNames = []; if (method_exists($entity, $config['getter'])) { foreach ($entity->{$config['getter']}() as $link) { $linked = $link->{$config['entityGetter']}(); $currentIds[] = $linked->getId(); $currentNames[$linked->getId()] = $linked->getName(); } } $snapshotIds = []; $snapshotNames = []; foreach ($snapshot[$key] ?? [] as $entry) { $id = $entry[$config['idKey']] ?? null; if ($id) { $snapshotIds[] = $id; $snapshotNames[$id] = $entry[$config['nameKey']] ?? $id; } } sort($currentIds); sort($snapshotIds); if ($currentIds !== $snapshotIds) { $currentDisplay = array_map(fn ($id) => $currentNames[$id] ?? $id, $currentIds); $restoredDisplay = array_map(fn ($id) => $snapshotNames[$id] ?? $id, $snapshotIds); $diff[$key] = [ 'current' => [] !== $currentDisplay ? implode(', ', $currentDisplay) : '(aucun)', 'restored' => [] !== $restoredDisplay ? implode(', ', $restoredDisplay) : '(aucun)', ]; } } } // Custom field values diff $snapshotCfvs = $snapshot['customFieldValues'] ?? []; if ([] !== $snapshotCfvs && method_exists($entity, 'getCustomFieldValues')) { $currentCfvsByFieldId = []; foreach ($entity->getCustomFieldValues() as $cfv) { $fieldId = $cfv->getCustomField()?->getId(); if (null !== $fieldId) { $currentCfvsByFieldId[$fieldId] = [ 'fieldName' => $cfv->getCustomField()->getName(), 'value' => $cfv->getValue(), ]; } } foreach ($snapshotCfvs as $cfvData) { $fieldId = $cfvData['fieldId'] ?? null; $fieldName = $cfvData['fieldName'] ?? $fieldId; if (!$fieldId) { continue; } $currentValue = $currentCfvsByFieldId[$fieldId]['value'] ?? null; $restoredValue = $cfvData['value'] ?? null; if ((string) ($currentValue ?? '') !== (string) ($restoredValue ?? '')) { $diff['customField:'.$fieldName] = ['current' => $currentValue, 'restored' => $restoredValue]; } } } } return $diff; } private function applyRestore(string $entityType, object $entity, array $snapshot, string $restoreMode): void { // Restore base fields $baseFields = self::BASE_FIELDS[$entityType] ?? []; foreach ($baseFields as $field) { $setter = 'set'.ucfirst($field); if (method_exists($entity, $setter) && array_key_exists($field, $snapshot)) { $entity->{$setter}($snapshot[$field]); } } // Restore constructeurs $this->restoreConstructeurs($entity, $snapshot); // Machine: restore site if ('machine' === $entityType && isset($snapshot['site'])) { $siteId = is_array($snapshot['site']) ? ($snapshot['site']['id'] ?? null) : $snapshot['site']; if ($siteId) { $site = $this->sites->find($siteId); if (null !== $site) { $entity->setSite($site); } } } if ('partial' === $restoreMode) { return; } // Full mode: restore slots match ($entityType) { 'composant' => $this->restoreComposantSlots($entity, $snapshot), 'piece' => $this->restorePieceSlots($entity, $snapshot), default => null, }; // Machine: restore links if ('machine' === $entityType) { $this->restoreMachineLinks($entity, $snapshot); } // Full mode: restore custom field values $this->restoreCustomFieldValues($entityType, $entity, $snapshot); } private function restoreConstructeurs(object $entity, array $snapshot): void { if (!method_exists($entity, 'getConstructeurLinks')) { return; } $targetIds = []; $targetRefs = []; foreach ($snapshot['constructeurIds'] ?? [] as $entry) { $id = is_array($entry) ? ($entry['id'] ?? null) : $entry; if ($id) { $targetIds[] = $id; $targetRefs[$id] = is_array($entry) ? ($entry['supplierReference'] ?? null) : null; } } // Remove current links not in snapshot foreach ($entity->getConstructeurLinks()->toArray() as $link) { $cId = $link->getConstructeur()->getId(); if (!in_array($cId, $targetIds, true)) { $this->em->remove($link); } else { // Update supplierReference if present in snapshot $link->setSupplierReference($targetRefs[$cId] ?? null); } } // Add missing constructeur links from snapshot $currentIds = array_map( fn ($link) => $link->getConstructeur()->getId(), $entity->getConstructeurLinks()->toArray(), ); foreach ($targetIds as $id) { if (!in_array($id, $currentIds, true)) { $constructeur = $this->constructeurs->find($id); if (null !== $constructeur) { $link = $this->createConstructeurLink($entity); $link->setConstructeur($constructeur); $link->setSupplierReference($targetRefs[$id] ?? null); $this->em->persist($link); } } } } private function createConstructeurLink(object $entity): object { if ($entity instanceof Machine) { $link = new MachineConstructeurLink(); $link->setMachine($entity); return $link; } if ($entity instanceof Piece) { $link = new PieceConstructeurLink(); $link->setPiece($entity); return $link; } if ($entity instanceof Composant) { $link = new ComposantConstructeurLink(); $link->setComposant($entity); return $link; } if ($entity instanceof Product) { $link = new ProductConstructeurLink(); $link->setProduct($entity); return $link; } throw new LogicException('Unsupported entity type for constructeur link'); } private function restoreMachineLinks(Machine $machine, array $snapshot): void { // Remove all existing links foreach ($machine->getProductLinks()->toArray() as $link) { $this->em->remove($link); } foreach ($machine->getPieceLinks()->toArray() as $link) { $this->em->remove($link); } foreach ($machine->getComponentLinks()->toArray() as $link) { $this->em->remove($link); } // Flush removals to avoid FK conflicts $this->em->flush(); // Recreate component links foreach ($snapshot['componentLinks'] ?? [] as $data) { $composant = !empty($data['composantId']) ? $this->composants->find($data['composantId']) : null; if (null === $composant) { continue; } $link = new MachineComponentLink(); $link->setMachine($machine); $link->setComposant($composant); $this->em->persist($link); } // Recreate piece links foreach ($snapshot['pieceLinks'] ?? [] as $data) { $piece = !empty($data['pieceId']) ? $this->pieces->find($data['pieceId']) : null; if (null === $piece) { continue; } $link = new MachinePieceLink(); $link->setMachine($machine); $link->setPiece($piece); if (isset($data['quantity']) && $data['quantity'] > 0) { $link->setQuantity((int) $data['quantity']); } $this->em->persist($link); } // Recreate product links foreach ($snapshot['productLinks'] ?? [] as $data) { $product = !empty($data['productId']) ? $this->products->find($data['productId']) : null; if (null === $product) { continue; } $link = new MachineProductLink(); $link->setMachine($machine); $link->setProduct($product); $this->em->persist($link); } } private function restoreComposantSlots(Composant $entity, array $snapshot): void { // Clear existing slots foreach ($entity->getPieceSlots()->toArray() as $slot) { $this->em->remove($slot); } foreach ($entity->getSubcomponentSlots()->toArray() as $slot) { $this->em->remove($slot); } foreach ($entity->getProductSlots()->toArray() as $slot) { $this->em->remove($slot); } // Flush removals first to avoid FK constraint conflicts with new slots $this->em->flush(); // Recreate piece slots foreach ($snapshot['pieceSlots'] ?? [] as $slotData) { $slot = new ComposantPieceSlot(); $slot->setComposant($entity); $slot->setQuantity($slotData['quantity'] ?? 1); $slot->setPosition($slotData['position'] ?? 0); if (!empty($slotData['typePieceId'])) { $slot->setTypePiece($this->modelTypes->find($slotData['typePieceId'])); } if (!empty($slotData['selectedPieceId'])) { $piece = $this->pieces->find($slotData['selectedPieceId']); if (null !== $piece) { $slot->setSelectedPiece($piece); } } $this->em->persist($slot); } // Recreate subcomponent slots foreach ($snapshot['subcomponentSlots'] ?? [] as $slotData) { $slot = new ComposantSubcomponentSlot(); $slot->setComposant($entity); $slot->setAlias($slotData['alias'] ?? null); $slot->setFamilyCode($slotData['familyCode'] ?? null); $slot->setPosition($slotData['position'] ?? 0); if (!empty($slotData['typeComposantId'])) { $slot->setTypeComposant($this->modelTypes->find($slotData['typeComposantId'])); } if (!empty($slotData['selectedComposantId'])) { $composant = $this->composants->find($slotData['selectedComposantId']); if (null !== $composant) { $slot->setSelectedComposant($composant); } } $this->em->persist($slot); } // Recreate product slots foreach ($snapshot['productSlots'] ?? [] as $slotData) { $slot = new ComposantProductSlot(); $slot->setComposant($entity); $slot->setFamilyCode($slotData['familyCode'] ?? null); $slot->setPosition($slotData['position'] ?? 0); if (!empty($slotData['typeProductId'])) { $slot->setTypeProduct($this->modelTypes->find($slotData['typeProductId'])); } if (!empty($slotData['selectedProductId'])) { $product = $this->products->find($slotData['selectedProductId']); if (null !== $product) { $slot->setSelectedProduct($product); } } $this->em->persist($slot); } } private function restorePieceSlots(Piece $entity, array $snapshot): void { foreach ($entity->getProductSlots()->toArray() as $slot) { $this->em->remove($slot); } // Flush removals first to avoid FK constraint conflicts $this->em->flush(); foreach ($snapshot['productSlots'] ?? [] as $slotData) { $slot = new PieceProductSlot(); $slot->setPiece($entity); $slot->setFamilyCode($slotData['familyCode'] ?? null); $slot->setPosition($slotData['position'] ?? 0); if (!empty($slotData['typeProductId'])) { $slot->setTypeProduct($this->modelTypes->find($slotData['typeProductId'])); } if (!empty($slotData['selectedProductId'])) { $product = $this->products->find($slotData['selectedProductId']); if (null !== $product) { $slot->setSelectedProduct($product); } } $this->em->persist($slot); } } private function restoreCustomFieldValues(string $entityType, object $entity, array $snapshot): void { $snapshotCfvs = $snapshot['customFieldValues'] ?? []; if ([] === $snapshotCfvs) { return; } // Build a map of current CFVs by fieldId for lookup $currentCfvsByFieldId = []; if (method_exists($entity, 'getCustomFieldValues')) { foreach ($entity->getCustomFieldValues() as $cfv) { $fieldId = $cfv->getCustomField()?->getId(); if (null !== $fieldId) { $currentCfvsByFieldId[$fieldId] = $cfv; } } } foreach ($snapshotCfvs as $cfvData) { $fieldId = $cfvData['fieldId'] ?? null; if (!$fieldId) { continue; } // Try to find the current CFV by fieldId (resilient to ID changes after sync) $cfv = $currentCfvsByFieldId[$fieldId] ?? null; // Fallback: try by original ID if fieldId lookup failed if (null === $cfv && !empty($cfvData['id'])) { $cfv = $this->customFieldValues->find($cfvData['id']); } if (null !== $cfv) { $cfv->setValue($cfvData['value'] ?? null); } } } /** * Build a complete snapshot of the entity in its current state (after restore). * Must be consistent with the snapshots built by the audit subscribers, * so that a future restore from a "restore" version works correctly. */ private function buildCurrentSnapshot(string $entityType, object $entity): array { $snapshot = ['id' => $entity->getId()]; // Base fields $baseFields = self::BASE_FIELDS[$entityType] ?? []; foreach ($baseFields as $field) { $getter = 'get'.ucfirst($field); if (method_exists($entity, $getter)) { $snapshot[$field] = $entity->{$getter}(); } } // Version if (method_exists($entity, 'getVersion')) { $snapshot['version'] = $entity->getVersion(); } // Constructeur links if (method_exists($entity, 'getConstructeurLinks')) { $snapshot['constructeurLinks'] = []; foreach ($entity->getConstructeurLinks() as $link) { $snapshot['constructeurLinks'][] = [ 'id' => $link->getId(), 'constructeurId' => $link->getConstructeur()->getId(), 'constructeurName' => $link->getConstructeur()->getName(), 'supplierReference' => $link->getSupplierReference(), ]; } } // Type (ModelType reference) $typeGetters = ['composant' => 'getTypeComposant', 'piece' => 'getTypePiece', 'product' => 'getTypeProduct']; $typeKeys = ['composant' => 'typeComposant', 'piece' => 'typePiece', 'product' => 'typeProduct']; if (isset($typeGetters[$entityType]) && method_exists($entity, $typeGetters[$entityType])) { $type = $entity->{$typeGetters[$entityType]}(); $snapshot[$typeKeys[$entityType]] = $type ? ['id' => $type->getId(), 'name' => $type->getName(), 'code' => $type->getCode()] : null; } // Machine: site if ('machine' === $entityType && method_exists($entity, 'getSite')) { $site = $entity->getSite(); $snapshot['site'] = $site ? ['id' => $site->getId(), 'name' => $site->getName()] : null; } // Machine: links if ('machine' === $entityType) { $snapshot['componentLinks'] = []; foreach ($entity->getComponentLinks() as $link) { $snapshot['componentLinks'][] = [ 'id' => $link->getId(), 'composantId' => $link->getComposant()?->getId(), 'composantName' => $link->getComposant()?->getName(), 'modelTypeId' => $link->getModelType()?->getId(), ]; } $snapshot['pieceLinks'] = []; foreach ($entity->getPieceLinks() as $link) { $snapshot['pieceLinks'][] = [ 'id' => $link->getId(), 'pieceId' => $link->getPiece()?->getId(), 'pieceName' => $link->getPiece()?->getName(), 'quantity' => $link->getQuantity(), 'modelTypeId' => $link->getModelType()?->getId(), ]; } $snapshot['productLinks'] = []; foreach ($entity->getProductLinks() as $link) { $snapshot['productLinks'][] = [ 'id' => $link->getId(), 'productId' => $link->getProduct()?->getId(), 'productName' => $link->getProduct()?->getName(), 'modelTypeId' => $link->getModelType()?->getId(), ]; } } // Composant/Piece: product if (in_array($entityType, ['composant', 'piece'], true) && method_exists($entity, 'getProduct')) { $product = $entity->getProduct(); $snapshot['product'] = $product ? ['id' => $product->getId(), 'name' => $product->getName()] : null; } // Slots if ('composant' === $entityType) { $snapshot['pieceSlots'] = []; foreach ($entity->getPieceSlots() as $slot) { $snapshot['pieceSlots'][] = [ 'id' => $slot->getId(), 'typePieceId' => $slot->getTypePiece()?->getId(), 'selectedPieceId' => $slot->getSelectedPiece()?->getId(), 'quantity' => $slot->getQuantity(), 'position' => $slot->getPosition(), ]; } $snapshot['subcomponentSlots'] = []; foreach ($entity->getSubcomponentSlots() as $slot) { $snapshot['subcomponentSlots'][] = [ 'id' => $slot->getId(), 'alias' => $slot->getAlias(), 'familyCode' => $slot->getFamilyCode(), 'typeComposantId' => $slot->getTypeComposant()?->getId(), 'selectedComposantId' => $slot->getSelectedComposant()?->getId(), 'position' => $slot->getPosition(), ]; } $snapshot['productSlots'] = $this->serializeProductSlots($entity->getProductSlots()); } if ('piece' === $entityType) { $snapshot['productSlots'] = $this->serializeProductSlots($entity->getProductSlots()); } // Custom field values if (method_exists($entity, 'getCustomFieldValues')) { $snapshot['customFieldValues'] = []; foreach ($entity->getCustomFieldValues() as $cfv) { $snapshot['customFieldValues'][] = [ 'id' => $cfv->getId(), 'fieldName' => $cfv->getCustomField()?->getName(), 'fieldId' => $cfv->getCustomField()?->getId(), 'value' => $cfv->getValue(), ]; } } return $snapshot; } /** * @param iterable $slots * * @return list> */ private function serializeProductSlots(iterable $slots): array { $serialized = []; foreach ($slots as $slot) { $serialized[] = [ 'id' => $slot->getId(), 'typeProductId' => $slot->getTypeProduct()?->getId(), 'selectedProductId' => $slot->getSelectedProduct()?->getId(), 'familyCode' => $slot->getFamilyCode(), 'position' => $slot->getPosition(), ]; } return $serialized; } }