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

173 lines
6.3 KiB
PHP

<?php
declare(strict_types=1);
namespace Esun\Catalog\Model;
use Esun\Catalog\Model\ResourceModel\SpecificationFile as SpecificationFileResource;
use Esun\Catalog\Model\ResourceModel\SpecificationFile\CollectionFactory as SpecificationFileCollectionFactory;
use Esun\Catalog\Model\ResourceModel\SpecificationImport\CollectionFactory as SpecificationImportCollectionFactory;
use Magento\Framework\Exception\LocalizedException;
class SpecificationZipImporter
{
public function __construct(
private readonly SpecificationMedia $media,
private readonly SpecificationImportCollectionFactory $importCollectionFactory,
private readonly SpecificationFileCollectionFactory $fileCollectionFactory,
private readonly SpecificationImportFactory $importFactory,
private readonly SpecificationFileFactory $fileFactory,
private readonly SpecificationFileResource $fileResource
) {
}
public function import(SpecificationBrand $brand, string $archivePath): SpecificationImport
{
if (!class_exists(\ZipArchive::class)) {
throw new LocalizedException(__('ZIP import requires the PHP ZipArchive extension.'));
}
$brandId = (int)$brand->getId();
if ($brandId <= 0) {
throw new LocalizedException(__('Please save the brand before importing specifications.'));
}
$zip = new \ZipArchive();
$absoluteArchivePath = $this->media->getAbsolutePath($archivePath);
if ($zip->open($absoluteArchivePath) !== true) {
throw new LocalizedException(__('Unable to open the ZIP file.'));
}
$this->deleteExistingBrandData($brandId);
$import = $this->importFactory->create();
$import->setData([
'brand_id' => $brandId,
'archive_file' => $archivePath,
'file_count' => 0,
'status' => 1,
])->save();
$brandName = (string)$brand->getData('brand_name');
$normalizedBrandName = $this->normalizeName($brandName);
$fileCount = 0;
for ($index = 0; $index < $zip->numFiles; $index++) {
$stat = $zip->statIndex($index);
$entryName = (string)($stat['name'] ?? '');
if ($this->shouldSkipEntry($entryName)) {
continue;
}
$parts = array_values(array_filter(explode('/', trim($entryName, '/')), static fn (string $part): bool => $part !== ''));
if (count($parts) < 3) {
continue;
}
$fileName = array_pop($parts);
$modelName = (string)array_pop($parts);
$entryBrandName = (string)array_pop($parts);
if ($this->normalizeName($entryBrandName) !== $normalizedBrandName) {
continue;
}
$contents = $zip->getFromIndex($index);
if ($contents === false) {
continue;
}
$lowerFileName = strtolower($fileName);
$relativePath = $this->media->writeSpecificationFile($brandId, $modelName, $lowerFileName, $contents);
$file = $this->fileFactory->create();
$file->setData([
'import_id' => (int)$import->getId(),
'brand_id' => $brandId,
'brand_name' => $brandName,
'brand_logo' => (string)$brand->getData('brand_logo'),
'model_name' => $modelName,
'file_name' => $lowerFileName,
'file_path' => $relativePath,
'product_name_key' => $this->buildProductNameKey($lowerFileName),
'is_product_specific' => (int)$brand->getData('is_product_specific') === 1 ? 1 : 0,
'file_size' => strlen($contents),
'file_mtime' => !empty($stat['mtime']) ? date('Y-m-d H:i:s', (int)$stat['mtime']) : null,
]);
$this->fileResource->save($file);
$fileCount++;
}
$zip->close();
$import->setData('file_count', $fileCount)->save();
if ($fileCount === 0) {
$import->delete();
$this->media->deleteBrandParsedFiles($brandId);
throw new LocalizedException(__('No specification files were found for brand "%1" in this ZIP file.', $brandName));
}
return $import;
}
public function buildProductNameKey(string $fileName): string
{
$withoutExtension = pathinfo(strtolower($fileName), PATHINFO_FILENAME);
$key = preg_replace('/[^a-z0-9]+/', ' ', $withoutExtension) ?: '';
$key = preg_replace('/\\s+/', ' ', $key) ?: '';
return trim($key);
}
public function buildProductSearchTokens(string $productName): array
{
$normalized = $this->buildProductNameKey($productName);
$tokens = [];
if ($normalized !== '') {
$tokens[$normalized] = $normalized;
$parts = preg_split('/\\s+/', $normalized) ?: [];
for ($length = count($parts); $length >= 1; $length--) {
for ($start = 0; $start <= count($parts) - $length; $start++) {
$token = implode(' ', array_slice($parts, $start, $length));
if (strlen($token) >= 3) {
$tokens[$token] = $token;
}
}
}
}
return array_values($tokens);
}
private function deleteExistingBrandData(int $brandId): void
{
foreach ($this->importCollectionFactory->create()->addFieldToFilter('brand_id', $brandId) as $import) {
$import->delete();
}
foreach ($this->fileCollectionFactory->create()->addFieldToFilter('brand_id', $brandId) as $file) {
$file->delete();
}
$this->media->deleteBrandParsedFiles($brandId);
}
private function shouldSkipEntry(string $entryName): bool
{
$entryName = trim($entryName);
if ($entryName === '' || str_ends_with($entryName, '/')) {
return true;
}
$parts = explode('/', $entryName);
$basename = end($parts);
return str_contains($entryName, '__MACOSX/')
|| $basename === '.DS_Store'
|| str_starts_with((string)$basename, '._');
}
private function normalizeName(string $value): string
{
$value = strtolower(trim($value));
$value = preg_replace('/[^a-z0-9]+/', '', $value) ?: '';
return $value;
}
}