['label' => 'Block Title', 'is_html' => false], 'content' => ['label' => 'Content', 'is_html' => true], ]; public function __construct( private readonly BlockRepositoryInterface $blockRepository, private readonly BlockFactory $blockFactory, private readonly CollectionFactory $collectionFactory, private readonly HtmlTranslationValidator $htmlValidator ) { } public function getType(): string { return 'cms_block'; } public function getLabel(): string { return 'CMS Block'; } public function getTitle(int $entityId): string { return (string)$this->getBlock($entityId)->getTitle(); } public function getEditUrlPath(): string { return 'cms/block/edit'; } public function getEditIdParam(): string { return 'block_id'; } public function getFields(int $entityId, array $targetStoreIds = []): array { $source = $this->getBlock($entityId); $target = $this->findTargetBlock($source, $targetStoreIds); $fields = []; foreach (self::FIELDS as $code => $metadata) { $fields[] = [ 'code' => $code, 'label' => $metadata['label'], 'source_value' => (string)($source->getData($code) ?? ''), 'target_value' => $target ? (string)($target->getData($code) ?? '') : '', 'is_html' => (bool)$metadata['is_html'], ]; } return $fields; } public function apply(int $entityId, array $storeIds, array $fields): array { $source = $this->getBlock($entityId); $fields = $this->filterAndValidateFields($fields); if ($fields === []) { throw new \RuntimeException('No translatable CMS block fields were provided.'); } $updated = 0; $created = 0; foreach (array_unique(array_map('intval', $storeIds)) as $storeId) { if ($storeId <= 0) { continue; } $block = $this->findByIdentifierAndStore((string)$source->getIdentifier(), $storeId); if ($block) { $this->applyFields($block, $fields); $block->setStores([$storeId]); $this->blockRepository->save($block); $updated++; continue; } if (in_array(0, array_map('intval', (array)$source->getStores()), true)) { throw new \RuntimeException( 'This CMS block is assigned to All Store Views. Create store-specific blocks first or remove All Store Views before creating localized copies.' ); } $copy = $this->cloneForStore($source, $storeId); $this->applyFields($copy, $fields); $this->blockRepository->save($copy); $created++; } if ($updated + $created === 0) { throw new \RuntimeException('No valid target store views were provided.'); } return [ 'updated' => $updated, 'created' => $created, 'message' => __('CMS block translations applied. Updated: %1, Created: %2.', $updated, $created)->render(), ]; } private function getBlock(int $blockId): Block { $block = $this->blockRepository->getById($blockId); if (!$block instanceof Block) { throw new \RuntimeException('CMS block model type is not supported.'); } return $block; } /** * @param int[] $targetStoreIds */ private function findTargetBlock(Block $source, array $targetStoreIds): ?Block { foreach ($targetStoreIds as $storeId) { $block = $this->findByIdentifierAndStore((string)$source->getIdentifier(), (int)$storeId); if ($block && (int)$block->getId() !== (int)$source->getId()) { return $block; } } return null; } private function findByIdentifierAndStore(string $identifier, int $storeId): ?Block { if ($storeId <= 0) { return null; } $collection = $this->collectionFactory->create(); $collection->addFieldToFilter('identifier', $identifier); $collection->addStoreFilter($storeId, false); $collection->setPageSize(1); $block = $collection->getFirstItem(); return $block && $block->getId() ? $block : null; } /** * @param array $fields * @return array */ private function filterAndValidateFields(array $fields): array { $result = []; foreach ($fields as $code => $value) { $code = (string)$code; if (!isset(self::FIELDS[$code])) { continue; } $value = (string)$value; if ((bool)self::FIELDS[$code]['is_html']) { $errors = $this->htmlValidator->validate($value); if ($errors !== []) { throw new \RuntimeException(sprintf( 'HTML for "%s" is invalid: %s', $code, implode('; ', $errors) )); } } $result[$code] = $value; } return $result; } /** * @param array $fields */ private function applyFields(Block $block, array $fields): void { foreach ($fields as $code => $value) { $block->setData($code, $value); } } private function cloneForStore(Block $source, int $storeId): Block { $data = $source->getData(); unset($data['block_id'], $data['store_id']); $copy = $this->blockFactory->create(); $copy->setData($data); $copy->setStores([$storeId]); $copy->setData('store_id', [$storeId]); return $copy; } }