217 lines
6.6 KiB
PHP
217 lines
6.6 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\PageRepositoryInterface;
|
|
use Magento\Cms\Model\Page;
|
|
use Magento\Cms\Model\PageFactory;
|
|
use Magento\Cms\Model\ResourceModel\Page\CollectionFactory;
|
|
|
|
class CmsPageEntity implements TranslatableEntityInterface
|
|
{
|
|
private const FIELDS = [
|
|
'title' => ['label' => 'Page Title', 'is_html' => false],
|
|
'content_heading' => ['label' => 'Content Heading', 'is_html' => false],
|
|
'content' => ['label' => 'Content', 'is_html' => true],
|
|
'meta_title' => ['label' => 'Meta Title', 'is_html' => false],
|
|
'meta_keywords' => ['label' => 'Meta Keywords', 'is_html' => false],
|
|
'meta_description' => ['label' => 'Meta Description', 'is_html' => false],
|
|
];
|
|
|
|
public function __construct(
|
|
private readonly PageRepositoryInterface $pageRepository,
|
|
private readonly PageFactory $pageFactory,
|
|
private readonly CollectionFactory $collectionFactory,
|
|
private readonly HtmlTranslationValidator $htmlValidator
|
|
) {
|
|
}
|
|
|
|
public function getType(): string
|
|
{
|
|
return 'cms_page';
|
|
}
|
|
|
|
public function getLabel(): string
|
|
{
|
|
return 'CMS Page';
|
|
}
|
|
|
|
public function getTitle(int $entityId): string
|
|
{
|
|
return (string)$this->getPage($entityId)->getTitle();
|
|
}
|
|
|
|
public function getEditUrlPath(): string
|
|
{
|
|
return 'cms/page/edit';
|
|
}
|
|
|
|
public function getEditIdParam(): string
|
|
{
|
|
return 'page_id';
|
|
}
|
|
|
|
public function getFields(int $entityId, array $targetStoreIds = []): array
|
|
{
|
|
$source = $this->getPage($entityId);
|
|
$target = $this->findTargetPage($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->getPage($entityId);
|
|
$fields = $this->filterAndValidateFields($fields);
|
|
if ($fields === []) {
|
|
throw new \RuntimeException('No translatable CMS page fields were provided.');
|
|
}
|
|
|
|
$updated = 0;
|
|
$created = 0;
|
|
foreach (array_unique(array_map('intval', $storeIds)) as $storeId) {
|
|
if ($storeId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$page = $this->findByIdentifierAndStore((string)$source->getIdentifier(), $storeId);
|
|
if ($page) {
|
|
$this->applyFields($page, $fields);
|
|
$page->setStores([$storeId]);
|
|
$this->pageRepository->save($page);
|
|
$updated++;
|
|
continue;
|
|
}
|
|
|
|
if (in_array(0, array_map('intval', (array)$source->getStores()), true)) {
|
|
throw new \RuntimeException(
|
|
'This CMS page is assigned to All Store Views. Create store-specific pages first or remove All Store Views before creating localized copies.'
|
|
);
|
|
}
|
|
|
|
$copy = $this->cloneForStore($source, $storeId);
|
|
$this->applyFields($copy, $fields);
|
|
$this->pageRepository->save($copy);
|
|
$created++;
|
|
}
|
|
|
|
if ($updated + $created === 0) {
|
|
throw new \RuntimeException('No valid target store views were provided.');
|
|
}
|
|
|
|
return [
|
|
'updated' => $updated,
|
|
'created' => $created,
|
|
'message' => __('CMS page translations applied. Updated: %1, Created: %2.', $updated, $created)->render(),
|
|
];
|
|
}
|
|
|
|
private function getPage(int $pageId): Page
|
|
{
|
|
$page = $this->pageRepository->getById($pageId);
|
|
if (!$page instanceof Page) {
|
|
throw new \RuntimeException('CMS page model type is not supported.');
|
|
}
|
|
|
|
return $page;
|
|
}
|
|
|
|
/**
|
|
* @param int[] $targetStoreIds
|
|
*/
|
|
private function findTargetPage(Page $source, array $targetStoreIds): ?Page
|
|
{
|
|
foreach ($targetStoreIds as $storeId) {
|
|
$page = $this->findByIdentifierAndStore((string)$source->getIdentifier(), (int)$storeId);
|
|
if ($page && (int)$page->getId() !== (int)$source->getId()) {
|
|
return $page;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function findByIdentifierAndStore(string $identifier, int $storeId): ?Page
|
|
{
|
|
if ($storeId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$collection = $this->collectionFactory->create();
|
|
$collection->addFieldToFilter('identifier', $identifier);
|
|
$collection->addStoreFilter($storeId, false);
|
|
$collection->setPageSize(1);
|
|
$page = $collection->getFirstItem();
|
|
|
|
return $page && $page->getId() ? $page : 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(Page $page, array $fields): void
|
|
{
|
|
foreach ($fields as $code => $value) {
|
|
$page->setData($code, $value);
|
|
}
|
|
}
|
|
|
|
private function cloneForStore(Page $source, int $storeId): Page
|
|
{
|
|
$data = $source->getData();
|
|
unset($data['page_id'], $data['store_id']);
|
|
|
|
$copy = $this->pageFactory->create();
|
|
$copy->setData($data);
|
|
$copy->setStores([$storeId]);
|
|
$copy->setData('store_id', [$storeId]);
|
|
|
|
return $copy;
|
|
}
|
|
}
|