308 lines
12 KiB
PHP
308 lines
12 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Esun\BlogImport\Model;
|
|
|
|
use Magento\Framework\App\Cache\TypeListInterface;
|
|
use Magento\Framework\App\Filesystem\DirectoryList;
|
|
use Magento\Framework\App\ResourceConnection;
|
|
use Magento\Framework\Exception\LocalizedException;
|
|
use Magento\Framework\Filter\TranslitUrl;
|
|
use Mageplaza\Blog\Model\Post;
|
|
use Mageplaza\Blog\Model\PostFactory;
|
|
use Mageplaza\Blog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory;
|
|
use Mageplaza\Blog\Model\ResourceModel\Post\CollectionFactory as PostCollectionFactory;
|
|
use Mageplaza\Blog\Model\ResourceModel\Post as PostResource;
|
|
|
|
class BlogImporter
|
|
{
|
|
private const MEDIUMTEXT_MAX_BYTES = 16777215;
|
|
private const MAGEPLAZA_POST_MEDIA_PREFIX = 'mageplaza/blog/post/';
|
|
|
|
public function __construct(
|
|
private readonly SourceReader $sourceReader,
|
|
private readonly HtmlProcessor $htmlProcessor,
|
|
private readonly MediaNormalizer $mediaNormalizer,
|
|
private readonly DirectoryList $directoryList,
|
|
private readonly TranslitUrl $translitUrl,
|
|
private readonly CategoryCollectionFactory $categoryCollectionFactory,
|
|
private readonly PostCollectionFactory $postCollectionFactory,
|
|
private readonly PostFactory $postFactory,
|
|
private readonly PostResource $postResource,
|
|
private readonly ResourceConnection $resourceConnection,
|
|
private readonly TypeListInterface $cacheTypeList
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @param callable(string): void|null $progress
|
|
* @return array<string, int>
|
|
* @throws LocalizedException
|
|
*/
|
|
public function import(
|
|
string $sourceDirectory,
|
|
int $storeId,
|
|
string $mediaSubdirectory,
|
|
bool $dryRun,
|
|
bool $allowMissingMedia,
|
|
?callable $progress = null
|
|
): array {
|
|
$mediaSubdirectory = trim($mediaSubdirectory, '/');
|
|
if ($mediaSubdirectory === '' || str_contains($mediaSubdirectory, '..')) {
|
|
throw new LocalizedException(__('The media subdirectory is invalid.'));
|
|
}
|
|
|
|
$source = $this->sourceReader->read($sourceDirectory);
|
|
$categoryIds = $this->resolveCategoryIds();
|
|
$prepared = [];
|
|
$missingMedia = [];
|
|
$mediaToNormalize = [];
|
|
$mediaRoot = $this->directoryList->getPath(DirectoryList::MEDIA)
|
|
. DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $mediaSubdirectory);
|
|
|
|
foreach ($source['records'] as $record) {
|
|
$urlKey = $this->translitUrl->filter(rawurldecode((string)$record['source_url_key']));
|
|
if ($urlKey === '') {
|
|
throw new LocalizedException(__('URL key is empty after normalization: %1', $record['source_url']));
|
|
}
|
|
if (isset($prepared[$urlKey]) && $prepared[$urlKey]['source_url'] !== $record['source_url']) {
|
|
throw new LocalizedException(__('Two source URLs normalize to the same URL key: %1', $urlKey));
|
|
}
|
|
|
|
$processed = $this->htmlProcessor->process($record['html_path'], $mediaSubdirectory);
|
|
$contentBytes = strlen($processed['html']);
|
|
if ($contentBytes > self::MEDIUMTEXT_MAX_BYTES) {
|
|
throw new LocalizedException(
|
|
__(
|
|
'Processed HTML for %1 is %2 bytes and exceeds the MEDIUMTEXT limit of %3 bytes.',
|
|
$record['source_url'],
|
|
$contentBytes,
|
|
self::MEDIUMTEXT_MAX_BYTES
|
|
)
|
|
);
|
|
}
|
|
foreach ($processed['media'] as $relativePath) {
|
|
$mediaPath = $mediaRoot . DIRECTORY_SEPARATOR
|
|
. str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
|
|
if (!is_file($mediaPath) || !is_readable($mediaPath)) {
|
|
$missingMedia[$relativePath] = $relativePath;
|
|
} elseif ($this->mediaNormalizer->needsNormalization($mediaPath)) {
|
|
$mediaToNormalize[$mediaPath] = $mediaPath;
|
|
}
|
|
}
|
|
if ($record['image_filename'] !== '') {
|
|
$coverPath = $mediaRoot . DIRECTORY_SEPARATOR . $record['image_filename'];
|
|
if (!is_file($coverPath) || !is_readable($coverPath)) {
|
|
$missingMedia[$record['image_filename']] = $record['image_filename'];
|
|
} elseif ($this->mediaNormalizer->needsNormalization($coverPath)) {
|
|
$mediaToNormalize[$coverPath] = $coverPath;
|
|
}
|
|
}
|
|
$record['url_key'] = $urlKey;
|
|
$record['post_content'] = $processed['html'];
|
|
$record['image'] = $this->buildPostImagePath($mediaSubdirectory, $record['image_filename']);
|
|
if (strlen($record['image']) > 255) {
|
|
throw new LocalizedException(__('Post image path exceeds 255 bytes for %1.', $record['source_url']));
|
|
}
|
|
$record['category_ids'] = array_values(array_map(
|
|
static fn(string $category): int => $categoryIds[$category],
|
|
array_values($record['categories'])
|
|
));
|
|
$prepared[$urlKey] = $record;
|
|
}
|
|
|
|
if ($missingMedia !== [] && !$allowMissingMedia) {
|
|
$sample = implode(', ', array_slice(array_values($missingMedia), 0, 10));
|
|
throw new LocalizedException(
|
|
__('Found %1 missing media file(s). First entries: %2', count($missingMedia), $sample)
|
|
);
|
|
}
|
|
|
|
$stats = [
|
|
'json_records' => (int)$source['json_records'],
|
|
'unique_posts' => count($prepared),
|
|
'duplicate_records' => (int)$source['duplicate_records'],
|
|
'missing_media' => count($missingMedia),
|
|
'normalized_media' => count($mediaToNormalize),
|
|
'created' => 0,
|
|
'updated' => 0,
|
|
'unchanged' => 0,
|
|
'failed' => 0,
|
|
];
|
|
if (!$dryRun) {
|
|
foreach ($mediaToNormalize as $mediaPath) {
|
|
$this->mediaNormalizer->normalize($mediaPath);
|
|
}
|
|
}
|
|
$resolvedPosts = [];
|
|
foreach ($prepared as $urlKey => $record) {
|
|
$resolvedPosts[$urlKey] = $this->loadPost($record);
|
|
}
|
|
if ($dryRun) {
|
|
return $stats;
|
|
}
|
|
|
|
foreach ($prepared as $urlKey => $record) {
|
|
$connection = $this->resourceConnection->getConnection();
|
|
$connection->beginTransaction();
|
|
try {
|
|
[$post, $state] = $resolvedPosts[$urlKey];
|
|
$changed = $this->populatePost($post, $record, $storeId);
|
|
if ($changed || $state === 'created') {
|
|
$this->postResource->save($post);
|
|
if ((string)$post->getUrlKey() !== $record['url_key']) {
|
|
throw new LocalizedException(
|
|
__('Mageplaza changed URL key %1 to %2.', $record['url_key'], $post->getUrlKey())
|
|
);
|
|
}
|
|
$stats[$state]++;
|
|
if ($progress !== null) {
|
|
$progress(sprintf('%s %s', ucfirst($state), $record['url_key']));
|
|
}
|
|
} else {
|
|
$stats['unchanged']++;
|
|
}
|
|
$connection->commit();
|
|
} catch (\Throwable $exception) {
|
|
$connection->rollBack();
|
|
$stats['failed']++;
|
|
throw new LocalizedException(
|
|
__('Failed importing %1: %2', $record['source_url'], $exception->getMessage()),
|
|
$exception
|
|
);
|
|
}
|
|
}
|
|
|
|
$this->cacheTypeList->invalidate('block_html');
|
|
$this->cacheTypeList->invalidate('full_page');
|
|
|
|
return $stats;
|
|
}
|
|
|
|
/** @return array<string, int> */
|
|
private function resolveCategoryIds(): array
|
|
{
|
|
$result = [];
|
|
foreach (['industry', 'activities', 'exhibition', 'news'] as $category) {
|
|
$collection = $this->categoryCollectionFactory->create()
|
|
->addFieldToFilter('url_key', $category)
|
|
->setPageSize(2);
|
|
if ((int)$collection->getSize() === 0) {
|
|
$collection = $this->categoryCollectionFactory->create()
|
|
->addFieldToFilter('name', $category)
|
|
->setPageSize(2);
|
|
}
|
|
if ((int)$collection->getSize() !== 1) {
|
|
throw new LocalizedException(
|
|
__('Expected exactly one Mageplaza Blog category for "%1"; found %2.', $category, $collection->getSize())
|
|
);
|
|
}
|
|
$result[$category] = (int)$collection->getFirstItem()->getId();
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/** @return array{0: Post, 1: 'created'|'updated'} */
|
|
private function loadPost(array $record): array
|
|
{
|
|
/** @var Post $post */
|
|
$post = $this->postCollectionFactory->create()
|
|
->addFieldToFilter('import_source', $record['source_url'])
|
|
->setPageSize(1)
|
|
->getFirstItem();
|
|
if ($post->getId()) {
|
|
$collision = $this->postCollectionFactory->create()
|
|
->addFieldToFilter('url_key', $record['url_key'])
|
|
->setPageSize(1)
|
|
->getFirstItem();
|
|
if ($collision->getId() && (int)$collision->getId() !== (int)$post->getId()) {
|
|
throw new LocalizedException(
|
|
__('URL key "%1" is already used by post ID %2.', $record['url_key'], $collision->getId())
|
|
);
|
|
}
|
|
|
|
return [$post, 'updated'];
|
|
}
|
|
|
|
$collision = $this->postCollectionFactory->create()
|
|
->addFieldToFilter('url_key', $record['url_key'])
|
|
->setPageSize(1)
|
|
->getFirstItem();
|
|
if ($collision->getId()) {
|
|
if (!$collision->getData('import_source')
|
|
&& trim((string)$collision->getName()) === trim((string)$record['title'])
|
|
) {
|
|
return [$collision, 'updated'];
|
|
}
|
|
throw new LocalizedException(
|
|
__('URL key "%1" is already used by post ID %2 with a different import source.', $record['url_key'], $collision->getId())
|
|
);
|
|
}
|
|
|
|
return [$this->postFactory->create(), 'created'];
|
|
}
|
|
|
|
private function populatePost(Post $post, array $record, int $storeId): bool
|
|
{
|
|
$data = [
|
|
'name' => $record['title'],
|
|
'short_description' => $record['short_description'],
|
|
'post_content' => $record['post_content'],
|
|
'image' => $record['image'],
|
|
'store_ids' => [$storeId],
|
|
'enabled' => 1,
|
|
'url_key' => $record['url_key'],
|
|
'in_rss' => 0,
|
|
'layout' => '2columns-left',
|
|
'allow_comment' => 0,
|
|
'meta_title' => $record['title'],
|
|
'publish_date' => $record['publish_date'] . ' 00:00:00',
|
|
'import_source' => $record['source_url'],
|
|
'categories_ids' => $record['category_ids'],
|
|
];
|
|
if (!$post->getId()) {
|
|
$data['created_at'] = $record['publish_date'] . ' 00:00:00';
|
|
}
|
|
$data['updated_at'] = gmdate('Y-m-d H:i:s');
|
|
|
|
$changed = !$post->getId();
|
|
foreach ($data as $key => $value) {
|
|
$oldValue = $key === 'categories_ids' ? $post->getCategoryIds() : $post->getData($key);
|
|
if ($key === 'store_ids' && is_string($oldValue)) {
|
|
$oldValue = array_filter(explode(',', $oldValue), 'strlen');
|
|
}
|
|
if ($key === 'updated_at') {
|
|
continue;
|
|
}
|
|
if (is_array($value)) {
|
|
$old = array_map('strval', (array)$oldValue);
|
|
$new = array_map('strval', $value);
|
|
sort($old);
|
|
sort($new);
|
|
$changed = $changed || $old !== $new;
|
|
} else {
|
|
$changed = $changed || (string)$oldValue !== (string)$value;
|
|
}
|
|
}
|
|
|
|
if ($changed) {
|
|
$post->addData($data);
|
|
}
|
|
|
|
return $changed;
|
|
}
|
|
|
|
private function buildPostImagePath(string $mediaSubdirectory, string $imageFilename): string
|
|
{
|
|
if ($imageFilename === '') {
|
|
return '';
|
|
}
|
|
if (str_starts_with($mediaSubdirectory . '/', self::MAGEPLAZA_POST_MEDIA_PREFIX)) {
|
|
$mediaSubdirectory = substr($mediaSubdirectory, strlen(self::MAGEPLAZA_POST_MEDIA_PREFIX));
|
|
}
|
|
|
|
return trim($mediaSubdirectory, '/') . '/' . $imageFilename;
|
|
}
|
|
}
|