173 lines
6.3 KiB
PHP
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;
|
|
}
|
|
}
|