Files
esun3d/app/code/Esun/BlogImport/Model/SourceReader.php
T
2026-09-15 10:35:56 +08:00

243 lines
11 KiB
PHP

<?php
declare(strict_types=1);
namespace Esun\BlogImport\Model;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Serialize\Serializer\Json;
class SourceReader
{
private const SOURCE_FILES = [
'industry' => 'esun_industry_details.json',
'activities' => 'esun_activities_details.json',
'exhibition' => 'esun_exhibition_details.json',
'news' => 'esun_news_details.json',
];
private const LIST_FILES = [
'industry' => 'esun_industry_all.json',
'activities' => 'esun_activities_all.json',
'exhibition' => 'esun_exhibition_all.json',
'news' => 'esun_news_all.json',
];
public function __construct(private readonly Json $json)
{
}
/**
* @return array{records: array<string, array<string, mixed>>, json_records: int, duplicate_records: int}
* @throws LocalizedException
*/
public function read(string $sourceDirectory): array
{
$sourceRoot = realpath($sourceDirectory);
if ($sourceRoot === false || !is_dir($sourceRoot) || !is_readable($sourceRoot)) {
throw new LocalizedException(__('Import source is not a readable directory: %1', $sourceDirectory));
}
$records = [];
$jsonRecords = 0;
foreach (self::SOURCE_FILES as $category => $filename) {
$listItems = $this->readListItems($sourceRoot, $category);
$jsonPath = $sourceRoot . DIRECTORY_SEPARATOR . $filename;
if (!is_file($jsonPath) || !is_readable($jsonPath)) {
throw new LocalizedException(__('Required JSON file is missing or unreadable: %1', $jsonPath));
}
try {
$data = $this->json->unserialize((string)file_get_contents($jsonPath));
} catch (\InvalidArgumentException $exception) {
throw new LocalizedException(__('Invalid JSON in %1: %2', $filename, $exception->getMessage()));
}
if (!is_array($data) || !isset($data['details']) || !is_array($data['details'])) {
throw new LocalizedException(__('The details array is missing from %1.', $filename));
}
if (isset($data['total_count']) && (int)$data['total_count'] !== count($data['details'])) {
throw new LocalizedException(
__('The total_count in %1 does not match its details count.', $filename)
);
}
foreach ($data['details'] as $index => $item) {
$jsonRecords++;
if (!is_array($item)) {
throw new LocalizedException(__('Invalid item %1 in %2.', $index + 1, $filename));
}
$this->validateItem($item, $index, $filename);
$sourceUrl = trim((string)$item['sourceUrl']);
if (!isset($listItems[$sourceUrl])) {
throw new LocalizedException(
__('No matching list item was found for %1 in %2.', $sourceUrl, self::LIST_FILES[$category])
);
}
$listItem = $listItems[$sourceUrl];
$path = (string)parse_url($sourceUrl, PHP_URL_PATH);
$pathSegments = array_values(array_filter(explode('/', trim($path, '/')), 'strlen'));
$sourceUrlKey = $pathSegments ? (string)end($pathSegments) : '';
if ($sourceUrlKey === '') {
throw new LocalizedException(
__('Cannot derive a URL key from item %1 in %2.', $index + 1, $filename)
);
}
$htmlPath = $this->resolveHtmlPath($sourceRoot, (string)$item['localHtmlFile']);
if (isset($records[$sourceUrl])) {
$existing = $records[$sourceUrl];
if ($existing['title'] !== trim((string)$item['title'])
|| $existing['publish_date'] !== (string)$item['publishDate']
) {
throw new LocalizedException(
__('Conflicting duplicate post data for source URL %1.', $sourceUrl)
);
}
if ($records[$sourceUrl]['short_description'] !== ''
&& $listItem['summary'] !== ''
&& $records[$sourceUrl]['short_description'] !== $listItem['summary']
) {
throw new LocalizedException(__('Conflicting summaries for source URL %1.', $sourceUrl));
}
if ($records[$sourceUrl]['image_filename'] !== ''
&& $listItem['image_filename'] !== ''
&& $records[$sourceUrl]['image_filename'] !== $listItem['image_filename']
) {
throw new LocalizedException(__('Conflicting list images for source URL %1.', $sourceUrl));
}
if ($records[$sourceUrl]['short_description'] === '') {
$records[$sourceUrl]['short_description'] = $listItem['summary'];
}
if ($records[$sourceUrl]['image_filename'] === '') {
$records[$sourceUrl]['image_filename'] = $listItem['image_filename'];
}
$records[$sourceUrl]['categories'][$category] = $category;
continue;
}
$records[$sourceUrl] = [
'title' => trim((string)$item['title']),
'source_url' => $sourceUrl,
'source_url_key' => $sourceUrlKey,
'publish_date' => (string)$item['publishDate'],
'html_path' => $htmlPath,
'short_description' => $listItem['summary'],
'image_filename' => $listItem['image_filename'],
'categories' => [$category => $category],
];
}
}
return [
'records' => $records,
'json_records' => $jsonRecords,
'duplicate_records' => $jsonRecords - count($records),
];
}
/**
* @return array<string, array{summary: string, image_filename: string}>
* @throws LocalizedException
*/
private function readListItems(string $sourceRoot, string $category): array
{
$filename = self::LIST_FILES[$category];
$jsonPath = $sourceRoot . DIRECTORY_SEPARATOR . $filename;
if (!is_file($jsonPath) || !is_readable($jsonPath)) {
throw new LocalizedException(__('Required list JSON file is missing or unreadable: %1', $jsonPath));
}
try {
$data = $this->json->unserialize((string)file_get_contents($jsonPath));
} catch (\InvalidArgumentException $exception) {
throw new LocalizedException(__('Invalid JSON in %1: %2', $filename, $exception->getMessage()));
}
if (!is_array($data) || !isset($data['news']) || !is_array($data['news'])) {
throw new LocalizedException(__('The news array is missing from %1.', $filename));
}
if (isset($data['total_count']) && (int)$data['total_count'] !== count($data['news'])) {
throw new LocalizedException(__('The total_count in %1 does not match its news count.', $filename));
}
$result = [];
foreach ($data['news'] as $index => $item) {
if (!is_array($item) || !isset($item['link']) || !is_string($item['link']) || trim($item['link']) === '') {
throw new LocalizedException(__('Missing or invalid link in item %1 of %2.', $index + 1, $filename));
}
$link = trim($item['link']);
if (isset($result[$link])) {
throw new LocalizedException(__('Duplicate link %1 in %2.', $link, $filename));
}
$summary = isset($item['summary']) && is_string($item['summary']) ? trim($item['summary']) : '';
$imageUrl = isset($item['imageUrl']) && is_string($item['imageUrl']) ? trim($item['imageUrl']) : '';
$result[$link] = [
'summary' => $summary,
'image_filename' => $this->extractImageFilename($imageUrl, $index, $filename),
];
}
return $result;
}
/** @throws LocalizedException */
private function extractImageFilename(string $imageUrl, int $index, string $filename): string
{
if ($imageUrl === '') {
return '';
}
$path = (string)preg_split('/[?#]/', $imageUrl, 2)[0];
$imageFilename = rawurldecode(basename($path));
if ($imageFilename === '' || $imageFilename === '.' || $imageFilename === '..') {
throw new LocalizedException(__('Invalid imageUrl in item %1 of %2.', $index + 1, $filename));
}
return $imageFilename;
}
/** @throws LocalizedException */
private function validateItem(array $item, int $index, string $filename): void
{
foreach (['title', 'sourceUrl', 'publishDate', 'localHtmlFile'] as $field) {
if (!isset($item[$field]) || !is_string($item[$field]) || trim($item[$field]) === '') {
throw new LocalizedException(
__('Missing or invalid %1 in item %2 of %3.', $field, $index + 1, $filename)
);
}
}
$sourceUrl = (string)$item['sourceUrl'];
$parts = parse_url($sourceUrl);
if (!is_array($parts)
|| ($parts['scheme'] ?? '') !== 'https'
|| strtolower((string)($parts['host'] ?? '')) !== 'www.esun3d.com'
|| !str_starts_with((string)($parts['path'] ?? ''), '/news/')
) {
throw new LocalizedException(__('Invalid sourceUrl in item %1 of %2.', $index + 1, $filename));
}
$date = \DateTimeImmutable::createFromFormat('!Y-m-d', (string)$item['publishDate']);
if ($date === false || $date->format('Y-m-d') !== $item['publishDate']) {
throw new LocalizedException(__('Invalid publishDate in item %1 of %2.', $index + 1, $filename));
}
}
/** @throws LocalizedException */
private function resolveHtmlPath(string $sourceRoot, string $relativePath): string
{
if ($relativePath === '' || str_contains($relativePath, "\0") || str_starts_with($relativePath, '/')) {
throw new LocalizedException(__('Invalid localHtmlFile path: %1', $relativePath));
}
$htmlPath = realpath($sourceRoot . DIRECTORY_SEPARATOR . $relativePath);
$rootPrefix = rtrim($sourceRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
if ($htmlPath === false
|| !is_file($htmlPath)
|| !is_readable($htmlPath)
|| !str_starts_with($htmlPath, $rootPrefix)
) {
throw new LocalizedException(__('HTML file is missing, unreadable, or outside the source: %1', $relativePath));
}
return $htmlPath;
}
}