category -> product association implemented

This commit is contained in:
echo committed 2025-02-16 20:02:41 +01:00
1 parent 0b8c5de8b0
commit c8bcfe6bd6
41 files changed
+2222 -703

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneApiTest
{
private $client;
public function __construct()
{
$this->client = new EcomZoneClient();
}
public function testProductFetch()
{
try {
// Get first page with one product
$result = $this->client->getCatalog(1, 1);
EcomZoneLogger::log("API Test Result", "INFO", [
"result" => $result,
]);
return $result;
} catch (Exception $e) {
EcomZoneLogger::log("API Test Failed", "ERROR", [
"error" => $e->getMessage(),
]);
throw $e;
}
}
public function getProducts($page = 1, $perPage = 10)
{
try {
$result = $this->client->getCatalog($page, $perPage);
return [
"products" => $result["data"] ?? [],
"total" => $result["total"] ?? 0,
"current_page" => $page,
"per_page" => $perPage,
];
} catch (Exception $e) {
EcomZoneLogger::log("Failed to get products", "ERROR", [
"error" => $e->getMessage(),
]);
throw $e;
}
}
}
@@ -0,0 +1,215 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneCategoryHandler implements ICategory
{
private int $defaultLangId;
private array $categoryCache = [];
private array $categoryLinkRewriteCache = [];
private int $defaultShopId;
public function __construct()
{
$this->defaultLangId = (int) Configuration::get("PS_LANG_DEFAULT");
$this->defaultShopId = (int) Context::getContext()->shop->id;
EcomZoneLogger::log("Category Handler initialized", "INFO", [
"default_lang" => $this->defaultLangId,
"shop_id" => $this->defaultShopId,
]);
}
public function createCategory(string $name, ?int $parentId = null): int
{
try {
if (isset($this->categoryCache[$name])) {
return $this->categoryCache[$name];
}
$parentId =
$parentId ?: (int) Configuration::get("PS_HOME_CATEGORY");
$category = new Category();
$category->name = [$this->defaultLangId => trim($name)];
$category->link_rewrite = [
$this->defaultLangId => $this->generateLinkRewrite($name),
];
$category->id_parent = $parentId;
$category->active = true;
$category->id_shop_default = $this->defaultShopId;
// Set shop association for multistore
if (Shop::isFeatureActive()) {
$category->id_shop_list = [$this->defaultShopId];
}
if (!$category->add()) {
throw new EcomZoneException(
"Failed to create category: $name",
EcomZoneException::CATEGORY_CREATE_ERROR
);
}
$this->categoryCache[$name] = (int) $category->id;
EcomZoneLogger::log("Category created", "INFO", [
"name" => $name,
"id" => $category->id,
"parent_id" => $parentId,
]);
return $this->categoryCache[$name];
} catch (Exception $e) {
EcomZoneLogger::log("Category creation failed", "ERROR", [
"name" => $name,
"error" => $e->getMessage(),
]);
throw new EcomZoneException(
"Category creation failed: " . $e->getMessage(),
EcomZoneException::CATEGORY_CREATE_ERROR,
$e
);
}
}
public function getCategoryIdByName(string $name, ?int $parentId = null): ?int
{
try {
if (isset($this->categoryCache[$name])) {
return $this->categoryCache[$name];
}
$name = trim($name);
$sql = 'SELECT c.id_category
FROM `' .
_DB_PREFIX_ .
'category_lang` cl
JOIN `' .
_DB_PREFIX_ .
'category` c ON c.id_category = cl.id_category
WHERE cl.name = "' .
pSQL($name) .
'"
AND cl.id_lang = ' .
(int) $this->defaultLangId .
'
AND c.active = 1';
if ($parentId !== null) {
$sql .= ' AND c.id_parent = ' . (int) $parentId;
}
$categoryId = (int) Db::getInstance()->getValue($sql);
if ($categoryId) {
$this->categoryCache[$name] = $categoryId;
return $categoryId;
}
return null;
} catch (Exception $e) {
EcomZoneLogger::log("Category lookup failed", "ERROR", [
"name" => $name,
"error" => $e->getMessage(),
]);
return null;
}
}
public function processCategoriesFromString(string $categories): array
{
$categoryNames = array_map('trim', explode('>', $categories));
$categoryIds = [];
$parentId = null;
foreach ($categoryNames as $name) {
$categoryId = $this->getCategoryIdByName($name, $parentId);
if (!$categoryId) {
$categoryId = $this->createCategory($name, $parentId);
}
if ($categoryId) {
$categoryIds[] = $categoryId;
$parentId = $categoryId;
}
}
return $categoryIds;
}
private function generateLinkRewrite(string $name): string
{
if (isset($this->categoryLinkRewriteCache[$name])) {
return $this->categoryLinkRewriteCache[$name];
}
$linkRewrite = Tools::str2url($name);
// Ensure uniqueness
$suffix = 1;
$originalLinkRewrite = $linkRewrite;
while ($this->linkRewriteExists($linkRewrite)) {
$linkRewrite = $originalLinkRewrite . "-" . $suffix++;
}
$this->categoryLinkRewriteCache[$name] = $linkRewrite;
return $linkRewrite;
}
private function linkRewriteExists(string $linkRewrite): bool
{
return (bool) Db::getInstance()->getValue(
'SELECT COUNT(*)
FROM `' .
_DB_PREFIX_ .
'category_lang` cl
JOIN `' .
_DB_PREFIX_ .
'category_shop` cs
ON cl.id_category = cs.id_category
WHERE cl.link_rewrite = "' .
pSQL($linkRewrite) .
'"
AND cl.id_lang = ' .
(int) $this->defaultLangId .
'
AND cs.id_shop = ' .
(int) $this->defaultShopId
);
}
public function createCategoryPath(array $categoryNames): int
{
$parentId = (int) Configuration::get("PS_HOME_CATEGORY");
$lastCategoryId = $parentId;
foreach ($categoryNames as $categoryName) {
$categoryName = trim($categoryName);
if (empty($categoryName)) {
continue;
}
try {
$categoryId = $this->getCategoryIdByName($categoryName);
if (!$categoryId) {
$categoryId = $this->createCategory(
$categoryName,
$parentId
);
}
$lastCategoryId = $categoryId;
$parentId = $categoryId;
} catch (Exception $e) {
EcomZoneLogger::log("Category path creation failed", "ERROR", [
"category" => $categoryName,
"error" => $e->getMessage(),
]);
break;
}
}
return $lastCategoryId;
}
}
+129 -84
View File
@@ -1,107 +1,152 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneClient
{
private $apiToken;
private $apiUrl;
private $maxRetries = 3;
private $timeout = 30;
public function __construct()
{
// Hardcoded API token for testing
$this->apiToken = 'klRyAdrXaxL0s6PEUp7LDlH6T8aPSCtBY8NiEHsHiWpc6646K2TZPi5KMxUg';
$this->apiUrl = Configuration::get('ECOMZONE_API_URL');
if (empty($this->apiUrl)) {
throw new Exception('API URL not configured');
$this->apiToken = Configuration::get("ECOMZONE_API_TOKEN");
$this->apiUrl = Configuration::get("ECOMZONE_API_URL");
if (empty($this->apiToken)) {
throw new EcomZoneException(
"API token not configured",
EcomZoneException::API_ERROR
);
}
if (empty($this->apiUrl)) {
throw new EcomZoneException(
"API URL not configured",
EcomZoneException::API_ERROR
);
}
EcomZoneLogger::log("API Client initialized", "INFO", [
"url" => $this->apiUrl,
]);
}
public function getCatalog($page = 1, $perPage = 100)
{
$url = $this->apiUrl . '/catalog?page=' . $page . '&per_page=' . $perPage;
EcomZoneLogger::log("Making API request", 'INFO', ['url' => $url]);
return $this->makeRequest('GET', $url);
$url = $this->apiUrl . "/catalog";
$params = [
"page" => $page,
"per_page" => $perPage,
];
return $this->makeRequest("GET", $url, $params);
}
public function getProduct($sku)
{
return $this->makeRequest('GET', $this->apiUrl . '/product/' . $sku);
$url = $this->apiUrl . "/product/" . urlencode($sku);
return $this->makeRequest("GET", $url);
}
public function createOrder($orderData)
{
return $this->makeRequest('POST', $this->apiUrl . '/ordering', $orderData);
}
public function getOrder($orderId)
{
return $this->makeRequest('GET', $this->apiUrl . '/order/' . $orderId);
}
private function makeRequest($method, $url, $data = null)
private function makeRequest($method, $url, $params = [], $data = null)
{
$retryCount = 0;
$maxRetries = 5;
$backoff = 1; // initial backoff time in seconds
do {
$curl = curl_init();
$headers = [
'Authorization: Bearer ' . $this->apiToken,
'Accept: application/json',
'Content-Type: application/json',
];
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_SSL_VERIFYPEER => false, // Temporarily disable SSL verification for testing
];
if ($data !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($data);
}
curl_setopt_array($curl, $options);
$response = curl_exec($curl);
$err = curl_error($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
EcomZoneLogger::log("API Response", 'INFO', [
'url' => $url,
'method' => $method,
'http_code' => $httpCode,
'curl_error' => $err,
'response' => $response
]);
curl_close($curl);
if ($err) {
if (strpos($err, 'Connection timed out') !== false && $retryCount < $maxRetries) {
EcomZoneLogger::log("Connection timed out. Retrying in $backoff seconds...", 'WARNING');
sleep($backoff);
$backoff *= 2; // exponential backoff
$retryCount++;
} else {
throw new Exception('cURL Error: ' . $err);
}
} else if ($httpCode >= 400) {
throw new Exception('API Error: HTTP ' . $httpCode . ' - ' . $response);
} else {
$decodedResponse = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('Invalid JSON response: ' . json_last_error_msg());
}
return $decodedResponse;
}
} while ($retryCount < $maxRetries);
}
$lastError = null;
do {
try {
EcomZoneLogger::log("Making API request", "INFO", [
"method" => $method,
"url" => $url,
"params" => $params,
"retry" => $retryCount,
]);
// Add query parameters
if (!empty($params)) {
$url .=
(strpos($url, "?") === false ? "?" : "&") .
http_build_query($params);
}
$curl = curl_init();
$options = [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => $this->timeout,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . $this->apiToken,
"Accept: application/json",
"Content-Type: application/json",
],
];
if ($data !== null) {
$options[CURLOPT_POSTFIELDS] = json_encode($data);
}
curl_setopt_array($curl, $options);
$response = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$error = curl_error($curl);
curl_close($curl);
if ($error) {
throw new Exception("cURL Error: " . $error);
}
if ($httpCode >= 400) {
throw new Exception(
"HTTP Error: " . $httpCode . " Response: " . $response
);
}
$decodedResponse = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception("Invalid JSON response");
}
EcomZoneLogger::log("API request successful", "INFO", [
"http_code" => $httpCode,
]);
return $decodedResponse;
} catch (Exception $e) {
$lastError = $e;
$retryCount++;
EcomZoneLogger::log("API request failed", "ERROR", [
"error" => $e->getMessage(),
"retry" => $retryCount,
]);
if ($retryCount < $this->maxRetries) {
sleep(pow(2, $retryCount));
continue;
}
throw new EcomZoneException(
"API request failed after {$this->maxRetries} retries: " .
$e->getMessage(),
EcomZoneException::API_ERROR,
$e
);
}
} while ($retryCount < $this->maxRetries);
throw new EcomZoneException(
"Max retries reached: " . $lastError->getMessage(),
EcomZoneException::API_ERROR,
$lastError
);
}
}
-40
View File
@@ -1,40 +0,0 @@
<?php
class EcomZoneCronTask extends ModuleGrid
{
public function __construct()
{
parent::__construct();
$this->name = 'ecomzone_cron';
$this->title = $this->l('EcomZone Product Sync');
$this->cron_frequency = 3600 * 2; // Run every hour, change this to 2 hours!!!
}
public function install()
{
if (!parent::install()) {
return false;
}
Configuration::updateValue('ECOMZONE_LAST_CRON_RUN', '');
return true;
}
public function run()
{
try {
EcomZoneLogger::log('Starting scheduled product sync');
$productSync = new EcomZoneProductSync();
$result = $produgtSync->importProducts();
Configuration::updateValue('ECOMZONE_LAST_CRON_RUN', date('Y-m-d H:i:s'));
EcomZoneLogger::log('Scheduled product sync completed', 'INFO', $result);
return true;
} catch (Exception $e) {
EcomZoneLogger::log('Scheduled product sync failed', 'ERROR', ['error' => $e->getMessage()]);
return false;
}
}
}
+13
View File
@@ -0,0 +1,13 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneException extends Exception
{
public const INVALID_PRODUCT_DATA = 1001;
public const CATEGORY_CREATE_ERROR = 1002;
public const IMAGE_PROCESSING_ERROR = 1003;
public const API_ERROR = 1004;
public const VALIDATION_ERROR = 1005;
}
+64 -19
View File
@@ -1,25 +1,70 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneLogger
{
const LOG_FILE = _PS_ROOT_DIR_ . '/var/logs/ecomzone.log';
public static function log($message, $level = 'INFO', $context = [])
private static $logFile = 'ecomzone.log';
private static $maxLogSize = 10485760; // 10MB
public static function log($message, $level = "INFO", $context = [])
{
$date = date('Y-m-d H:i:s');
$contextStr = !empty($context) ? json_encode($context) : '';
$logMessage = "[$date] [$level] $message $contextStr\n";
file_put_contents(self::LOG_FILE, $logMessage, FILE_APPEND);
// Also log to PrestaShop
PrestaShopLogger::addLog(
"EcomZone: $message",
($level === 'ERROR' ? 3 : 1),
null,
'EcomZone',
null,
true
);
try {
$logDir = _PS_MODULE_DIR_ . 'ecomzone/log/';
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$timestamp = date('Y-m-d H:i:s');
$contextStr = !empty($context) ? json_encode($context) : '';
$logMessage = sprintf(
"[%s] %s: %s %s\n",
$timestamp,
$level,
$message,
$contextStr
);
file_put_contents(
$logDir . self::$logFile,
$logMessage,
FILE_APPEND
);
} catch (Exception $e) {
// Fallback to PrestaShop's logger
PrestaShopLogger::addLog(
"EcomZone: $message",
($level === 'ERROR' ? 3 : 1)
);
}
}
}
private static function initLogFile()
{
if (!file_exists(self::$logFile)) {
$logDir = dirname(self::$logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
touch(self::$logFile);
chmod(self::$logFile, 0666);
}
}
private static function rotateLogIfNeeded()
{
if (!file_exists(self::$logFile)) {
return;
}
if (filesize(self::$logFile) > self::$maxLogSize) {
$backup = self::$logFile . "." . date("Y-m-d-H-i-s") . ".bak";
rename(self::$logFile, $backup);
touch(self::$logFile);
chmod(self::$logFile, 0666);
}
}
}
-81
View File
@@ -1,81 +0,0 @@
<?php
class EcomZoneOrderSync
{
private $client;
public function __construct()
{
$this->client = new EcomZoneClient();
}
public function syncOrder($orderId)
{
try {
EcomZoneLogger::log("Starting order sync", 'INFO', ['order_id' => $orderId]);
$order = new Order($orderId);
$customer = new Customer($order->id_customer);
$address = new Address($order->id_address_delivery);
$orderData = $this->prepareOrderData($order, $customer, $address);
$result = $this->client->createOrder($orderData);
EcomZoneLogger::log("Order sync completed", 'INFO', [
'order_id' => $orderId,
'result' => $result
]);
return $result;
} catch (Exception $e) {
EcomZoneLogger::log("Order sync failed", 'ERROR', [
'order_id' => $orderId,
'error' => $e->getMessage()
]);
throw $e;
}
}
private function prepareOrderData($order, $customer, $address)
{
return [
'order_index' => $order->id,
'ext_id' => $order->reference,
'payment' => [
'method' => $this->getPaymentMethod($order),
'customer_price' => $order->total_paid
],
'customer_data' => [
'full_name' => $customer->firstname . ' ' . $customer->lastname,
'email' => $customer->email,
'phone_number' => $address->phone,
'country' => Country::getIsoById($address->id_country),
'address' => $address->address1 . ' ' . $address->address2,
'city' => $address->city,
'post_code' => $address->postcode
],
'items' => $this->getOrderItems($order)
];
}
private function getPaymentMethod($order)
{
// Map PrestaShop payment modules to eComZone payment methods
return $order->module === 'cashondelivery' ? 'cod' : 'pp';
}
private function getOrderItems($order)
{
$items = [];
$products = $order->getProducts();
foreach ($products as $product) {
$items[] = [
'full_sku' => $product['reference'],
'quantity' => $product['product_quantity']
];
}
return $items;
}
}
+345
View File
@@ -0,0 +1,345 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneProductImport
{
private $module;
private $defaultLangId;
private $defaultShopId;
public function __construct()
{
$this->module = Module::getInstanceByName("ecomzone");
$this->defaultLangId = (int) Configuration::get("PS_LANG_DEFAULT");
$this->defaultShopId = (int) Context::getContext()->shop->id;
}
public function importProduct($productData)
{
try {
$data = $productData["data"];
// Start transaction
Db::getInstance()->execute("START TRANSACTION");
// Get or create product
$product = $this->getOrCreateProduct($data["sku"]);
// Update basic information
$this->updateBasicInfo($product, $data);
// Update categories
$this->updateCategories($product, $data);
// Update images
$this->updateImages($product, $data);
// Update specifications
$this->updateSpecifications($product, $data);
// Update stock
$this->updateStock($product, $data);
// Commit transaction
Db::getInstance()->execute("COMMIT");
EcomZoneLogger::log("Product imported successfully", "INFO", [
"sku" => $data["sku"],
]);
return true;
} catch (Exception $e) {
Db::getInstance()->execute("ROLLBACK");
EcomZoneLogger::log("Product import failed", "ERROR", [
"sku" => $data["sku"] ?? "unknown",
"error" => $e->getMessage(),
]);
throw $e;
}
}
private function getOrCreateProduct($sku)
{
$productId = Product::getIdByReference($sku);
if ($productId) {
$product = new Product($productId);
} else {
$product = new Product();
$product->reference = $sku;
}
return $product;
}
private function updateBasicInfo($product, $data)
{
$product->name[$this->defaultLangId] = $data["product_name"];
$product->description[$this->defaultLangId] = $data["long_description"];
$product->description_short[$this->defaultLangId] =
$data["description"];
$product->price = (float) $data["product_price"];
$product->wholesale_price =
(float) ($data["recommended_retail_price"] ?? 0);
$product->ean13 = $data["ean"] ?? "";
// Product settings
$product->active = true;
$product->visibility = "both";
$product->available_for_order = true;
$product->show_price = true;
$product->indexed = 1;
// Dimensions
if (!empty($data["dimensions"])) {
$dimensions = json_decode($data["dimensions"], true);
$product->weight = (float) ($dimensions["weight"] ?? 0);
$product->height = (float) ($dimensions["height"] ?? 0);
$product->width = (float) ($dimensions["width"] ?? 0);
$product->depth = (float) ($dimensions["length"] ?? 0);
}
if (!$product->id) {
$product->add();
} else {
$product->update();
}
}
private function updateCategories($product, $data)
{
$categoryIds = [];
// Process main categories
if (!empty($data["categories"])) {
$categories = json_decode($data["categories"], true);
foreach ($categories as $categoryName) {
if (strpos($categoryName, "0") === 0) {
continue;
} // Skip special categories
$categoryId = $this->getOrCreateCategory($categoryName);
if ($categoryId) {
$categoryIds[] = $categoryId;
}
}
}
// Process Google categories
if (!empty($data["google_categories"])) {
$googleCategories = json_decode($data["google_categories"], true);
foreach ($googleCategories as $categoryName) {
$categoryId = $this->getOrCreateCategory($categoryName);
if ($categoryId) {
$categoryIds[] = $categoryId;
}
}
}
// Ensure we have at least default category
if (empty($categoryIds)) {
$categoryIds[] = (int) Configuration::get("PS_HOME_CATEGORY");
}
$categoryIds = array_unique($categoryIds);
$product->id_category_default = reset($categoryIds);
$product->updateCategories($categoryIds);
}
private function getOrCreateCategory($name)
{
$name = trim($name);
if (empty($name)) {
return null;
}
// Try to find existing category
$categoryId = (int) Db::getInstance()->getValue(
'SELECT c.id_category
FROM `' .
_DB_PREFIX_ .
'category_lang` cl
JOIN `' .
_DB_PREFIX_ .
'category` c ON c.id_category = cl.id_category
WHERE cl.name = "' .
pSQL($name) .
'"
AND cl.id_lang = ' .
(int) $this->defaultLangId
);
if ($categoryId) {
return $categoryId;
}
// Create new category
$category = new Category();
$category->name = [$this->defaultLangId => $name];
$category->link_rewrite = [
$this->defaultLangId => Tools::str2url($name),
];
$category->id_parent = (int) Configuration::get("PS_HOME_CATEGORY");
$category->active = true;
if ($category->add()) {
return (int) $category->id;
}
return null;
}
private function updateImages($product, $data)
{
$imageUrls = [];
// Main image
if (!empty($data["image"])) {
$imageUrls[] = $data["image"];
}
// Additional images
if (!empty($data["images"])) {
$additionalImages = json_decode($data["images"], true);
if (is_array($additionalImages)) {
$imageUrls = array_merge($imageUrls, $additionalImages);
}
}
// GIF or video thumbnail
if (!empty($data["gif_or_video"])) {
$imageUrls[] = $data["gif_or_video"];
}
if (empty($imageUrls)) {
return;
}
// Delete existing images
$product->deleteImages();
// Import new images
foreach ($imageUrls as $index => $imageUrl) {
$this->importImage($product, $imageUrl, $index === 0);
}
}
private function importImage($product, $imageUrl, $cover = false)
{
$tmpFile = tempnam(_PS_TMP_IMG_DIR_, "ecomzone_");
try {
if (!copy($imageUrl, $tmpFile)) {
throw new Exception("Failed to download image");
}
$image = new Image();
$image->id_product = $product->id;
$image->position = Image::getHighestPosition($product->id) + 1;
$image->cover = $cover;
if (!$image->add()) {
throw new Exception("Failed to create image record");
}
$imagesTypes = ImageType::getImagesTypes("products");
foreach ($imagesTypes as $type) {
ImageManager::resize(
$tmpFile,
_PS_PROD_IMG_DIR_ .
$image->getExistingImgPath() .
"." .
$type["name"] .
".jpg",
$type["width"],
$type["height"]
);
}
// Save original image
ImageManager::resize(
$tmpFile,
_PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . ".jpg"
);
} catch (Exception $e) {
EcomZoneLogger::log("Image import failed", "ERROR", [
"url" => $imageUrl,
"error" => $e->getMessage(),
]);
} finally {
if (file_exists($tmpFile)) {
unlink($tmpFile);
}
}
}
private function updateSpecifications($product, $data)
{
if (empty($data["specifications"])) {
return;
}
try {
$specifications = json_decode($data["specifications"], true);
if (!is_array($specifications)) {
return;
}
foreach ($specifications as $name => $value) {
if (empty($value)) {
continue;
}
// Get or create feature
$featureId = Feature::getIdByName($name);
if (!$featureId) {
$feature = new Feature();
$feature->name[$this->defaultLangId] = $name;
if (!$feature->add()) {
continue;
}
$featureId = $feature->id;
}
// Get or create feature value
$featureValue = new FeatureValue();
$featureValue->id_feature = $featureId;
$featureValue->value[$this->defaultLangId] = $value;
if (!$featureValue->add()) {
continue;
}
// Associate with product
$product->addFeaturesToDB([$featureId => $featureValue->id]);
}
} catch (Exception $e) {
EcomZoneLogger::log("Failed to update specifications", "ERROR", [
"sku" => $product->reference,
"error" => $e->getMessage(),
]);
}
}
private function updateStock($product, $data)
{
try {
$quantity = (int) ($data["stock_quantity"] ?? $data["stock"] ?? 0);
StockAvailable::setQuantity($product->id, 0, $quantity);
// Update out of stock behavior
StockAvailable::setProductOutOfStock(
$product->id,
$quantity > 0 ? 2 : 0, // 2 = Allow orders, 0 = Deny orders
$this->defaultShopId
);
} catch (Exception $e) {
throw new EcomZoneException(
"Failed to update stock: " . $e->getMessage(),
EcomZoneException::INVALID_PRODUCT_DATA,
$e
);
}
}
}
+684 -232
View File
@@ -1,29 +1,558 @@
<?php
if (!defined("_PS_VERSION_")) {
exit();
}
class EcomZoneProductSync
class EcomZoneProductSync implements IProductSync
{
private $client;
private EcomZoneClient $client;
private EcomZoneCategoryHandler $categoryHandler;
private int $defaultLangId;
private int $defaultShopId;
public function __construct()
{
$this->client = new EcomZoneClient();
$this->categoryHandler = new EcomZoneCategoryHandler();
$this->defaultLangId = (int) Configuration::get("PS_LANG_DEFAULT");
$this->defaultShopId = (int) Context::getContext()->shop->id;
EcomZoneLogger::log("Product Sync initialized", "INFO");
}
private function resizeImage($src, $dest, $width, $height)
public function importProducts(int $perPage = 100): array
{
list($srcWidth, $srcHeight, $type) = getimagesize($src);
$srcImage = $this->createImageFromType($src, $type);
$destImage = imagecreatetruecolor($width, $height);
imagecopyresampled($destImage, $srcImage, 0, 0, 0, 0, $width, $height, $srcWidth, $srcHeight);
$this->saveImageFromType($destImage, $dest, $type);
imagedestroy($srcImage);
imagedestroy($destImage);
$page = 1;
$totalImported = 0;
$totalAvailable = 0;
$errors = [];
try {
EcomZoneLogger::log("Starting product import", "INFO", [
"per_page" => $perPage,
]);
do {
$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(),
]);
}
}
$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
);
$this->clearCache();
return [
"success" => true,
"imported" => $totalImported,
"total" => $totalAvailable,
"errors" => $errors,
];
} catch (Exception $e) {
EcomZoneLogger::log("Product import process failed", "ERROR", [
"error" => $e->getMessage(),
]);
throw $e;
}
}
private function createImageFromType($filename, $type)
public function importSingleProduct(array $productData): bool
{
try {
// Get SKU from catalog data
$sku = $productData["sku"] ?? $productData["data"]["sku"] ?? null;
if (!$sku) {
throw new EcomZoneException("Missing SKU in product data");
}
// Fetch complete product data from product endpoint
$fullProductData = $this->client->getProduct($sku);
if (!isset($fullProductData["data"])) {
throw new EcomZoneException("Invalid product response");
}
$data = $fullProductData["data"];
EcomZoneLogger::log("Processing product", "INFO", [
"sku" => $sku,
"categories" => $data["categories"] ?? null,
"google_categories" => $data["google_categories"] ?? null
]);
$this->validateProductData($data);
// Start transaction
Db::getInstance()->execute("START TRANSACTION");
$product = $this->getOrCreateProduct($sku);
// Update product data
$this->updateProductData($product, $data);
$this->updateProductCategories($product, $data);
$this->updateProductStock($product, $data);
$this->processProductImages($product, $data);
// Commit transaction
Db::getInstance()->execute("COMMIT");
return true;
} catch (Exception $e) {
Db::getInstance()->execute("ROLLBACK");
EcomZoneLogger::log("Product import failed", "ERROR", [
"sku" => $sku ?? "unknown",
"error" => $e->getMessage()
]);
return false;
}
}
private function validateProductData(array $data): void
{
$requiredFields = [
'sku',
'product_name',
'product_price',
'description',
'stock'
];
foreach ($requiredFields as $field) {
if (empty($data[$field])) {
throw new EcomZoneException(
"Missing required field: {$field}",
EcomZoneException::VALIDATION_ERROR
);
}
}
}
private function getOrCreateProduct(string $sku): Product
{
try {
$productId = Product::getIdByReference($sku);
if ($productId) {
$product = new Product($productId);
if (!Validate::isLoadedObject($product)) {
throw new Exception("Invalid product object");
}
return $product;
}
$product = new Product();
$product->reference = $sku;
return $product;
} catch (Exception $e) {
throw new EcomZoneException(
"Failed to get/create product: " . $e->getMessage(),
EcomZoneException::INVALID_PRODUCT_DATA,
$e
);
}
}
private function updateProductData(Product $product, array $data): void
{
try {
// Basic information
$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);
$product->reference = $data['sku'];
$product->ean13 = $data['ean'] ?? '';
// Shop configuration
$product->id_shop_default = $this->defaultShopId;
$product->active = true;
$product->visibility = 'both';
$product->available_for_order = true;
$product->show_price = true;
$product->indexed = 1;
// Update dimensions if available
if (!empty($data['dimensions'])) {
$dimensions = json_decode($data['dimensions'], true);
if (is_array($dimensions)) {
$product->weight = (float)($dimensions['weight'] ?? 0);
$product->height = (float)($dimensions['height'] ?? 0);
$product->width = (float)($dimensions['width'] ?? 0);
$product->depth = (float)($dimensions['length'] ?? 0);
}
}
// Set tax rules if configured
$taxRulesGroupId = (int)Configuration::get('PS_TAX_RULES_GROUP_DEFAULT');
if ($taxRulesGroupId) {
$product->id_tax_rules_group = $taxRulesGroupId;
}
// Save the product
if (!$product->id) {
if (!$product->add()) {
throw new EcomZoneException('Failed to create product');
}
} else {
if (!$product->update()) {
throw new EcomZoneException('Failed to update product');
}
}
// Update shop association
if (Shop::isFeatureActive()) {
Shop::setContext(Shop::CONTEXT_SHOP, $this->defaultShopId);
$product->id_shop_list = [$this->defaultShopId];
$product->id_shop_default = $this->defaultShopId;
// Associate product with shop
Db::getInstance()->execute('
INSERT IGNORE INTO '._DB_PREFIX_.'product_shop
(id_product, id_shop, price, wholesale_price)
VALUES
('.(int)$product->id.', '.(int)$this->defaultShopId.',
'.(float)$product->price.', '.(float)$product->wholesale_price.')
');
}
// Process specifications if available
if (!empty($data['specifications'])) {
$this->updateProductFeatures($product, $data['specifications']);
}
} catch (Exception $e) {
throw new EcomZoneException(
'Failed to update product data: ' . $e->getMessage(),
EcomZoneException::INVALID_PRODUCT_DATA,
$e
);
}
}
private function updateProductCategories(Product $product, array $data): void
{
try {
$categoryIds = [];
// Process main categories
if (!empty($data['categories'])) {
$categories = json_decode($data['categories'], true);
if (is_array($categories)) {
foreach ($categories as $categoryPath) {
// Skip special categories that start with '0'
if (strpos($categoryPath, '0') === 0) {
continue;
}
// Create each category as a single path
$categoryId = $this->categoryHandler->createCategoryPath([$categoryPath]);
if ($categoryId) {
$categoryIds[] = $categoryId;
}
}
}
}
// Process Google categories
if (!empty($data['google_categories'])) {
$googleCategories = json_decode($data['google_categories'], true);
if (is_array($googleCategories)) {
$categoryId = $this->categoryHandler->createCategoryPath($googleCategories);
if ($categoryId) {
$categoryIds[] = $categoryId;
}
}
}
// Ensure we have at least the default category
if (empty($categoryIds)) {
$categoryIds[] = (int)Configuration::get('PS_HOME_CATEGORY');
}
// Remove duplicates
$categoryIds = array_unique($categoryIds);
// Set default category
$product->id_category_default = reset($categoryIds);
// Save the product to ensure it has an ID
if (!$product->id) {
$product->add();
} else {
$product->update();
}
// Clear existing category associations
Db::getInstance()->execute('
DELETE FROM `'._DB_PREFIX_.'category_product`
WHERE id_product = '.(int)$product->id
);
// Insert new category associations
foreach ($categoryIds as $categoryId) {
Db::getInstance()->execute('
INSERT INTO `'._DB_PREFIX_.'category_product`
(id_category, id_product, position)
VALUES
('.(int)$categoryId.', '.(int)$product->id.', 0)
');
}
// Update shop associations if needed
if (Shop::isFeatureActive()) {
$product->id_shop_list = [$this->defaultShopId];
$product->id_shop_default = $this->defaultShopId;
}
EcomZoneLogger::log('Categories updated', 'DEBUG', [
'product' => $product->reference,
'category_ids' => $categoryIds
]);
} catch (Exception $e) {
throw new EcomZoneException(
'Failed to update product categories: ' . $e->getMessage(),
EcomZoneException::CATEGORY_CREATE_ERROR,
$e
);
}
}
private function updateProductStock(Product $product, array $data): void
{
try {
$quantity = (int) ($data['stock_quantity'] ?? 0);
StockAvailable::setQuantity($product->id, 0, $quantity);
// Update out of stock behavior
StockAvailable::setProductOutOfStock(
$product->id,
$quantity > 0 ? 2 : 0, // 2 = Allow orders, 0 = Deny orders
$this->defaultShopId
);
} catch (Exception $e) {
EcomZoneLogger::log("Failed to update stock", "WARNING", [
"product" => $product->reference,
"error" => $e->getMessage()
]);
}
}
private function processProductImages(Product $product, array $data): void
{
try {
$imageUrls = [];
// 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);
}
}
if (empty($imageUrls)) {
return;
}
// Delete existing images if any
$product->deleteImages();
foreach ($imageUrls as $index => $imageUrl) {
$this->importProductImage($product, $imageUrl, $index === 0);
}
} catch (Exception $e) {
EcomZoneLogger::log("Failed to process product images", "WARNING", [
"sku" => $product->reference,
"error" => $e->getMessage(),
]);
}
}
private function importProductImage(Product $product, string $imageUrl, bool $cover = false): void
{
try {
// Create temporary file
$tempFile = tempnam(_PS_TMP_IMG_DIR_, 'import_');
// Download image
if (!copy($imageUrl, $tempFile)) {
throw new Exception("Failed to download image: " . $imageUrl);
}
// Create image object
$image = new Image();
$image->id_product = $product->id;
$image->position = Image::getHighestPosition($product->id) + 1;
$image->cover = $cover;
if (!$image->add()) {
throw new Exception("Failed to create image record");
}
// Create product image directory if it doesn't exist
$imagePath = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath();
$imageDir = dirname($imagePath);
if (!file_exists($imageDir)) {
mkdir($imageDir, 0755, true);
}
// Generate image
if (!ImageManager::resize(
$tempFile,
$imagePath . '.jpg'
)) {
throw new Exception("Failed to process image");
}
// Generate thumbnails
$this->generateThumbnails($image);
// Clean up
unlink($tempFile);
} catch (Exception $e) {
EcomZoneLogger::log("Failed to import product image", "ERROR", [
"product" => $product->reference,
"image_url" => $imageUrl,
"error" => $e->getMessage()
]);
}
}
private function generateThumbnails(Image $image): void
{
try {
$imageTypes = ImageType::getImagesTypes('products');
foreach ($imageTypes as $imageType) {
$dir = _PS_PROD_IMG_DIR_ . $image->getExistingImgPath();
if (!file_exists($dir)) {
mkdir($dir, 0755, true);
}
ImageManager::resize(
_PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '.jpg',
_PS_PROD_IMG_DIR_ . $image->getExistingImgPath() . '-' . stripslashes($imageType['name']) . '.jpg',
(int)$imageType['width'],
(int)$imageType['height']
);
}
} 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:
@@ -36,236 +565,159 @@ class EcomZoneProductSync
throw new Exception("Unsupported image type: " . $type);
}
}
private function saveImageFromType($image, $filename, $type)
private function createDirectoryIfNotExists(string $directory): void
{
switch ($type) {
case IMAGETYPE_JPEG:
$this->createDirectoryIfNotExists(dirname($filename));
imagejpeg($image, $filename);
break;
case IMAGETYPE_PNG:
$this->createDirectoryIfNotExists(dirname($filename));
imagepng($image, $filename);
break;
case IMAGETYPE_GIF:
$this->createDirectoryIfNotExists(dirname($filename));
imagegif($image, $filename);
break;
case IMAGETYPE_GIF:
imagegif($image, $filename);
break;
default:
throw new Exception("Unsupported image type: " . $type);
}
}
public function importProducts($perPage = 100)
{
$page = 1;
$totalImported = 0;
$totalAvailable = 0;
EcomZoneLogger::log("Starting product import - perPage: $perPage");
EcomZoneLogger::log("Number of products to be imported per page: $perPage", 'INFO');
try {
do {
$catalog = $this->client->getCatalog($page, $perPage);
$importedCount = 0;
foreach ($catalog['data'] as $product) {
if ($this->importSingleProduct($product)) {
$importedCount++;
}
}
$totalImported += $importedCount;
$totalAvailable = $catalog['total'];
$page++;
EcomZoneLogger::log("Imported page $page", 'INFO', [
'total_imported' => $totalImported,
'page' => $page,
'total_available' => $totalAvailable
]);
} while ($page <= ceil($totalAvailable / $perPage) && isset($catalog['next_page_url']) && $catalog['next_page_url'] !== null);
EcomZoneLogger::log("Finished importing products", 'INFO', [
'total_imported' => $totalImported,
'total_available' => $totalAvailable
]);
// Clear cache
Tools::clearSmartyCache();
Tools::clearXMLCache();
Media::clearCache();
PrestaShopAutoload::getInstance()->generateIndex();
return [
'success' => true,
'imported' => $totalImported,
'total' => $totalAvailable
];
} catch (Exception $e) {
EcomZoneLogger::log("Error importing products: " . $e->getMessage(), 'ERROR');
throw $e;
}
}
private function importSingleProduct($productData)
{
// Extract data from nested structure
$data = isset($productData['data']) ? $productData['data'] : $productData;
if (!isset($data['sku']) || !isset($data['product_name']) || !isset($data['description']) || !isset($data['product_price'])) {
EcomZoneLogger::log("Invalid product data", 'ERROR', ['data' => $productData]);
return false;
}
try {
// Check if product already exists by reference
$productId = Db::getInstance()->getValue('
SELECT id_product
FROM ' . _DB_PREFIX_ . 'product
WHERE reference = "' . pSQL($data['sku']) . '"
');
$product = $productId ? new Product($productId) : new Product();
$defaultLangId = (int)Configuration::get('PS_LANG_DEFAULT');
$product->reference = $data['sku'];
$product->name[$defaultLangId] = $data['product_name'];
$product->description[$defaultLangId] = $data['long_description'] ?? $data['description'];
$product->description_short[$defaultLangId] = $data['description'];
$product->price = $data['product_price'];
$product->active = true;
$product->quantity = (int)$data['stock'];
$homeCategoryId = (int)Configuration::get('PS_HOME_CATEGORY');
$product->id_category_default = $homeCategoryId;
$product->addToCategories([$homeCategoryId]);
$product->visibility = 'both'; // Ensure product is visible in both catalog and search
$product->active = true;
$product->quantity = (int)$data['stock'];
$homeCategoryId = (int)Configuration::get('PS_HOME_CATEGORY');
$product->id_category_default = $homeCategoryId;
$product->addToCategories([$homeCategoryId]);
// Save product first to get ID
if (!$product->id) {
$product->add();
} else {
$product->update();
if (!is_dir($directory)) {
if (!mkdir($directory, 0755, true)) {
throw new Exception(
"Failed to create directory: " . $directory
);
}
StockAvailable::setQuantity($product->id, 0, (int)$data['stock']);
$product->available_for_order = true; // Ensure product is available for order
$product->show_price = true; // Ensure price is shown
}
}
// Handle image import if URL is provided
if (isset($data['image']) && !empty($data['image'])) {
$this->importProductImage($product, $data['image']);
private function updateProductFeatures(Product $product, string $specificationsJson): void
{
try {
$specifications = json_decode($specificationsJson, true);
if (!is_array($specifications)) {
return;
}
StockAvailable::setQuantity($product->id, 0, (int)$data['stock']);
EcomZoneLogger::log("Imported product", 'INFO', [
'sku' => $data['sku'],
'id' => $product->id,
'name' => $data['product_name']
foreach ($specifications as $featureName => $featureValue) {
if (empty($featureValue)) {
continue;
}
// Convert array or object to string if necessary
if (is_array($featureValue) || is_object($featureValue)) {
$featureValue = json_encode($featureValue);
}
// Ensure we have a string value
$featureValue = (string)$featureValue;
// Get or create feature using DB query
$featureId = $this->getOrCreateFeature($featureName);
if (!$featureId) {
continue;
}
// Get or create feature value
$featureValueId = $this->getOrCreateFeatureValue($featureId, $featureValue);
if (!$featureValueId) {
continue;
}
// Associate feature with product
$product->addFeaturesToDB(
[$featureId => $featureValueId],
$this->defaultLangId
);
}
EcomZoneLogger::log("Features updated successfully", "DEBUG", [
"product" => $product->reference,
"specifications" => $specifications
]);
return true;
} catch (Exception $e) {
EcomZoneLogger::log("Error importing product", 'ERROR', [
'sku' => $data['sku'],
'error' => $e->getMessage()
]);
return false;
EcomZoneLogger::log(
"Failed to update product features",
"WARNING",
[
"product" => $product->reference,
"error" => $e->getMessage(),
]
);
}
}
private function getOrCreateFeature(string $name): ?int
{
// Try to find existing feature
$featureId = (int)Db::getInstance()->getValue(
'SELECT f.id_feature
FROM `'._DB_PREFIX_.'feature_lang` fl
JOIN `'._DB_PREFIX_.'feature` f ON f.id_feature = fl.id_feature
WHERE fl.name = "'.pSQL($name).'"
AND fl.id_lang = '.(int)$this->defaultLangId
);
private function importProductImage($product, $imageUrl)
{
try {
// Create temporary file
$tmpFile = tempnam(_PS_TMP_IMG_DIR_, 'ecomzone_');
// Download image
if (!copy($imageUrl, $tmpFile)) {
throw new Exception("Failed to download image from: " . $imageUrl);
}
// Get image info
$imageInfo = getimagesize($tmpFile);
if (!$imageInfo) {
unlink($tmpFile);
throw new Exception("Invalid image file");
}
// Validate image dimensions and file size
if ($imageInfo[0] > 2000 || $imageInfo[1] > 2000 || filesize($tmpFile) > 5000000) {
unlink($tmpFile);
throw new Exception("Image dimensions or file size exceed limits");
}
// Generate unique name
$imageName = $product->reference . '-' . time() . '.' . pathinfo($imageUrl, PATHINFO_EXTENSION);
// Delete existing images if any
$product->deleteImages();
// Add new image
$image = new Image();
$image->id_product = $product->id;
$image->position = 1;
$image->cover = true;
// Save the image to the correct directory
$imagePath = _PS_PROD_IMG_DIR_ . $image->getImgPath() . '.' . $image->image_format;
$this->createDirectoryIfNotExists(dirname($imagePath));
if (!copy($tmpFile, $imagePath)) {
unlink($tmpFile);
throw new Exception("Failed to save image to: " . $imagePath);
if ($featureId) {
return $featureId;
}
// Associate the image with the product
if (!$image->add()) {
unlink($tmpFile);
throw new Exception("Failed to add image to product");
// Create new feature
$feature = new Feature();
$feature->name = array_fill_keys(Language::getIDs(), $name);
return $feature->add() ? (int)$feature->id : null;
}
private function getOrCreateFeatureValue(int $featureId, string $value): ?int
{
// Try to find existing feature value
$valueId = (int)Db::getInstance()->getValue(
'SELECT fv.id_feature_value
FROM `'._DB_PREFIX_.'feature_value_lang` fvl
JOIN `'._DB_PREFIX_.'feature_value` fv ON fv.id_feature_value = fvl.id_feature_value
WHERE fv.id_feature = '.(int)$featureId.'
AND fvl.value = "'.pSQL($value).'"
AND fvl.id_lang = '.(int)$this->defaultLangId
);
if ($valueId) {
return $valueId;
}
// Manually resize the image and generate thumbnails
$this->resizeImage($imagePath, _PS_PROD_IMG_DIR_ . $image->getImgPath() . '-home_default.' . $image->image_format, 250, 250);
$this->resizeImage($imagePath, _PS_PROD_IMG_DIR_ . $image->getImgPath() . '-large_default.' . $image->image_format, 800, 800);
// Cleanup
unlink($tmpFile);
EcomZoneLogger::log("Imported product image", 'INFO', [
'sku' => $product->reference,
'image' => $imageUrl
]);
// Create new feature value
$featureValue = new FeatureValue();
$featureValue->id_feature = $featureId;
$featureValue->value = array_fill_keys(Language::getIDs(), $value);
} catch (Exception $e) {
EcomZoneLogger::log("Error importing product image", 'ERROR', [
'sku' => $product->reference,
'image' => $imageUrl,
'error' => $e->getMessage()
]);
return $featureValue->add() ? (int)$featureValue->id : null;
}
private function updateProductDimensions(
Product $product,
string $dimensionsJson
): void {
try {
$dimensions = json_decode($dimensionsJson, true);
if (!is_array($dimensions)) {
return;
}
$product->weight = (float) ($dimensions["weight"] ?? 0);
$product->height = (float) ($dimensions["height"] ?? 0);
$product->width = (float) ($dimensions["width"] ?? 0);
$product->depth = (float) ($dimensions["length"] ?? 0);
$product->update();
} catch (Exception $e) {
EcomZoneLogger::log(
"Failed to update product dimensions",
"WARNING",
[
"product" => $product->reference,
"error" => $e->getMessage(),
]
);
}
}
private function clearCache(): void
{
Tools::clearAllCache();
Tools::clearSmartyCache();
Tools::clearXMLCache();
Media::clearCache();
PrestaShopAutoload::getInstance()->generateIndex();
}
}
private function createDirectoryIfNotExists($directory)
{
if (!is_dir($directory)) {
mkdir($directory, 0755, true);
}
}
}
$token = Configuration::get('ECOMZONE_CRON_TOKEN');
echo $token;
+11
View File
@@ -0,0 +1,11 @@
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
interface ICategory
{
public function processCategoriesFromString(string $categories): array;
public function createCategory(string $name, ?int $parentId = null): int;
public function getCategoryIdByName(string $name, ?int $parentId = null): ?int;
}
@@ -0,0 +1,10 @@
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
interface IProductSync
{
public function importProducts(int $perPage = 100): array;
public function importSingleProduct(array $productData): bool;
}