Files
2026-09-15 10:35:56 +08:00

213 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
namespace Esun\Translation\Model\Entity;
use Esun\Translation\Api\TranslatableEntityInterface;
use Esun\Translation\Model\Html\HtmlTranslationValidator;
use Magento\Cms\Api\BlockRepositoryInterface;
use Magento\Cms\Model\Block;
use Magento\Cms\Model\BlockFactory;
use Magento\Cms\Model\ResourceModel\Block\CollectionFactory;
class CmsBlockEntity implements TranslatableEntityInterface
{
private const FIELDS = [
'title' => ['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<string, mixed> $fields
* @return array<string, string>
*/
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<string, string> $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;
}
}