backup
This commit is contained in:
1 parent
c8bcfe6bd6
commit
9c277e6db8
18 files changed
+2506
-671
No files matched your search
@@ -3,6 +3,8 @@ if (!defined("_PS_VERSION_")) {
|
||||
exit();
|
||||
}
|
||||
|
||||
require_once dirname(__FILE__) . '/interfaces/IProductSync.php';
|
||||
|
||||
class EcomZoneProductSync implements IProductSync
|
||||
{
|
||||
private EcomZoneClient $client;
|
||||
@@ -22,63 +24,112 @@ class EcomZoneProductSync implements IProductSync
|
||||
|
||||
public function importProducts(int $perPage = 100): array
|
||||
{
|
||||
$page = 1;
|
||||
$totalImported = 0;
|
||||
$totalAvailable = 0;
|
||||
$errors = [];
|
||||
|
||||
try {
|
||||
EcomZoneLogger::log("Starting product import", "INFO", [
|
||||
// Get or initialize sync state
|
||||
$syncState = $this->getSyncState();
|
||||
|
||||
// If this is a new sync (page 1 and no imports), reset everything
|
||||
if (empty($syncState) || ($syncState['current_page'] ?? 1) === 1 && ($syncState['total_imported'] ?? 0) === 0) {
|
||||
$this->resetSyncState();
|
||||
$syncState = [];
|
||||
}
|
||||
|
||||
$page = $syncState['current_page'] ?? 1;
|
||||
$totalImported = $syncState['total_imported'] ?? 0;
|
||||
$totalAvailable = $syncState['total_available'] ?? 0;
|
||||
$errors = $syncState['errors'] ?? [];
|
||||
$maxProductsPerBatch = 20; // Process 20 products at a time to avoid timeout
|
||||
|
||||
EcomZoneLogger::log("Starting/Resuming product import", "INFO", [
|
||||
"page" => $page,
|
||||
"per_page" => $perPage,
|
||||
"total_imported" => $totalImported,
|
||||
"is_new_sync" => empty($syncState)
|
||||
]);
|
||||
|
||||
do {
|
||||
$catalog = $this->client->getCatalog($page, $perPage);
|
||||
// Get catalog for current page
|
||||
$catalog = $this->client->getCatalog($page, $perPage);
|
||||
|
||||
if (!isset($catalog["data"]) || !is_array($catalog["data"])) {
|
||||
throw new EcomZoneException(
|
||||
"Invalid catalog response",
|
||||
EcomZoneException::API_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($catalog["data"] as $product) {
|
||||
try {
|
||||
if ($this->importSingleProduct($product)) {
|
||||
$totalImported++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$errors[] = [
|
||||
"sku" => $product["sku"] ?? "unknown",
|
||||
"error" => $e->getMessage(),
|
||||
];
|
||||
EcomZoneLogger::log("Product import failed", "ERROR", [
|
||||
"sku" => $product["sku"] ?? "unknown",
|
||||
"error" => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (!isset($catalog["data"]) || !is_array($catalog["data"])) {
|
||||
throw new EcomZoneException(
|
||||
"Invalid catalog response",
|
||||
EcomZoneException::API_ERROR
|
||||
);
|
||||
}
|
||||
|
||||
// Update total available on first page or if it changes
|
||||
if ($page === 1 || ($catalog["total"] ?? 0) !== $totalAvailable) {
|
||||
$totalAvailable = $catalog["total"] ?? 0;
|
||||
$page++;
|
||||
}
|
||||
|
||||
EcomZoneLogger::log("Page processed", "INFO", [
|
||||
"page" => $page - 1,
|
||||
"imported" => $totalImported,
|
||||
"total" => $totalAvailable,
|
||||
]);
|
||||
} while (
|
||||
isset($catalog["next_page_url"]) &&
|
||||
$catalog["next_page_url"] !== null
|
||||
);
|
||||
$productsProcessed = 0;
|
||||
$batchErrors = [];
|
||||
|
||||
// Process only a batch of products
|
||||
foreach ($catalog["data"] as $product) {
|
||||
if ($productsProcessed >= $maxProductsPerBatch) {
|
||||
break;
|
||||
}
|
||||
|
||||
$this->clearCache();
|
||||
try {
|
||||
if ($this->importSingleProduct($product)) {
|
||||
$totalImported++;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$batchErrors[] = [
|
||||
"sku" => $product["sku"] ?? "unknown",
|
||||
"error" => $e->getMessage(),
|
||||
];
|
||||
EcomZoneLogger::log("Product import failed", "ERROR", [
|
||||
"sku" => $product["sku"] ?? "unknown",
|
||||
"error" => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
$productsProcessed++;
|
||||
}
|
||||
|
||||
// Update errors array with new batch errors
|
||||
$errors = array_merge($errors, $batchErrors);
|
||||
|
||||
// Calculate progress
|
||||
$progress = [
|
||||
'current_page' => $page,
|
||||
'total_imported' => $totalImported,
|
||||
'total_available' => $totalAvailable,
|
||||
'errors' => $errors,
|
||||
'is_complete' => false,
|
||||
'products_processed_on_page' => $productsProcessed
|
||||
];
|
||||
|
||||
// Check if we need to continue to next page
|
||||
if ($productsProcessed < count($catalog["data"])) {
|
||||
// Still more products to process on this page
|
||||
$progress['products_processed_on_page'] = $productsProcessed;
|
||||
} else {
|
||||
// Move to next page if available
|
||||
if (isset($catalog["next_page_url"]) && $catalog["next_page_url"] !== null) {
|
||||
$progress['current_page'] = $page + 1;
|
||||
$progress['products_processed_on_page'] = 0;
|
||||
} else {
|
||||
$progress['is_complete'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Save progress
|
||||
$this->saveSyncState($progress);
|
||||
|
||||
// Clear cache if complete
|
||||
if ($progress['is_complete']) {
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
return [
|
||||
"success" => true,
|
||||
"imported" => $totalImported,
|
||||
"total" => $totalAvailable,
|
||||
"errors" => $errors,
|
||||
"is_complete" => $progress['is_complete'],
|
||||
"current_page" => $progress['current_page']
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Product import process failed", "ERROR", [
|
||||
@@ -192,8 +243,11 @@ class EcomZoneProductSync implements IProductSync
|
||||
$product->name[$this->defaultLangId] = $data['product_name'];
|
||||
$product->description[$this->defaultLangId] = $data['long_description'] ?? $data['description'];
|
||||
$product->description_short[$this->defaultLangId] = $data['description'];
|
||||
$product->price = (float)$data['product_price'];
|
||||
$product->wholesale_price = (float)($data['recommended_retail_price'] ?? 0);
|
||||
|
||||
// Update prices
|
||||
$product->wholesale_price = (float)$data['product_price']; // Original price as wholesale price
|
||||
$product->price = (float)($data['recommended_retail_price'] ?? $data['product_price']); // Recommended retail price for shop display
|
||||
|
||||
$product->reference = $data['sku'];
|
||||
$product->ean13 = $data['ean'] ?? '';
|
||||
|
||||
@@ -354,15 +408,32 @@ class EcomZoneProductSync implements IProductSync
|
||||
private function updateProductStock(Product $product, array $data): void
|
||||
{
|
||||
try {
|
||||
$quantity = (int) ($data['stock_quantity'] ?? 0);
|
||||
StockAvailable::setQuantity($product->id, 0, $quantity);
|
||||
$quantity = (int)($data['stock'] ?? 0);
|
||||
|
||||
// Make sure product is saved first
|
||||
if (!$product->id) {
|
||||
$product->save();
|
||||
}
|
||||
|
||||
// Update stock quantity
|
||||
StockAvailable::setQuantity(
|
||||
$product->id,
|
||||
0, // Combination ID (0 for products without combinations)
|
||||
$quantity
|
||||
);
|
||||
|
||||
// Update out of stock behavior
|
||||
StockAvailable::setProductOutOfStock(
|
||||
$product->id,
|
||||
$quantity > 0 ? 2 : 0, // 2 = Allow orders, 0 = Deny orders
|
||||
2, // Always allow orders
|
||||
$this->defaultShopId
|
||||
);
|
||||
|
||||
EcomZoneLogger::log("Stock updated", "DEBUG", [
|
||||
"product" => $product->reference,
|
||||
"quantity" => $quantity
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Failed to update stock", "WARNING", [
|
||||
"product" => $product->reference,
|
||||
@@ -375,49 +446,126 @@ class EcomZoneProductSync implements IProductSync
|
||||
{
|
||||
try {
|
||||
$imageUrls = [];
|
||||
|
||||
EcomZoneLogger::log("Raw product data for images", "DEBUG", [
|
||||
"product" => $product->reference,
|
||||
"data" => json_encode($data)
|
||||
]);
|
||||
|
||||
// Process main image
|
||||
if (!empty($data["image"])) {
|
||||
$imageUrls[] = $data["image"];
|
||||
}
|
||||
|
||||
// Process additional images
|
||||
if (!empty($data["images"])) {
|
||||
$additionalImages = json_decode($data["images"], true);
|
||||
if (is_array($additionalImages)) {
|
||||
$imageUrls = array_merge($imageUrls, $additionalImages);
|
||||
// Handle different possible data structures
|
||||
if (isset($data['images']) && is_string($data['images'])) {
|
||||
$images = json_decode($data['images'], true);
|
||||
if (json_last_error() === JSON_ERROR_NONE && is_array($images)) {
|
||||
$imageUrls = array_merge($imageUrls, $images);
|
||||
}
|
||||
} elseif (isset($data['images']) && is_array($data['images'])) {
|
||||
$imageUrls = array_merge($imageUrls, $data['images']);
|
||||
}
|
||||
|
||||
// Add main image if available
|
||||
if (!empty($data['image'])) {
|
||||
array_unshift($imageUrls, $data['image']); // Add as first image
|
||||
}
|
||||
|
||||
// Filter out empty or invalid URLs
|
||||
$imageUrls = array_filter($imageUrls, function($url) {
|
||||
return !empty($url) && filter_var($url, FILTER_VALIDATE_URL);
|
||||
});
|
||||
|
||||
if (empty($imageUrls)) {
|
||||
EcomZoneLogger::log("No valid images found for product", "WARNING", [
|
||||
"product" => $product->reference,
|
||||
"data" => json_encode($data)
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete existing images if any
|
||||
$product->deleteImages();
|
||||
|
||||
$successCount = 0;
|
||||
foreach ($imageUrls as $index => $imageUrl) {
|
||||
$this->importProductImage($product, $imageUrl, $index === 0);
|
||||
try {
|
||||
EcomZoneLogger::log("Processing image", "DEBUG", [
|
||||
"product" => $product->reference,
|
||||
"image_url" => $imageUrl,
|
||||
"index" => $index
|
||||
]);
|
||||
|
||||
$this->importProductImage($product, $imageUrl, $index === 0);
|
||||
$successCount++;
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Failed to process image", "WARNING", [
|
||||
"product" => $product->reference,
|
||||
"image_url" => $imageUrl,
|
||||
"error" => $e->getMessage()
|
||||
]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($successCount === 0 && !empty($imageUrls)) {
|
||||
throw new Exception(sprintf(
|
||||
"Failed to process any images for the product. Attempted %d images.",
|
||||
count($imageUrls)
|
||||
));
|
||||
}
|
||||
|
||||
EcomZoneLogger::log("Processed product images", "INFO", [
|
||||
"product" => $product->reference,
|
||||
"total_images" => count($imageUrls),
|
||||
"successful_imports" => $successCount
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Failed to process product images", "WARNING", [
|
||||
EcomZoneLogger::log("Failed to process product images", "ERROR", [
|
||||
"sku" => $product->reference,
|
||||
"error" => $e->getMessage(),
|
||||
"data" => json_encode($data)
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function importProductImage(Product $product, string $imageUrl, bool $cover = false): void
|
||||
{
|
||||
try {
|
||||
// Create temporary file
|
||||
$tempFile = tempnam(_PS_TMP_IMG_DIR_, 'import_');
|
||||
// Create temporary directory if it doesn't exist
|
||||
$tempDir = _PS_MODULE_DIR_ . 'ecomzone/tmp/';
|
||||
if (!is_dir($tempDir)) {
|
||||
mkdir($tempDir, 0755, true);
|
||||
chmod($tempDir, 0755);
|
||||
}
|
||||
|
||||
// Clean URL - handle double escaped slashes
|
||||
$cleanUrl = stripslashes(stripslashes(trim($imageUrl)));
|
||||
|
||||
// Download image
|
||||
if (!copy($imageUrl, $tempFile)) {
|
||||
throw new Exception("Failed to download image: " . $imageUrl);
|
||||
EcomZoneLogger::log("Processing image URL", "DEBUG", [
|
||||
"product" => $product->reference,
|
||||
"original_url" => $imageUrl,
|
||||
"cleaned_url" => $cleanUrl
|
||||
]);
|
||||
|
||||
// Create temp file
|
||||
$tempFile = tempnam($tempDir, 'import_');
|
||||
if ($tempFile) {
|
||||
chmod($tempFile, 0644);
|
||||
}
|
||||
|
||||
// Use the client class to download the image
|
||||
$imageResult = $this->client->downloadImage($cleanUrl);
|
||||
$imageData = $imageResult['data'];
|
||||
$contentType = $imageResult['content_type'];
|
||||
|
||||
// Save image data to temp file
|
||||
file_put_contents($tempFile, $imageData);
|
||||
|
||||
// Verify the downloaded file is a valid image
|
||||
$imageInfo = @getimagesize($tempFile);
|
||||
if (!$imageInfo) {
|
||||
throw new Exception("Downloaded file is not a valid image. Content type: $contentType");
|
||||
}
|
||||
|
||||
// Create image object
|
||||
$image = new Image();
|
||||
$image->id_product = $product->id;
|
||||
@@ -433,29 +581,60 @@ class EcomZoneProductSync implements IProductSync
|
||||
$imageDir = dirname($imagePath);
|
||||
|
||||
if (!file_exists($imageDir)) {
|
||||
mkdir($imageDir, 0755, true);
|
||||
if (!mkdir($imageDir, 0755, true)) {
|
||||
throw new Exception("Failed to create image directory: " . $imageDir);
|
||||
}
|
||||
chmod($imageDir, 0755);
|
||||
}
|
||||
|
||||
// Generate image
|
||||
if (!ImageManager::resize(
|
||||
$tempFile,
|
||||
$imagePath . '.jpg'
|
||||
$imagePath . '.jpg',
|
||||
null,
|
||||
null,
|
||||
'jpg'
|
||||
)) {
|
||||
throw new Exception("Failed to process image");
|
||||
}
|
||||
|
||||
// Ensure the generated image has correct permissions
|
||||
chmod($imagePath . '.jpg', 0644);
|
||||
|
||||
// Generate thumbnails
|
||||
$this->generateThumbnails($image);
|
||||
|
||||
// Clean up
|
||||
unlink($tempFile);
|
||||
if (file_exists($tempFile)) {
|
||||
unlink($tempFile);
|
||||
}
|
||||
|
||||
EcomZoneLogger::log("Image imported successfully", "DEBUG", [
|
||||
"product" => $product->reference,
|
||||
"image_url" => $cleanUrl,
|
||||
"mime_type" => $imageInfo['mime']
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Failed to import product image", "ERROR", [
|
||||
"product" => $product->reference,
|
||||
"image_url" => $imageUrl,
|
||||
"error" => $e->getMessage()
|
||||
"clean_url" => $cleanUrl ?? '',
|
||||
"error" => $e->getMessage(),
|
||||
"content_type" => $contentType ?? null
|
||||
]);
|
||||
|
||||
// Clean up on error
|
||||
if (isset($tempFile) && file_exists($tempFile)) {
|
||||
unlink($tempFile);
|
||||
}
|
||||
|
||||
// Delete image record if it was created
|
||||
if (isset($image) && $image->id) {
|
||||
$image->delete();
|
||||
}
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,117 +642,41 @@ class EcomZoneProductSync implements IProductSync
|
||||
{
|
||||
try {
|
||||
$imageTypes = ImageType::getImagesTypes('products');
|
||||
$sourceFile = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg';
|
||||
|
||||
if (!file_exists($sourceFile)) {
|
||||
throw new Exception("Source image file not found");
|
||||
}
|
||||
|
||||
foreach ($imageTypes as $imageType) {
|
||||
$dir = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath();
|
||||
$destination = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg';
|
||||
$dir = dirname($destination);
|
||||
|
||||
if (!file_exists($dir)) {
|
||||
mkdir($dir, 0755, true);
|
||||
if (!mkdir($dir, 0755, true)) {
|
||||
throw new Exception("Failed to create thumbnail directory: " . $dir);
|
||||
}
|
||||
chmod($dir, 0755);
|
||||
}
|
||||
ImageManager::resize(
|
||||
_PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg',
|
||||
_PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg',
|
||||
|
||||
if (!ImageManager::resize(
|
||||
$sourceFile,
|
||||
$destination,
|
||||
(int)$imageType['width'],
|
||||
(int)$imageType['height']
|
||||
);
|
||||
)) {
|
||||
throw new Exception("Failed to generate thumbnail: " . $imageType['name']);
|
||||
}
|
||||
|
||||
// Set proper permissions for the thumbnail
|
||||
chmod($destination, 0644);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
EcomZoneLogger::log("Failed to generate thumbnails", "WARNING", [
|
||||
"image_id" => $image->id,
|
||||
"error" => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function generateImageType(
|
||||
string $srcPath,
|
||||
string $destPath,
|
||||
?int $width,
|
||||
?int $height
|
||||
): void {
|
||||
try {
|
||||
$this->createDirectoryIfNotExists(dirname($destPath));
|
||||
|
||||
if ($width === null || $height === null) {
|
||||
copy($srcPath, $destPath);
|
||||
return;
|
||||
}
|
||||
|
||||
$imageInfo = getimagesize($srcPath);
|
||||
if (!$imageInfo) {
|
||||
throw new Exception("Invalid image file");
|
||||
}
|
||||
|
||||
$srcImage = $this->createImageFromType($srcPath, $imageInfo[2]);
|
||||
$destImage = imagecreatetruecolor($width, $height);
|
||||
|
||||
// Preserve transparency for PNG images
|
||||
if ($imageInfo[2] === IMAGETYPE_PNG) {
|
||||
imagealphablending($destImage, false);
|
||||
imagesavealpha($destImage, true);
|
||||
$transparent = imagecolorallocatealpha(
|
||||
$destImage,
|
||||
255,
|
||||
255,
|
||||
255,
|
||||
127
|
||||
);
|
||||
imagefilledrectangle(
|
||||
$destImage,
|
||||
0,
|
||||
0,
|
||||
$width,
|
||||
$height,
|
||||
$transparent
|
||||
);
|
||||
}
|
||||
|
||||
imagecopyresampled(
|
||||
$destImage,
|
||||
$srcImage,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
$width,
|
||||
$height,
|
||||
$imageInfo[0],
|
||||
$imageInfo[1]
|
||||
);
|
||||
|
||||
imagejpeg($destImage, $destPath, 95);
|
||||
|
||||
imagedestroy($srcImage);
|
||||
imagedestroy($destImage);
|
||||
} catch (Exception $e) {
|
||||
throw new EcomZoneException(
|
||||
"Failed to generate image type: " . $e->getMessage(),
|
||||
EcomZoneException::IMAGE_PROCESSING_ERROR,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function createImageFromType(string $filename, int $type)
|
||||
{
|
||||
switch ($type) {
|
||||
case IMAGETYPE_JPEG:
|
||||
return imagecreatefromjpeg($filename);
|
||||
case IMAGETYPE_PNG:
|
||||
return imagecreatefrompng($filename);
|
||||
case IMAGETYPE_GIF:
|
||||
return imagecreatefromgif($filename);
|
||||
default:
|
||||
throw new Exception("Unsupported image type: " . $type);
|
||||
}
|
||||
}
|
||||
|
||||
private function createDirectoryIfNotExists(string $directory): void
|
||||
{
|
||||
if (!is_dir($directory)) {
|
||||
if (!mkdir($directory, 0755, true)) {
|
||||
throw new Exception(
|
||||
"Failed to create directory: " . $directory
|
||||
);
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,6 +819,27 @@ class EcomZoneProductSync implements IProductSync
|
||||
Media::clearCache();
|
||||
PrestaShopAutoload::getInstance()->generateIndex();
|
||||
}
|
||||
|
||||
private function getSyncState(): array
|
||||
{
|
||||
$state = Configuration::get('ECOMZONE_SYNC_STATE');
|
||||
return $state ? json_decode($state, true) : [];
|
||||
}
|
||||
|
||||
private function saveSyncState(array $state): void
|
||||
{
|
||||
Configuration::updateValue('ECOMZONE_SYNC_STATE', json_encode($state));
|
||||
}
|
||||
|
||||
public function resetSyncState(): void
|
||||
{
|
||||
// Delete both sync state and progress
|
||||
Configuration::deleteByName('ECOMZONE_SYNC_STATE');
|
||||
Configuration::deleteByName('ECOMZONE_SYNC_PROGRESS');
|
||||
Configuration::deleteByName('ECOMZONE_LAST_SYNC');
|
||||
|
||||
EcomZoneLogger::log("Sync state reset", "INFO");
|
||||
}
|
||||
}
|
||||
|
||||
$token = Configuration::get('ECOMZONE_CRON_TOKEN');
|
||||
|
||||
Reference in new issue
Block a user