- cron.php: przywrocono E_WARNING i E_DEPRECATED (wyciszono tylko E_NOTICE i E_STRICT) - IntegrationsRepository: try-catch po zapisie tokenow Apilo - blad DB nie sklada false po cichu - ProductRepository/ArticleRepository: error_log gdy safeUnlink wykryje sciezke poza upload/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3596 lines
140 KiB
PHP
3596 lines
140 KiB
PHP
<?php
|
||
namespace Domain\Product;
|
||
|
||
/**
|
||
* Repository odpowiedzialny za dostęp do danych produktów
|
||
*
|
||
* Zgodnie z wzorcem Repository Pattern, ta klasa enkapsuluje
|
||
* logikę dostępu do bazy danych dla produktów.
|
||
*/
|
||
class ProductRepository
|
||
{
|
||
private const MAX_PER_PAGE = 100;
|
||
|
||
/**
|
||
* @var \medoo Instancja Medoo ORM
|
||
*/
|
||
private $db;
|
||
|
||
/**
|
||
* Konstruktor - przyjmuje instancję bazy danych
|
||
*
|
||
* @param \medoo $db Instancja Medoo ORM
|
||
*/
|
||
public function __construct($db)
|
||
{
|
||
$this->db = $db;
|
||
}
|
||
|
||
/**
|
||
* Pobiera stan magazynowy produktu
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return int|null Ilość produktu lub null jeśli nie znaleziono
|
||
*/
|
||
public function getQuantity(int $productId): ?int
|
||
{
|
||
$quantity = $this->db->get('pp_shop_products', 'quantity', ['id' => $productId]);
|
||
|
||
// Medoo zwraca false jeśli nie znaleziono
|
||
return $quantity !== false ? (int)$quantity : null;
|
||
}
|
||
|
||
/**
|
||
* Pobiera produkt po ID
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return array|null Dane produktu lub null
|
||
*/
|
||
public function find(int $productId): ?array
|
||
{
|
||
$product = $this->db->get('pp_shop_products', '*', ['id' => $productId]);
|
||
return $product ?: null;
|
||
}
|
||
|
||
/**
|
||
* Zwraca liste produktow z archiwum do panelu admin.
|
||
*
|
||
* @return array{items: array<int, array<string, mixed>>, total: int}
|
||
*/
|
||
public function listArchivedForAdmin(
|
||
array $filters,
|
||
string $sortColumn = 'id',
|
||
string $sortDir = 'DESC',
|
||
int $page = 1,
|
||
int $perPage = 10
|
||
): array {
|
||
$allowedSortColumns = [
|
||
'id' => 'psp.id',
|
||
'name' => 'name',
|
||
'price_brutto' => 'psp.price_brutto',
|
||
'price_brutto_promo' => 'psp.price_brutto_promo',
|
||
'quantity' => 'psp.quantity',
|
||
'combinations' => 'combinations',
|
||
];
|
||
|
||
$sortSql = $allowedSortColumns[$sortColumn] ?? 'psp.id';
|
||
$sortDir = strtoupper(trim($sortDir)) === 'ASC' ? 'ASC' : 'DESC';
|
||
$page = max(1, $page);
|
||
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
|
||
$offset = ($page - 1) * $perPage;
|
||
|
||
$where = ['psp.archive = 1', 'psp.parent_id IS NULL'];
|
||
$params = [];
|
||
|
||
$phrase = trim((string)($filters['phrase'] ?? ''));
|
||
if (strlen($phrase) > 255) {
|
||
$phrase = substr($phrase, 0, 255);
|
||
}
|
||
|
||
if ($phrase !== '') {
|
||
$where[] = '(
|
||
psp.ean LIKE :phrase
|
||
OR psp.sku LIKE :phrase
|
||
OR EXISTS (
|
||
SELECT 1
|
||
FROM pp_shop_products_langs AS pspl2
|
||
WHERE pspl2.product_id = psp.id
|
||
AND pspl2.name LIKE :phrase
|
||
)
|
||
)';
|
||
$params[':phrase'] = '%' . $phrase . '%';
|
||
}
|
||
|
||
$whereSql = implode(' AND ', $where);
|
||
|
||
$sqlCount = "
|
||
SELECT COUNT(0)
|
||
FROM pp_shop_products AS psp
|
||
WHERE {$whereSql}
|
||
";
|
||
|
||
$stmtCount = $this->db->query($sqlCount, $params);
|
||
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
|
||
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
|
||
|
||
$sql = "
|
||
SELECT
|
||
psp.id,
|
||
psp.price_brutto,
|
||
psp.price_brutto_promo,
|
||
psp.quantity,
|
||
psp.sku,
|
||
psp.ean,
|
||
(
|
||
SELECT pspl.name
|
||
FROM pp_shop_products_langs AS pspl
|
||
INNER JOIN pp_langs AS pl ON pl.id = pspl.lang_id
|
||
WHERE pspl.product_id = psp.id
|
||
AND pspl.name <> ''
|
||
ORDER BY pl.o ASC
|
||
LIMIT 1
|
||
) AS name,
|
||
(
|
||
SELECT pspi.src
|
||
FROM pp_shop_products_images AS pspi
|
||
WHERE pspi.product_id = psp.id
|
||
ORDER BY pspi.o ASC, pspi.id ASC
|
||
LIMIT 1
|
||
) AS image_src,
|
||
(
|
||
SELECT pspi.alt
|
||
FROM pp_shop_products_images AS pspi
|
||
WHERE pspi.product_id = psp.id
|
||
ORDER BY pspi.o ASC, pspi.id ASC
|
||
LIMIT 1
|
||
) AS image_alt,
|
||
(
|
||
SELECT COUNT(0)
|
||
FROM pp_shop_products AS pspc
|
||
WHERE pspc.parent_id = psp.id
|
||
) AS combinations
|
||
FROM pp_shop_products AS psp
|
||
WHERE {$whereSql}
|
||
ORDER BY {$sortSql} {$sortDir}, psp.id {$sortDir}
|
||
LIMIT {$perPage} OFFSET {$offset}
|
||
";
|
||
|
||
$stmt = $this->db->query($sql, $params);
|
||
$items = $stmt ? $stmt->fetchAll() : [];
|
||
|
||
return [
|
||
'items' => is_array($items) ? $items : [],
|
||
'total' => $total,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Pobiera cenę produktu (promocyjną jeśli jest niższa, w przeciwnym razie regularną)
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return float|null Cena brutto lub null jeśli nie znaleziono
|
||
*/
|
||
public function getPrice(int $productId): ?float
|
||
{
|
||
$prices = $this->db->get('pp_shop_products', ['price_brutto', 'price_brutto_promo'], ['id' => $productId]);
|
||
|
||
if (!$prices) {
|
||
return null;
|
||
}
|
||
|
||
if ($prices['price_brutto_promo'] != '' && $prices['price_brutto_promo'] < $prices['price_brutto']) {
|
||
return (float)$prices['price_brutto_promo'];
|
||
}
|
||
|
||
return (float)$prices['price_brutto'];
|
||
}
|
||
|
||
/**
|
||
* Pobiera nazwę produktu w danym języku
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param string $langId ID języka
|
||
* @return string|null Nazwa produktu lub null jeśli nie znaleziono
|
||
*/
|
||
public function getName(int $productId, string $langId): ?string
|
||
{
|
||
$name = $this->db->get('pp_shop_products_langs', 'name', ['AND' => ['product_id' => $productId, 'lang_id' => $langId]]);
|
||
|
||
return $name ?: null;
|
||
}
|
||
|
||
/**
|
||
* Aktualizuje ilość produktu
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param int $quantity Nowa ilość
|
||
* @return bool Czy aktualizacja się powiodła
|
||
*/
|
||
public function updateQuantity(int $productId, int $quantity): bool
|
||
{
|
||
$result = $this->db->update(
|
||
'pp_shop_products',
|
||
['quantity' => $quantity],
|
||
['id' => $productId]
|
||
);
|
||
|
||
return $result !== false;
|
||
}
|
||
|
||
/**
|
||
* Przywraca produkt z archiwum (wraz z kombinacjami)
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return bool Czy operacja się powiodła
|
||
*/
|
||
public function unarchive(int $productId): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [ 'status' => 1, 'archive' => 0 ], [ 'id' => $productId ] );
|
||
$this->db->update( 'pp_shop_products', [ 'status' => 1, 'archive' => 0 ], [ 'parent_id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Przenosi produkt do archiwum (wraz z kombinacjami)
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return bool Czy operacja się powiodła
|
||
*/
|
||
public function archive(int $productId): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [ 'status' => 0, 'archive' => 1 ], [ 'id' => $productId ] );
|
||
$this->db->update( 'pp_shop_products', [ 'status' => 0, 'archive' => 1 ], [ 'parent_id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Pobiera listę wszystkich produktów głównych (id => name) do masowej edycji.
|
||
* Zwraca tylko produkty bez parent_id (bez kombinacji).
|
||
*
|
||
* @return array<int, string> Mapa id => nazwa produktu
|
||
*/
|
||
public function allProductsForMassEdit(): array
|
||
{
|
||
$defaultLang = $this->db->get( 'pp_langs', 'id', [ 'start' => 1 ] );
|
||
if ( !$defaultLang ) {
|
||
$defaultLang = 'pl';
|
||
}
|
||
|
||
$results = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => null ] );
|
||
$products = [];
|
||
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $id ) {
|
||
$name = $this->db->get( 'pp_shop_products_langs', 'name', [
|
||
'AND' => [ 'product_id' => $id, 'lang_id' => $defaultLang ]
|
||
] );
|
||
$products[ (int) $id ] = $name ?: '';
|
||
}
|
||
}
|
||
|
||
return $products;
|
||
}
|
||
|
||
/**
|
||
* Pobiera listę ID produktów przypisanych do danej kategorii.
|
||
*
|
||
* @param int $categoryId ID kategorii
|
||
* @return int[] Lista ID produktów
|
||
*/
|
||
public function getProductsByCategory(int $categoryId): array
|
||
{
|
||
$results = $this->db->select(
|
||
'pp_shop_products_categories',
|
||
'product_id',
|
||
[ 'category_id' => $categoryId ]
|
||
);
|
||
|
||
return is_array( $results ) ? $results : [];
|
||
}
|
||
|
||
/**
|
||
* Aplikuje rabat procentowy na produkt (cena promocyjna = cena - X%).
|
||
* Aktualizuje również ceny kombinacji produktu.
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param float $discountPercent Procent rabatu
|
||
* @return array|null Tablica z price_brutto i price_brutto_promo lub null przy błędzie
|
||
*/
|
||
public function applyDiscountPercent(int $productId, float $discountPercent): ?array
|
||
{
|
||
$product = $this->db->get( 'pp_shop_products', [
|
||
'vat', 'price_brutto', 'price_netto'
|
||
], [ 'id' => $productId ] );
|
||
|
||
if ( !$product ) {
|
||
return null;
|
||
}
|
||
|
||
$vat = $product['vat'];
|
||
$priceBrutto = (float) $product['price_brutto'];
|
||
$priceNetto = (float) $product['price_netto'];
|
||
|
||
$priceBruttoPromo = $priceBrutto - ( $priceBrutto * ( $discountPercent / 100 ) );
|
||
$priceNettoPromo = $priceNetto - ( $priceNetto * ( $discountPercent / 100 ) );
|
||
|
||
if ( $priceBrutto == $priceBruttoPromo ) {
|
||
$priceBruttoPromo = null;
|
||
}
|
||
if ( $priceNetto == $priceNettoPromo ) {
|
||
$priceNettoPromo = null;
|
||
}
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_brutto_promo' => $priceBruttoPromo,
|
||
'price_netto_promo' => $priceNettoPromo
|
||
], [ 'id' => $productId ] );
|
||
|
||
$this->updateCombinationPrices( $productId, $priceNetto, $vat, $priceNettoPromo );
|
||
|
||
return [
|
||
'price_brutto' => $priceBrutto,
|
||
'price_brutto_promo' => $priceBruttoPromo
|
||
];
|
||
}
|
||
|
||
// ─── Krok 1: metody CRUD admin ───────────────────────────────────
|
||
|
||
/**
|
||
* Liczba produktów (nie-archiwum, parent_id IS NULL).
|
||
*/
|
||
public function countProducts(?array $where = null): int
|
||
{
|
||
if ( $where ) {
|
||
return (int) $this->db->count( 'pp_shop_products', $where );
|
||
}
|
||
|
||
return (int) $this->db->count( 'pp_shop_products', [ 'archive' => 0 ] );
|
||
}
|
||
|
||
/**
|
||
* Lista produktów do panelu admin (paginacja, sortowanie, filtrowanie).
|
||
*
|
||
* @return array{items: array, total: int}
|
||
*/
|
||
public function listForAdmin(
|
||
array $filters = [],
|
||
string $sortColumn = 'id',
|
||
string $sortDir = 'DESC',
|
||
int $page = 1,
|
||
int $perPage = 15
|
||
): array {
|
||
$page = max( 1, $page );
|
||
$perPage = min( max( 1, $perPage ), self::MAX_PER_PAGE );
|
||
$offset = ( $page - 1 ) * $perPage;
|
||
|
||
$allowedSort = [ 'id', 'name', 'price_brutto', 'status', 'promoted', 'quantity' ];
|
||
if ( !in_array( $sortColumn, $allowedSort, true ) ) {
|
||
$sortColumn = 'id';
|
||
}
|
||
$sortDir = strtoupper( $sortDir ) === 'ASC' ? 'ASC' : 'DESC';
|
||
|
||
$conditions = [ 'psp.archive = 0', 'psp.parent_id IS NULL' ];
|
||
$params = [];
|
||
|
||
$search = trim( (string) ( $filters['search'] ?? '' ) );
|
||
if ( $search !== '' ) {
|
||
$conditions[] = '( pspl.name LIKE :search OR psp.ean LIKE :search_ean OR psp.sku LIKE :search_sku )';
|
||
$params[':search'] = '%' . $search . '%';
|
||
$params[':search_ean'] = '%' . $search . '%';
|
||
$params[':search_sku'] = '%' . $search . '%';
|
||
}
|
||
|
||
$statusFilter = (string) ( $filters['status'] ?? '' );
|
||
if ( $statusFilter === '1' || $statusFilter === '0' ) {
|
||
$conditions[] = 'psp.status = :status';
|
||
$params[':status'] = (int) $statusFilter;
|
||
}
|
||
|
||
$promotedFilter = (string) ( $filters['promoted'] ?? '' );
|
||
if ( $promotedFilter === '1' || $promotedFilter === '0' ) {
|
||
$conditions[] = 'psp.promoted = :promoted';
|
||
$params[':promoted'] = (int) $promotedFilter;
|
||
}
|
||
|
||
$whereSql = implode( ' AND ', $conditions );
|
||
$needsJoin = $search !== '' || $sortColumn === 'name';
|
||
|
||
$joinSql = $needsJoin
|
||
? 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = psp.id AND pspl.lang_id = ' . $this->db->quote( $this->defaultLangId() )
|
||
: '';
|
||
|
||
$orderColumn = $sortColumn === 'name' ? 'pspl.name' : 'psp.' . $sortColumn;
|
||
|
||
$countStmt = $this->db->query(
|
||
'SELECT COUNT( DISTINCT psp.id ) AS total '
|
||
. 'FROM pp_shop_products AS psp '
|
||
. $joinSql . ' '
|
||
. 'WHERE ' . $whereSql,
|
||
$params
|
||
);
|
||
$countRow = $countStmt ? $countStmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
|
||
$total = isset( $countRow[0]['total'] ) ? (int) $countRow[0]['total'] : 0;
|
||
|
||
$stmt = $this->db->query(
|
||
'SELECT DISTINCT psp.id '
|
||
. 'FROM pp_shop_products AS psp '
|
||
. $joinSql . ' '
|
||
. 'WHERE ' . $whereSql
|
||
. ' ORDER BY ' . $orderColumn . ' ' . $sortDir . ', psp.id DESC'
|
||
. ' LIMIT ' . $offset . ', ' . $perPage,
|
||
$params
|
||
);
|
||
|
||
$rows = $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
|
||
$items = [];
|
||
|
||
foreach ( $rows as $row ) {
|
||
$id = (int) ( $row['id'] ?? 0 );
|
||
if ( $id > 0 ) {
|
||
$product = $this->findForAdmin( $id );
|
||
if ( $product ) {
|
||
$items[] = $product;
|
||
}
|
||
}
|
||
}
|
||
|
||
return [
|
||
'items' => $items,
|
||
'total' => $total,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Lista produktów dla REST API z filtrowaniem, sortowaniem i paginacją.
|
||
*
|
||
* @param array $filters Filtry: search, status, promoted
|
||
* @param string $sortColumn Kolumna sortowania
|
||
* @param string $sortDir Kierunek: ASC|DESC
|
||
* @param int $page Numer strony
|
||
* @param int $perPage Wyników na stronę (max 100)
|
||
* @return array{items: array, total: int, page: int, per_page: int}
|
||
*/
|
||
public function listForApi(
|
||
array $filters = [],
|
||
string $sortColumn = 'id',
|
||
string $sortDir = 'DESC',
|
||
int $page = 1,
|
||
int $perPage = 50
|
||
): array {
|
||
$allowedSortColumns = [
|
||
'id' => 'psp.id',
|
||
'name' => 'name',
|
||
'price_brutto' => 'psp.price_brutto',
|
||
'status' => 'psp.status',
|
||
'promoted' => 'psp.promoted',
|
||
'quantity' => 'psp.quantity',
|
||
];
|
||
|
||
$sortSql = isset($allowedSortColumns[$sortColumn]) ? $allowedSortColumns[$sortColumn] : 'psp.id';
|
||
$sortDir = strtoupper(trim($sortDir)) === 'ASC' ? 'ASC' : 'DESC';
|
||
$page = max(1, $page);
|
||
$perPage = min(self::MAX_PER_PAGE, max(1, $perPage));
|
||
$offset = ($page - 1) * $perPage;
|
||
|
||
$where = ['psp.archive = 0', 'psp.parent_id IS NULL'];
|
||
$params = [];
|
||
|
||
$search = trim((string)($filters['search'] ?? ''));
|
||
if (strlen($search) > 255) {
|
||
$search = substr($search, 0, 255);
|
||
}
|
||
|
||
if ($search !== '') {
|
||
$where[] = '(
|
||
psp.ean LIKE :search
|
||
OR psp.sku LIKE :search
|
||
OR EXISTS (
|
||
SELECT 1
|
||
FROM pp_shop_products_langs AS pspl2
|
||
WHERE pspl2.product_id = psp.id
|
||
AND pspl2.name LIKE :search
|
||
)
|
||
)';
|
||
$params[':search'] = '%' . $search . '%';
|
||
}
|
||
|
||
$statusFilter = (string)($filters['status'] ?? '');
|
||
if ($statusFilter === '1' || $statusFilter === '0') {
|
||
$where[] = 'psp.status = :status';
|
||
$params[':status'] = (int)$statusFilter;
|
||
}
|
||
|
||
$promotedFilter = (string)($filters['promoted'] ?? '');
|
||
if ($promotedFilter === '1' || $promotedFilter === '0') {
|
||
$where[] = 'psp.promoted = :promoted';
|
||
$params[':promoted'] = (int)$promotedFilter;
|
||
}
|
||
|
||
// Attribute filters: attribute_{id} = {value_id}
|
||
$attrFilters = isset($filters['attributes']) && is_array($filters['attributes']) ? $filters['attributes'] : [];
|
||
$attrIdx = 0;
|
||
foreach ($attrFilters as $attrId => $valueId) {
|
||
$attrId = (int)$attrId;
|
||
$valueId = (int)$valueId;
|
||
if ($attrId <= 0 || $valueId <= 0) {
|
||
continue;
|
||
}
|
||
$paramAttr = ':attr_id_' . $attrIdx;
|
||
$paramVal = ':attr_val_' . $attrIdx;
|
||
$where[] = "EXISTS (
|
||
SELECT 1
|
||
FROM pp_shop_products AS psp_var{$attrIdx}
|
||
INNER JOIN pp_shop_products_attributes AS pspa{$attrIdx}
|
||
ON pspa{$attrIdx}.product_id = psp_var{$attrIdx}.id
|
||
WHERE psp_var{$attrIdx}.parent_id = psp.id
|
||
AND pspa{$attrIdx}.attribute_id = {$paramAttr}
|
||
AND pspa{$attrIdx}.value_id = {$paramVal}
|
||
)";
|
||
$params[$paramAttr] = $attrId;
|
||
$params[$paramVal] = $valueId;
|
||
$attrIdx++;
|
||
}
|
||
|
||
$whereSql = implode(' AND ', $where);
|
||
|
||
$sqlCount = "
|
||
SELECT COUNT(0)
|
||
FROM pp_shop_products AS psp
|
||
WHERE {$whereSql}
|
||
";
|
||
|
||
$stmtCount = $this->db->query($sqlCount, $params);
|
||
$countRows = $stmtCount ? $stmtCount->fetchAll() : [];
|
||
$total = isset($countRows[0][0]) ? (int)$countRows[0][0] : 0;
|
||
|
||
$sql = "
|
||
SELECT
|
||
psp.id,
|
||
psp.sku,
|
||
psp.ean,
|
||
psp.price_brutto,
|
||
psp.price_brutto_promo,
|
||
psp.price_netto,
|
||
psp.price_netto_promo,
|
||
psp.quantity,
|
||
psp.status,
|
||
psp.promoted,
|
||
psp.vat,
|
||
psp.weight,
|
||
psp.date_add,
|
||
psp.date_modify,
|
||
(
|
||
SELECT pspl.name
|
||
FROM pp_shop_products_langs AS pspl
|
||
INNER JOIN pp_langs AS pl ON pl.id = pspl.lang_id
|
||
WHERE pspl.product_id = psp.id
|
||
AND pspl.name <> ''
|
||
ORDER BY pl.o ASC
|
||
LIMIT 1
|
||
) AS name,
|
||
(
|
||
SELECT pspi.src
|
||
FROM pp_shop_products_images AS pspi
|
||
WHERE pspi.product_id = psp.id
|
||
ORDER BY pspi.o ASC, pspi.id ASC
|
||
LIMIT 1
|
||
) AS main_image
|
||
FROM pp_shop_products AS psp
|
||
WHERE {$whereSql}
|
||
ORDER BY {$sortSql} {$sortDir}, psp.id {$sortDir}
|
||
LIMIT {$perPage} OFFSET {$offset}
|
||
";
|
||
|
||
$stmt = $this->db->query($sql, $params);
|
||
$rows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||
|
||
$items = [];
|
||
if (is_array($rows)) {
|
||
foreach ($rows as $row) {
|
||
$items[] = [
|
||
'id' => (int)$row['id'],
|
||
'sku' => $row['sku'],
|
||
'ean' => $row['ean'],
|
||
'name' => $row['name'],
|
||
'price_brutto' => $row['price_brutto'] !== null ? (float)$row['price_brutto'] : null,
|
||
'price_brutto_promo' => $row['price_brutto_promo'] !== null ? (float)$row['price_brutto_promo'] : null,
|
||
'price_netto' => $row['price_netto'] !== null ? (float)$row['price_netto'] : null,
|
||
'price_netto_promo' => $row['price_netto_promo'] !== null ? (float)$row['price_netto_promo'] : null,
|
||
'quantity' => (int)$row['quantity'],
|
||
'status' => (int)$row['status'],
|
||
'promoted' => (int)$row['promoted'],
|
||
'vat' => (int)$row['vat'],
|
||
'weight' => $row['weight'] !== null ? (float)$row['weight'] : null,
|
||
'main_image' => $row['main_image'],
|
||
'date_add' => $row['date_add'],
|
||
'date_modify' => $row['date_modify'],
|
||
];
|
||
}
|
||
}
|
||
|
||
return [
|
||
'items' => $items,
|
||
'total' => $total,
|
||
'page' => $page,
|
||
'per_page' => $perPage,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Szczegóły produktu dla REST API.
|
||
*
|
||
* @param int $id ID produktu
|
||
* @return array|null Dane produktu lub null
|
||
*/
|
||
public function findForApi(int $id): ?array
|
||
{
|
||
$product = $this->db->get('pp_shop_products', '*', ['id' => $id]);
|
||
if (!$product) {
|
||
return null;
|
||
}
|
||
|
||
if (!empty($product['archive'])) {
|
||
return null;
|
||
}
|
||
|
||
$result = [
|
||
'id' => (int)$product['id'],
|
||
'sku' => $product['sku'],
|
||
'ean' => $product['ean'],
|
||
'price_brutto' => $product['price_brutto'] !== null ? (float)$product['price_brutto'] : null,
|
||
'price_brutto_promo' => $product['price_brutto_promo'] !== null ? (float)$product['price_brutto_promo'] : null,
|
||
'price_netto' => $product['price_netto'] !== null ? (float)$product['price_netto'] : null,
|
||
'price_netto_promo' => $product['price_netto_promo'] !== null ? (float)$product['price_netto_promo'] : null,
|
||
'quantity' => (int)$product['quantity'],
|
||
'status' => (int)$product['status'],
|
||
'promoted' => (int)$product['promoted'],
|
||
'vat' => (int)$product['vat'],
|
||
'weight' => $product['weight'] !== null ? (float)$product['weight'] : null,
|
||
'stock_0_buy' => (int)($product['stock_0_buy'] ?? 0),
|
||
'custom_label_0' => $product['custom_label_0'],
|
||
'custom_label_1' => $product['custom_label_1'],
|
||
'custom_label_2' => $product['custom_label_2'],
|
||
'custom_label_3' => $product['custom_label_3'],
|
||
'custom_label_4' => $product['custom_label_4'],
|
||
'new_to_date' => $product['new_to_date'],
|
||
'additional_message' => (int)($product['additional_message'] ?? 0),
|
||
'additional_message_required' => (int)($product['additional_message_required'] ?? 0),
|
||
'additional_message_text' => $product['additional_message_text'],
|
||
'set_id' => $product['set_id'] !== null ? (int)$product['set_id'] : null,
|
||
'product_unit_id' => $product['product_unit_id'] !== null ? (int)$product['product_unit_id'] : null,
|
||
'producer_id' => $product['producer_id'] !== null ? (int)$product['producer_id'] : null,
|
||
'producer_name' => $this->resolveProducerName($product['producer_id']),
|
||
'date_add' => $product['date_add'],
|
||
'date_modify' => $product['date_modify'],
|
||
];
|
||
|
||
// Languages
|
||
$langs = $this->db->select('pp_shop_products_langs', '*', ['product_id' => $id]);
|
||
$result['languages'] = [];
|
||
if (is_array($langs)) {
|
||
foreach ($langs as $lang) {
|
||
$result['languages'][$lang['lang_id']] = [
|
||
'name' => $lang['name'],
|
||
'short_description' => $lang['short_description'],
|
||
'description' => $lang['description'],
|
||
'meta_description' => $lang['meta_description'],
|
||
'meta_keywords' => $lang['meta_keywords'],
|
||
'meta_title' => $lang['meta_title'],
|
||
'seo_link' => $lang['seo_link'],
|
||
'copy_from' => $lang['copy_from'],
|
||
'warehouse_message_zero' => $lang['warehouse_message_zero'],
|
||
'warehouse_message_nonzero' => $lang['warehouse_message_nonzero'],
|
||
'tab_name_1' => $lang['tab_name_1'],
|
||
'tab_description_1' => $lang['tab_description_1'],
|
||
'tab_name_2' => $lang['tab_name_2'],
|
||
'tab_description_2' => $lang['tab_description_2'],
|
||
'canonical' => $lang['canonical'],
|
||
'security_information' => $lang['security_information'] ?? null,
|
||
];
|
||
}
|
||
}
|
||
|
||
// Images
|
||
$images = $this->db->select('pp_shop_products_images', ['id', 'src', 'alt'], [
|
||
'product_id' => $id,
|
||
'ORDER' => ['o' => 'ASC', 'id' => 'ASC'],
|
||
]);
|
||
$result['images'] = is_array($images) ? $images : [];
|
||
foreach ($result['images'] as &$img) {
|
||
$img['id'] = (int)$img['id'];
|
||
}
|
||
unset($img);
|
||
|
||
// Categories
|
||
$categories = $this->db->select('pp_shop_products_categories', 'category_id', ['product_id' => $id]);
|
||
$result['categories'] = [];
|
||
if (is_array($categories)) {
|
||
foreach ($categories as $catId) {
|
||
$result['categories'][] = (int)$catId;
|
||
}
|
||
}
|
||
|
||
// Attributes (enriched with names) — batch-loaded
|
||
$attributes = $this->db->select('pp_shop_products_attributes', ['attribute_id', 'value_id'], ['product_id' => $id]);
|
||
$result['attributes'] = [];
|
||
if (is_array($attributes) && !empty($attributes)) {
|
||
$attrIds = [];
|
||
$valueIds = [];
|
||
foreach ($attributes as $attr) {
|
||
$attrIds[] = (int)$attr['attribute_id'];
|
||
$valueIds[] = (int)$attr['value_id'];
|
||
}
|
||
$attrNamesMap = $this->batchLoadAttributeNames($attrIds);
|
||
$valueNamesMap = $this->batchLoadValueNames($valueIds);
|
||
$attrTypesMap = $this->batchLoadAttributeTypes($attrIds);
|
||
|
||
foreach ($attributes as $attr) {
|
||
$attrId = (int)$attr['attribute_id'];
|
||
$valId = (int)$attr['value_id'];
|
||
$result['attributes'][] = [
|
||
'attribute_id' => $attrId,
|
||
'attribute_type' => isset($attrTypesMap[$attrId]) ? $attrTypesMap[$attrId] : 0,
|
||
'attribute_names' => isset($attrNamesMap[$attrId]) ? $attrNamesMap[$attrId] : [],
|
||
'value_id' => $valId,
|
||
'value_names' => isset($valueNamesMap[$valId]) ? $valueNamesMap[$valId] : [],
|
||
];
|
||
}
|
||
}
|
||
|
||
// Custom fields (Dodatkowe pola)
|
||
$customFields = $this->db->select('pp_shop_products_custom_fields', ['name', 'type', 'is_required'], ['id_product' => $id]);
|
||
$result['custom_fields'] = [];
|
||
if (is_array($customFields)) {
|
||
foreach ($customFields as $cf) {
|
||
$result['custom_fields'][] = [
|
||
'name' => $cf['name'],
|
||
'type' => !empty($cf['type']) ? $cf['type'] : 'text',
|
||
'is_required' => $cf['is_required'],
|
||
];
|
||
}
|
||
}
|
||
|
||
// Variants (only for parent products)
|
||
if (empty($product['parent_id'])) {
|
||
$result['variants'] = $this->findVariantsForApi($id);
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Pobiera warianty produktu z atrybutami i tłumaczeniami dla REST API.
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @return array Lista wariantów
|
||
*/
|
||
public function findVariantsForApi(int $productId): array
|
||
{
|
||
$stmt = $this->db->query(
|
||
'SELECT id, permutation_hash, sku, ean, price_brutto, price_brutto_promo,
|
||
price_netto, price_netto_promo, quantity, stock_0_buy, weight, status
|
||
FROM pp_shop_products
|
||
WHERE parent_id = :pid
|
||
ORDER BY id ASC',
|
||
[':pid' => $productId]
|
||
);
|
||
|
||
$rows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||
if (!is_array($rows)) {
|
||
return [];
|
||
}
|
||
|
||
// Collect all variant IDs, then load attributes in batch
|
||
$variantIds = [];
|
||
foreach ($rows as $row) {
|
||
$variantIds[] = (int)$row['id'];
|
||
}
|
||
|
||
// Load all attributes for all variants at once
|
||
$allAttrsRaw = [];
|
||
if (!empty($variantIds)) {
|
||
$allAttrsRaw = $this->db->select('pp_shop_products_attributes', ['product_id', 'attribute_id', 'value_id'], ['product_id' => $variantIds]);
|
||
if (!is_array($allAttrsRaw)) {
|
||
$allAttrsRaw = [];
|
||
}
|
||
}
|
||
|
||
// Group by variant and collect unique IDs for batch loading
|
||
$attrsByVariant = [];
|
||
$allAttrIds = [];
|
||
$allValueIds = [];
|
||
foreach ($allAttrsRaw as $a) {
|
||
$pid = (int)$a['product_id'];
|
||
$aId = (int)$a['attribute_id'];
|
||
$vId = (int)$a['value_id'];
|
||
$attrsByVariant[$pid][] = ['attribute_id' => $aId, 'value_id' => $vId];
|
||
$allAttrIds[] = $aId;
|
||
$allValueIds[] = $vId;
|
||
}
|
||
|
||
$attrNamesMap = $this->batchLoadAttributeNames($allAttrIds);
|
||
$valueNamesMap = $this->batchLoadValueNames($allValueIds);
|
||
|
||
$variants = [];
|
||
foreach ($rows as $row) {
|
||
$variantId = (int)$row['id'];
|
||
$variantAttrs = [];
|
||
if (isset($attrsByVariant[$variantId])) {
|
||
foreach ($attrsByVariant[$variantId] as $a) {
|
||
$aId = $a['attribute_id'];
|
||
$vId = $a['value_id'];
|
||
$variantAttrs[] = [
|
||
'attribute_id' => $aId,
|
||
'attribute_names' => isset($attrNamesMap[$aId]) ? $attrNamesMap[$aId] : [],
|
||
'value_id' => $vId,
|
||
'value_names' => isset($valueNamesMap[$vId]) ? $valueNamesMap[$vId] : [],
|
||
];
|
||
}
|
||
}
|
||
|
||
$variants[] = [
|
||
'id' => $variantId,
|
||
'permutation_hash' => $row['permutation_hash'],
|
||
'sku' => $row['sku'],
|
||
'ean' => $row['ean'],
|
||
'price_brutto' => $row['price_brutto'] !== null ? (float)$row['price_brutto'] : null,
|
||
'price_brutto_promo' => $row['price_brutto_promo'] !== null ? (float)$row['price_brutto_promo'] : null,
|
||
'price_netto' => $row['price_netto'] !== null ? (float)$row['price_netto'] : null,
|
||
'price_netto_promo' => $row['price_netto_promo'] !== null ? (float)$row['price_netto_promo'] : null,
|
||
'quantity' => (int)$row['quantity'],
|
||
'stock_0_buy' => (int)($row['stock_0_buy'] ?? 0),
|
||
'weight' => $row['weight'] !== null ? (float)$row['weight'] : null,
|
||
'status' => (int)$row['status'],
|
||
'attributes' => $variantAttrs,
|
||
];
|
||
}
|
||
|
||
return $variants;
|
||
}
|
||
|
||
/**
|
||
* Pobiera pojedynczy wariant po ID dla REST API.
|
||
*
|
||
* @param int $variantId ID wariantu
|
||
* @return array|null Dane wariantu lub null
|
||
*/
|
||
public function findVariantForApi(int $variantId): ?array
|
||
{
|
||
$row = $this->db->get('pp_shop_products', '*', ['id' => $variantId]);
|
||
if (!$row || empty($row['parent_id'])) {
|
||
return null;
|
||
}
|
||
|
||
$attrs = $this->db->select('pp_shop_products_attributes', ['attribute_id', 'value_id'], ['product_id' => $variantId]);
|
||
$variantAttrs = [];
|
||
if (is_array($attrs) && !empty($attrs)) {
|
||
$attrIds = [];
|
||
$valueIds = [];
|
||
foreach ($attrs as $a) {
|
||
$attrIds[] = (int)$a['attribute_id'];
|
||
$valueIds[] = (int)$a['value_id'];
|
||
}
|
||
$attrNamesMap = $this->batchLoadAttributeNames($attrIds);
|
||
$valueNamesMap = $this->batchLoadValueNames($valueIds);
|
||
|
||
foreach ($attrs as $a) {
|
||
$aId = (int)$a['attribute_id'];
|
||
$vId = (int)$a['value_id'];
|
||
$variantAttrs[] = [
|
||
'attribute_id' => $aId,
|
||
'attribute_names' => isset($attrNamesMap[$aId]) ? $attrNamesMap[$aId] : [],
|
||
'value_id' => $vId,
|
||
'value_names' => isset($valueNamesMap[$vId]) ? $valueNamesMap[$vId] : [],
|
||
];
|
||
}
|
||
}
|
||
|
||
return [
|
||
'id' => (int)$row['id'],
|
||
'parent_id' => (int)$row['parent_id'],
|
||
'permutation_hash' => $row['permutation_hash'],
|
||
'sku' => $row['sku'],
|
||
'ean' => $row['ean'],
|
||
'price_brutto' => $row['price_brutto'] !== null ? (float)$row['price_brutto'] : null,
|
||
'price_brutto_promo' => $row['price_brutto_promo'] !== null ? (float)$row['price_brutto_promo'] : null,
|
||
'price_netto' => $row['price_netto'] !== null ? (float)$row['price_netto'] : null,
|
||
'price_netto_promo' => $row['price_netto_promo'] !== null ? (float)$row['price_netto_promo'] : null,
|
||
'quantity' => (int)$row['quantity'],
|
||
'stock_0_buy' => (int)($row['stock_0_buy'] ?? 0),
|
||
'weight' => $row['weight'] !== null ? (float)$row['weight'] : null,
|
||
'status' => (int)$row['status'],
|
||
'attributes' => $variantAttrs,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Tworzy nowy wariant (kombinację) produktu przez API.
|
||
*
|
||
* @param int $parentId ID produktu nadrzędnego
|
||
* @param array $data Dane wariantu (attributes, sku, ean, price_brutto, etc.)
|
||
* @return array|null ['id' => int, 'permutation_hash' => string] lub null przy błędzie
|
||
*/
|
||
public function createVariantForApi(int $parentId, array $data): ?array
|
||
{
|
||
$parent = $this->db->get('pp_shop_products', ['id', 'archive', 'parent_id', 'vat'], ['id' => $parentId]);
|
||
if (!$parent) {
|
||
return null;
|
||
}
|
||
if (!empty($parent['archive'])) {
|
||
return null;
|
||
}
|
||
if (!empty($parent['parent_id'])) {
|
||
return null;
|
||
}
|
||
|
||
$attributes = isset($data['attributes']) && is_array($data['attributes']) ? $data['attributes'] : [];
|
||
if (empty($attributes)) {
|
||
return null;
|
||
}
|
||
|
||
// Build permutation hash
|
||
ksort($attributes);
|
||
$hashParts = [];
|
||
foreach ($attributes as $attrId => $valueId) {
|
||
$hashParts[] = (int)$attrId . '-' . (int)$valueId;
|
||
}
|
||
$permutationHash = implode('|', $hashParts);
|
||
|
||
// Check duplicate
|
||
$existing = $this->db->count('pp_shop_products', [
|
||
'AND' => [
|
||
'parent_id' => $parentId,
|
||
'permutation_hash' => $permutationHash,
|
||
],
|
||
]);
|
||
if ($existing > 0) {
|
||
return null;
|
||
}
|
||
|
||
$insertData = [
|
||
'parent_id' => $parentId,
|
||
'permutation_hash' => $permutationHash,
|
||
'vat' => $parent['vat'] ?? 0,
|
||
'sku' => isset($data['sku']) ? (string)$data['sku'] : null,
|
||
'ean' => isset($data['ean']) ? (string)$data['ean'] : null,
|
||
'price_brutto' => isset($data['price_brutto']) ? (float)$data['price_brutto'] : null,
|
||
'price_netto' => isset($data['price_netto']) ? (float)$data['price_netto'] : null,
|
||
'quantity' => isset($data['quantity']) ? (int)$data['quantity'] : 0,
|
||
'stock_0_buy' => isset($data['stock_0_buy']) ? (int)$data['stock_0_buy'] : 0,
|
||
'weight' => isset($data['weight']) ? (float)$data['weight'] : null,
|
||
'status' => 1,
|
||
];
|
||
|
||
$this->db->insert('pp_shop_products', $insertData);
|
||
$variantId = (int)$this->db->id();
|
||
if ($variantId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
// Insert attribute rows
|
||
foreach ($attributes as $attrId => $valueId) {
|
||
$this->db->insert('pp_shop_products_attributes', [
|
||
'product_id' => $variantId,
|
||
'attribute_id' => (int)$attrId,
|
||
'value_id' => (int)$valueId,
|
||
]);
|
||
}
|
||
|
||
return [
|
||
'id' => $variantId,
|
||
'permutation_hash' => $permutationHash,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Aktualizuje wariant produktu przez API.
|
||
*
|
||
* @param int $variantId ID wariantu
|
||
* @param array $data Pola do aktualizacji
|
||
* @return bool true jeśli sukces
|
||
*/
|
||
public function updateVariantForApi(int $variantId, array $data): bool
|
||
{
|
||
$variant = $this->db->get('pp_shop_products', ['id', 'parent_id'], ['id' => $variantId]);
|
||
if (!$variant || empty($variant['parent_id'])) {
|
||
return false;
|
||
}
|
||
|
||
$casts = [
|
||
'sku' => 'string',
|
||
'ean' => 'string',
|
||
'price_brutto' => 'float_or_null',
|
||
'price_netto' => 'float_or_null',
|
||
'price_brutto_promo' => 'float_or_null',
|
||
'price_netto_promo' => 'float_or_null',
|
||
'quantity' => 'int',
|
||
'stock_0_buy' => 'int',
|
||
'weight' => 'float_or_null',
|
||
'status' => 'int',
|
||
];
|
||
|
||
$updateData = [];
|
||
foreach ($casts as $field => $type) {
|
||
if (array_key_exists($field, $data)) {
|
||
$value = $data[$field];
|
||
if ($type === 'string') {
|
||
$updateData[$field] = ($value !== null) ? (string)$value : '';
|
||
} elseif ($type === 'int') {
|
||
$updateData[$field] = (int)$value;
|
||
} elseif ($type === 'float_or_null') {
|
||
$updateData[$field] = ($value !== null && $value !== '') ? (float)$value : null;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (empty($updateData)) {
|
||
return true;
|
||
}
|
||
|
||
$this->db->update('pp_shop_products', $updateData, ['id' => $variantId]);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Usuwa wariant produktu przez API.
|
||
*
|
||
* @param int $variantId ID wariantu
|
||
* @return bool true jeśli sukces
|
||
*/
|
||
public function deleteVariantForApi(int $variantId): bool
|
||
{
|
||
$variant = $this->db->get('pp_shop_products', ['id', 'parent_id'], ['id' => $variantId]);
|
||
if (!$variant || empty($variant['parent_id'])) {
|
||
return false;
|
||
}
|
||
|
||
$this->db->delete('pp_shop_products_langs', ['product_id' => $variantId]);
|
||
$this->db->delete('pp_shop_products_attributes', ['product_id' => $variantId]);
|
||
$this->db->delete('pp_shop_products', ['id' => $variantId]);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Batch-loads attribute names for multiple attribute IDs.
|
||
*
|
||
* @param int[] $attrIds
|
||
* @return array<int, array<string, string>> [attrId => [langId => name]]
|
||
*/
|
||
private function batchLoadAttributeNames(array $attrIds): array
|
||
{
|
||
if (empty($attrIds)) {
|
||
return [];
|
||
}
|
||
$translations = $this->db->select(
|
||
'pp_shop_attributes_langs',
|
||
['attribute_id', 'lang_id', 'name'],
|
||
['attribute_id' => array_values(array_unique($attrIds))]
|
||
);
|
||
$result = [];
|
||
if (is_array($translations)) {
|
||
foreach ($translations as $t) {
|
||
$aId = (int)($t['attribute_id'] ?? 0);
|
||
$langId = (string)($t['lang_id'] ?? '');
|
||
if ($aId > 0 && $langId !== '') {
|
||
$result[$aId][$langId] = (string)($t['name'] ?? '');
|
||
}
|
||
}
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Batch-loads value names for multiple value IDs.
|
||
*
|
||
* @param int[] $valueIds
|
||
* @return array<int, array<string, string>> [valueId => [langId => name]]
|
||
*/
|
||
private function batchLoadValueNames(array $valueIds): array
|
||
{
|
||
if (empty($valueIds)) {
|
||
return [];
|
||
}
|
||
$translations = $this->db->select(
|
||
'pp_shop_attributes_values_langs',
|
||
['value_id', 'lang_id', 'name'],
|
||
['value_id' => array_values(array_unique($valueIds))]
|
||
);
|
||
$result = [];
|
||
if (is_array($translations)) {
|
||
foreach ($translations as $t) {
|
||
$vId = (int)($t['value_id'] ?? 0);
|
||
$langId = (string)($t['lang_id'] ?? '');
|
||
if ($vId > 0 && $langId !== '') {
|
||
$result[$vId][$langId] = (string)($t['name'] ?? '');
|
||
}
|
||
}
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Batch-loads attribute types for multiple attribute IDs.
|
||
*
|
||
* @param int[] $attrIds
|
||
* @return array<int, int> [attrId => type]
|
||
*/
|
||
private function batchLoadAttributeTypes(array $attrIds): array
|
||
{
|
||
if (empty($attrIds)) {
|
||
return [];
|
||
}
|
||
$rows = $this->db->select(
|
||
'pp_shop_attributes',
|
||
['id', 'type'],
|
||
['id' => array_values(array_unique($attrIds))]
|
||
);
|
||
$result = [];
|
||
if (is_array($rows)) {
|
||
foreach ($rows as $row) {
|
||
$result[(int)$row['id']] = (int)($row['type'] ?? 0);
|
||
}
|
||
}
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* Zwraca nazwę producenta po ID (null jeśli brak).
|
||
*
|
||
* @param mixed $producerId
|
||
* @return string|null
|
||
*/
|
||
private function resolveProducerName($producerId): ?string
|
||
{
|
||
if (empty($producerId)) {
|
||
return null;
|
||
}
|
||
$name = $this->db->get('pp_shop_producer', 'name', ['id' => (int)$producerId]);
|
||
return ($name !== false && $name !== null) ? (string)$name : null;
|
||
}
|
||
|
||
/**
|
||
* Szczegóły produktu (admin) — zastępuje factory product_details().
|
||
*/
|
||
public function findForAdmin(int $productId): ?array
|
||
{
|
||
$product = $this->db->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
|
||
if ( !$product ) {
|
||
return null;
|
||
}
|
||
|
||
$results = $this->db->select( 'pp_shop_products_langs', '*', [ 'product_id' => $productId ] );
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
$product['languages'][ $row['lang_id'] ] = $row;
|
||
}
|
||
}
|
||
|
||
$product['images'] = $this->db->select( 'pp_shop_products_images', '*', [ 'product_id' => $productId, 'ORDER' => [ 'o' => 'ASC', 'id' => 'ASC' ] ] );
|
||
$product['files'] = $this->db->select( 'pp_shop_products_files', '*', [ 'product_id' => $productId ] );
|
||
$product['categories'] = $this->db->select( 'pp_shop_products_categories', 'category_id', [ 'product_id' => $productId ] );
|
||
$product['attributes'] = $this->db->select( 'pp_shop_products_attributes', [ 'attribute_id', 'value_id' ], [ 'product_id' => $productId ] );
|
||
$product['products_related'] = $this->db->select( 'pp_shop_products_related', 'product_related_id', [ 'product_id' => $productId ] );
|
||
$product['custom_fields'] = $this->db->select( 'pp_shop_products_custom_fields', '*', [ 'id_product' => $productId ] );
|
||
|
||
return $product;
|
||
}
|
||
|
||
/**
|
||
* Prosta lista produktów (id => name) — do selectów w formularzu.
|
||
*/
|
||
public function allProductsList(): array
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => null ] );
|
||
$products = [];
|
||
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $id ) {
|
||
$products[ $id ] = $this->db->get( 'pp_shop_products_langs', 'name', [
|
||
'AND' => [ 'product_id' => $id, 'lang_id' => 'pl' ]
|
||
] );
|
||
}
|
||
}
|
||
|
||
return $products;
|
||
}
|
||
|
||
/**
|
||
* Tekst z nazwami kategorii produktu (oddzielone " / ").
|
||
*/
|
||
public function productCategoriesText(int $productId): string
|
||
{
|
||
$results = $this->db->query(
|
||
'SELECT category_id FROM pp_shop_products_categories WHERE product_id = :pid',
|
||
[ ':pid' => $productId ]
|
||
);
|
||
$rows = $results ? $results->fetchAll( \PDO::FETCH_ASSOC ) : [];
|
||
|
||
if ( !is_array( $rows ) || empty( $rows ) ) {
|
||
return '';
|
||
}
|
||
|
||
$out = ' - ';
|
||
foreach ( $rows as $i => $row ) {
|
||
$title = $this->db->get( 'pp_shop_categories_langs', 'title', [
|
||
'AND' => [
|
||
'category_id' => (int) $row['category_id'],
|
||
'lang_id' => $this->defaultLangId(),
|
||
]
|
||
] );
|
||
$out .= $title ?: '';
|
||
if ( $i < count( $rows ) - 1 ) {
|
||
$out .= ' / ';
|
||
}
|
||
}
|
||
|
||
return $out;
|
||
}
|
||
|
||
/**
|
||
* ID produktu nadrzędnego (parent_id).
|
||
*/
|
||
public function getParentId(int $productId): ?int
|
||
{
|
||
$parentId = $this->db->get( 'pp_shop_products', 'parent_id', [ 'id' => $productId ] );
|
||
return $parentId ? (int) $parentId : null;
|
||
}
|
||
|
||
/**
|
||
* Domyślna nazwa produktu (w domyślnym języku).
|
||
*/
|
||
public function productDefaultName(int $productId): ?string
|
||
{
|
||
$name = $this->db->get( 'pp_shop_products_langs', 'name', [
|
||
'AND' => [ 'product_id' => $productId, 'lang_id' => $this->defaultLangId() ]
|
||
] );
|
||
return $name ?: null;
|
||
}
|
||
|
||
/**
|
||
* ID domyślnego języka.
|
||
*/
|
||
private function defaultLangId(): string
|
||
{
|
||
$langId = $this->db->get( 'pp_langs', 'id', [ 'start' => 1 ] );
|
||
return $langId ?: 'pl';
|
||
}
|
||
|
||
// ─── Krok 2: zapis produktu ──────────────────────────────────────
|
||
|
||
/**
|
||
* Zapis produktu (insert lub update). Zastępuje factory save().
|
||
*
|
||
* @param array $d Dane produktu (z formularza)
|
||
* @param int|null $userId ID aktualnego użytkownika admin
|
||
* @return int|null ID produktu lub null przy błędzie
|
||
*/
|
||
public function saveProduct(array $d, ?int $userId = null): ?int
|
||
{
|
||
$productId = (int) ( $d['id'] ?? 0 );
|
||
$isNew = !$productId;
|
||
|
||
$productData = [
|
||
'date_modify' => date( 'Y-m-d H:i:s' ),
|
||
'modify_by' => $userId !== null ? (int) $userId : 0,
|
||
'status' => ( $d['status'] ?? '' ) === 'on' ? 1 : 0,
|
||
'price_netto' => $this->nullIfEmpty( $d['price_netto'] ?? null ),
|
||
'price_brutto' => $this->nullIfEmpty( $d['price_brutto'] ?? null ),
|
||
'vat' => $d['vat'] ?? 0,
|
||
'promoted' => ( $d['promoted'] ?? '' ) === 'on' ? 1 : 0,
|
||
'layout_id' => $this->nullIfEmpty( $d['layout_id'] ?? null ),
|
||
'price_netto_promo' => $this->nullIfEmpty( $d['price_netto_promo'] ?? null ),
|
||
'price_brutto_promo' => $this->nullIfEmpty( $d['price_brutto_promo'] ?? null ),
|
||
'new_to_date' => $this->nullIfEmpty( $d['new_to_date'] ?? null ),
|
||
'stock_0_buy' => ( $d['stock_0_buy'] ?? '' ) === 'on' ? 1 : 0,
|
||
'wp' => $this->nullIfEmpty( $d['wp'] ?? null ),
|
||
'sku' => $this->nullIfEmpty( $d['sku'] ?? null ),
|
||
'ean' => $this->nullIfEmpty( $d['ean'] ?? null ),
|
||
'custom_label_0' => $this->nullIfEmpty( $d['custom_label_0'] ?? null ),
|
||
'custom_label_1' => $this->nullIfEmpty( $d['custom_label_1'] ?? null ),
|
||
'custom_label_2' => $this->nullIfEmpty( $d['custom_label_2'] ?? null ),
|
||
'custom_label_3' => $this->nullIfEmpty( $d['custom_label_3'] ?? null ),
|
||
'custom_label_4' => $this->nullIfEmpty( $d['custom_label_4'] ?? null ),
|
||
'additional_message' => ( $d['additional_message'] ?? '' ) == 'on' ? 1 : 0,
|
||
'set_id' => !empty( $d['set'] ) ? (int) $d['set'] : null,
|
||
'quantity' => (int) ( $d['quantity'] ?? 0 ),
|
||
'additional_message_text' => $this->nullIfEmpty( $d['additional_message_text'] ?? null ),
|
||
'additional_message_required' => ( $d['additional_message_required'] ?? '' ) == 'on' ? 1 : 0,
|
||
'producer_id' => !empty( $d['producer_id'] ) ? $d['producer_id'] : null,
|
||
'product_unit_id' => !empty( $d['product_unit'] ) ? $d['product_unit'] : null,
|
||
'weight' => !empty( $d['weight'] ) ? $d['weight'] : null,
|
||
];
|
||
|
||
if ( $isNew ) {
|
||
$productData['date_add'] = date( 'Y-m-d H:i:s' );
|
||
$this->db->insert( 'pp_shop_products', $productData );
|
||
$productId = (int) $this->db->id();
|
||
|
||
if ( !$productId ) {
|
||
return null;
|
||
}
|
||
} else {
|
||
$this->db->update( 'pp_shop_products', $productData, [ 'id' => $productId ] );
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'additional_message' => $productData['additional_message'],
|
||
], [ 'parent_id' => $productId ] );
|
||
|
||
$this->updateCombinationPricesFromBase(
|
||
$productId,
|
||
$d['price_brutto'] ?? 0,
|
||
$d['vat'] ?? 0,
|
||
$d['price_brutto_promo'] ?? null
|
||
);
|
||
}
|
||
|
||
$this->saveLanguages( $productId, $d, $isNew );
|
||
$this->saveCategories( $productId, $d['categories'] ?? null );
|
||
$this->saveRelatedProducts( $productId, $d['products_related'] ?? null );
|
||
$this->moveTemporaryFiles( $productId );
|
||
$this->moveTemporaryImages( $productId );
|
||
|
||
if ( !empty( $d['gallery_order'] ) ) {
|
||
$this->saveImagesOrder( $productId, $d['gallery_order'] );
|
||
}
|
||
|
||
// Zapisz custom fields tylko gdy jawnie podane (partial update przez API może nie zawierać tego klucza)
|
||
if ( array_key_exists( 'custom_field_name', $d ) ) {
|
||
$this->saveCustomFields( $productId, $d['custom_field_name'] ?? [], $d['custom_field_type'] ?? [], $d['custom_field_required'] ?? [] );
|
||
}
|
||
|
||
if ( !$isNew ) {
|
||
$this->cleanupDeletedFiles( $productId );
|
||
$this->cleanupDeletedImages( $productId );
|
||
}
|
||
|
||
\Shared\Helpers\Helpers::htacces();
|
||
\Shared\Helpers\Helpers::delete_dir( '../temp/' );
|
||
\Shared\Helpers\Helpers::delete_dir( '../thumbs/' );
|
||
|
||
if ( !$isNew ) {
|
||
$redis = \Shared\Cache\RedisConnection::getInstance()->getConnection();
|
||
if ( $redis ) {
|
||
$redis->flushAll();
|
||
}
|
||
}
|
||
|
||
return $productId;
|
||
}
|
||
|
||
private function saveLanguages(int $productId, array $d, bool $isNew): void
|
||
{
|
||
$langs = ( new \Domain\Languages\LanguagesRepository( $this->db ) )->languagesList( true );
|
||
|
||
foreach ( $langs as $lg ) {
|
||
$lid = $lg['id'];
|
||
$langData = [
|
||
'name' => $this->nullIfEmpty( $d['name'][$lid] ?? null ),
|
||
'short_description' => $this->nullIfEmpty( $d['short_description'][$lid] ?? null ),
|
||
'description' => $this->nullIfEmpty( $d['description'][$lid] ?? null ),
|
||
'meta_description' => $this->nullIfEmpty( $d['meta_description'][$lid] ?? null ),
|
||
'meta_keywords' => $this->nullIfEmpty( $d['meta_keywords'][$lid] ?? null ),
|
||
'seo_link' => !empty( $d['seo_link'][$lid] ) ? \Shared\Helpers\Helpers::seo( $d['seo_link'][$lid] ) : null,
|
||
'copy_from' => $this->nullIfEmpty( $d['copy_from'][$lid] ?? null ),
|
||
'warehouse_message_zero' => $this->nullIfEmpty( $d['warehouse_message_zero'][$lid] ?? null ),
|
||
'warehouse_message_nonzero' => $this->nullIfEmpty( $d['warehouse_message_nonzero'][$lid] ?? null ),
|
||
'tab_name_1' => $this->nullIfEmpty( $d['tab_name_1'][$lid] ?? null ),
|
||
'tab_description_1' => $this->nullIfEmpty( $d['tab_description_1'][$lid] ?? null ),
|
||
'tab_name_2' => $this->nullIfEmpty( $d['tab_name_2'][$lid] ?? null ),
|
||
'tab_description_2' => $this->nullIfEmpty( $d['tab_description_2'][$lid] ?? null ),
|
||
'canonical' => $this->nullIfEmpty( $d['canonical'][$lid] ?? null ),
|
||
'meta_title' => $this->nullIfEmpty( $d['meta_title'][$lid] ?? null ),
|
||
'security_information' => $this->nullIfEmpty( $d['security_information'][$lid] ?? null ),
|
||
];
|
||
|
||
if ( $isNew ) {
|
||
$langData['product_id'] = $productId;
|
||
$langData['lang_id'] = $lid;
|
||
$this->db->insert( 'pp_shop_products_langs', $langData );
|
||
} else {
|
||
$translationId = $this->db->get( 'pp_shop_products_langs', 'id', [
|
||
'AND' => [ 'product_id' => $productId, 'lang_id' => $lid ]
|
||
] );
|
||
|
||
if ( $translationId ) {
|
||
$currentSeoLink = $this->db->get( 'pp_shop_products_langs', 'seo_link', [ 'id' => $translationId ] );
|
||
$newSeoLink = $langData['seo_link'] ?: \Shared\Helpers\Helpers::seo( 'p-' . $productId . '-' . ( $d['name'][$lid] ?? '' ) );
|
||
|
||
if ( $newSeoLink !== $currentSeoLink && $currentSeoLink != '' ) {
|
||
$this->handleSeoRedirects( $productId, $lid, $newSeoLink, $currentSeoLink );
|
||
}
|
||
|
||
$this->db->update( 'pp_shop_products_langs', $langData, [ 'id' => $translationId ] );
|
||
} else {
|
||
$langData['product_id'] = $productId;
|
||
$langData['lang_id'] = $lid;
|
||
$this->db->insert( 'pp_shop_products_langs', $langData );
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private function handleSeoRedirects(int $productId, string $langId, string $newSeoLink, string $currentSeoLink): void
|
||
{
|
||
if ( $this->db->count( 'pp_redirects', [ 'from' => $newSeoLink, 'to' => $currentSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] ) ) {
|
||
$this->db->delete( 'pp_redirects', [ 'from' => $newSeoLink, 'to' => $currentSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] );
|
||
}
|
||
|
||
$this->db->delete( 'pp_redirects', [
|
||
'AND' => [
|
||
'product_id' => $productId,
|
||
'lang_id' => $langId,
|
||
'from' => $currentSeoLink,
|
||
'to[!]' => $newSeoLink,
|
||
],
|
||
] );
|
||
|
||
$seoUsed = (bool) $this->db->count( 'pp_shop_products_langs', [
|
||
'AND' => [
|
||
'lang_id' => $langId,
|
||
'seo_link' => $currentSeoLink,
|
||
'product_id[!]' => $productId,
|
||
],
|
||
] );
|
||
|
||
if ( !$seoUsed ) {
|
||
$this->db->delete( 'pp_redirects', [
|
||
'AND' => [
|
||
'from' => $currentSeoLink,
|
||
'lang_id' => $langId,
|
||
'product_id[!]' => $productId,
|
||
],
|
||
] );
|
||
|
||
if ( !$this->db->count( 'pp_redirects', [ 'from' => $currentSeoLink, 'to' => $newSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] ) ) {
|
||
if ( \Shared\Helpers\Helpers::canAddRedirect( $currentSeoLink, $newSeoLink, $langId ) ) {
|
||
$this->db->insert( 'pp_redirects', [ 'from' => $currentSeoLink, 'to' => $newSeoLink, 'lang_id' => $langId, 'product_id' => $productId ] );
|
||
}
|
||
}
|
||
} else {
|
||
$this->db->delete( 'pp_redirects', [ 'AND' => [ 'product_id' => $productId, 'lang_id' => $langId, 'from' => $currentSeoLink ] ] );
|
||
}
|
||
}
|
||
|
||
private function saveCategories(int $productId, $categories): void
|
||
{
|
||
if ( !is_array( $categories ) ) {
|
||
$categories = $categories ? [ $categories ] : [];
|
||
}
|
||
|
||
$notIn = array_merge( [0], $categories );
|
||
$this->db->delete( 'pp_shop_products_categories', [ 'AND' => [ 'product_id' => $productId, 'category_id[!]' => $notIn ] ] );
|
||
|
||
$existing = $this->db->select( 'pp_shop_products_categories', 'category_id', [ 'product_id' => $productId ] );
|
||
$existing = is_array( $existing ) ? $existing : [];
|
||
$toAdd = array_diff( $categories, $existing );
|
||
|
||
foreach ( $toAdd as $categoryId ) {
|
||
if ( $productId && $categoryId ) {
|
||
$order = (int) $this->db->max( 'pp_shop_products_categories', 'o' ) + 1;
|
||
$this->db->insert( 'pp_shop_products_categories', [
|
||
'product_id' => $productId,
|
||
'category_id' => (int) $categoryId,
|
||
'o' => $order,
|
||
] );
|
||
}
|
||
}
|
||
}
|
||
|
||
private function saveRelatedProducts(int $productId, $products): void
|
||
{
|
||
if ( !is_array( $products ) ) {
|
||
$products = $products ? [ $products ] : [];
|
||
}
|
||
|
||
$notIn = array_merge( [0], $products );
|
||
$this->db->delete( 'pp_shop_products_related', [ 'AND' => [ 'product_id' => $productId, 'product_related_id[!]' => $notIn ] ] );
|
||
|
||
$existing = $this->db->select( 'pp_shop_products_related', 'product_related_id', [ 'product_id' => $productId ] );
|
||
$existing = is_array( $existing ) ? $existing : [];
|
||
$toAdd = array_diff( $products, $existing );
|
||
|
||
foreach ( $toAdd as $relatedId ) {
|
||
if ( $productId && $relatedId ) {
|
||
$this->db->insert( 'pp_shop_products_related', [
|
||
'product_id' => $productId,
|
||
'product_related_id' => (int) $relatedId,
|
||
] );
|
||
}
|
||
}
|
||
}
|
||
|
||
private function moveTemporaryFiles(int $productId): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_files', '*', [ 'product_id' => null ] );
|
||
if ( !is_array( $results ) ) return;
|
||
|
||
$created = false;
|
||
foreach ( $results as $row ) {
|
||
$dir = '/upload/product_files/product_' . $productId;
|
||
$newName = str_replace( '/upload/product_files/tmp', $dir, $row['src'] );
|
||
|
||
if ( file_exists( '..' . $row['src'] ) ) {
|
||
if ( !is_dir( '../' . $dir ) && !$created ) {
|
||
if ( mkdir( '../' . $dir, 0755, true ) ) $created = true;
|
||
}
|
||
rename( '..' . $row['src'], '..' . $newName );
|
||
}
|
||
|
||
$this->db->update( 'pp_shop_products_files', [ 'src' => $newName, 'product_id' => $productId ], [ 'id' => $row['id'] ] );
|
||
}
|
||
}
|
||
|
||
private function moveTemporaryImages(int $productId): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_images', '*', [ 'product_id' => null ] );
|
||
if ( !is_array( $results ) ) return;
|
||
|
||
$created = false;
|
||
foreach ( $results as $row ) {
|
||
$dir = '/upload/product_images/product_' . $productId;
|
||
$newName = str_replace( '/upload/product_images/tmp', $dir, $row['src'] );
|
||
|
||
if ( file_exists( '..' . $newName ) ) {
|
||
$ext = strrpos( $newName, '.' );
|
||
$base = substr( $newName, 0, $ext );
|
||
$extension = substr( $newName, $ext );
|
||
$count = 1;
|
||
while ( file_exists( '..' . $base . '_' . $count . $extension ) ) {
|
||
++$count;
|
||
}
|
||
$newName = $base . '_' . $count . $extension;
|
||
}
|
||
|
||
if ( file_exists( '..' . $row['src'] ) ) {
|
||
if ( !is_dir( '../' . $dir ) && !$created ) {
|
||
if ( mkdir( '../' . $dir, 0755, true ) ) $created = true;
|
||
}
|
||
rename( '..' . $row['src'], '..' . $newName );
|
||
}
|
||
|
||
$this->db->update( 'pp_shop_products_images', [ 'src' => $newName, 'product_id' => $productId ], [ 'id' => $row['id'] ] );
|
||
}
|
||
}
|
||
|
||
private function saveCustomFields(int $productId, array $names, array $types, array $required): void
|
||
{
|
||
$existingIds = [];
|
||
foreach ( $names as $i => $name ) {
|
||
if ( !empty( $name ) ) {
|
||
$id = (int) $this->db->get( 'pp_shop_products_custom_fields', 'id_additional_field', [
|
||
'AND' => [ 'id_product' => $productId, 'name' => $name ]
|
||
] );
|
||
if ( $id ) $existingIds[] = $id;
|
||
}
|
||
}
|
||
|
||
if ( !empty( $existingIds ) ) {
|
||
$this->db->delete( 'pp_shop_products_custom_fields', [
|
||
'AND' => [ 'id_product' => $productId, 'id_additional_field[!]' => $existingIds ]
|
||
] );
|
||
} else {
|
||
$this->db->delete( 'pp_shop_products_custom_fields', [ 'id_product' => $productId ] );
|
||
}
|
||
|
||
foreach ( $names as $i => $name ) {
|
||
if ( empty( $name ) ) continue;
|
||
|
||
$typeVal = $types[$i] ?? 'text';
|
||
$isRequired = !empty( $required[$i] ) ? 1 : 0;
|
||
|
||
if ( !$this->db->count( 'pp_shop_products_custom_fields', [ 'AND' => [ 'id_product' => $productId, 'name' => $name ] ] ) ) {
|
||
$this->db->insert( 'pp_shop_products_custom_fields', [
|
||
'id_product' => $productId,
|
||
'name' => $name,
|
||
'type' => $typeVal,
|
||
'is_required' => $isRequired,
|
||
] );
|
||
} else {
|
||
$this->db->update( 'pp_shop_products_custom_fields', [
|
||
'type' => $typeVal,
|
||
'is_required' => $isRequired,
|
||
], [ 'AND' => [ 'id_product' => $productId, 'name' => $name ] ] );
|
||
}
|
||
}
|
||
}
|
||
|
||
private function cleanupDeletedFiles(int $productId): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_files', '*', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
$this->safeUnlink( $row['src'] );
|
||
}
|
||
}
|
||
$this->db->delete( 'pp_shop_products_files', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||
}
|
||
|
||
private function cleanupDeletedImages(int $productId): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_images', '*', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
$this->safeUnlink( $row['src'] );
|
||
}
|
||
}
|
||
$this->db->delete( 'pp_shop_products_images', [ 'AND' => [ 'product_id' => $productId, 'to_delete' => 1 ] ] );
|
||
}
|
||
|
||
private function nullIfEmpty($value)
|
||
{
|
||
return ( $value !== null && $value !== '' && $value !== 0.00 ) ? $value : null;
|
||
}
|
||
|
||
// ─── Koniec kroku 2 ──────────────────────────────────────────────
|
||
|
||
// ─── Krok 3: Operacje na produktach ──────────────────────────────
|
||
|
||
/**
|
||
* Usuwa produkt i wszystkie powiązane dane.
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return bool
|
||
*/
|
||
public function delete(int $productId): bool
|
||
{
|
||
// Usuń kombinacje (produkty z parent_id = productId)
|
||
$combinations = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => $productId ] );
|
||
if ( $combinations ) {
|
||
foreach ( $combinations as $combId ) {
|
||
$this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $combId ] );
|
||
$this->db->delete( 'pp_shop_products', [ 'id' => $combId ] );
|
||
}
|
||
}
|
||
|
||
$this->db->delete( 'pp_shop_products_categories', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products_langs', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products_images', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products_files', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products_custom_fields', [ 'id_product' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_products', [ 'id' => $productId ] );
|
||
$this->db->delete( 'pp_shop_product_sets_products', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_routes', [ 'product_id' => $productId ] );
|
||
$this->db->delete( 'pp_redirects', [ 'product_id' => $productId ] );
|
||
|
||
\Shared\Helpers\Helpers::delete_dir( '../upload/product_images/product_' . $productId . '/' );
|
||
\Shared\Helpers\Helpers::delete_dir( '../upload/product_files/product_' . $productId . '/' );
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Duplikuje produkt z opcjonalnym kopiowaniem kombinacji.
|
||
*
|
||
* @param int $productId ID produktu do zduplikowania
|
||
* @param bool $withCombinations Czy kopiować kombinacje
|
||
* @return bool
|
||
*/
|
||
public function duplicate(int $productId, bool $withCombinations = false): bool
|
||
{
|
||
$product = $this->db->get( 'pp_shop_products', '*', [ 'id' => $productId ] );
|
||
if ( !$product ) {
|
||
return false;
|
||
}
|
||
|
||
$this->db->insert( 'pp_shop_products', [
|
||
'price_netto' => $product['price_netto'],
|
||
'price_brutto' => $product['price_brutto'],
|
||
'price_netto_promo' => $product['price_netto_promo'],
|
||
'price_brutto_promo' => $product['price_brutto_promo'],
|
||
'vat' => $product['vat'],
|
||
'promoted' => $product['promoted'],
|
||
'layout_id' => $product['layout_id'],
|
||
'new_to_date' => $product['new_to_date'],
|
||
'stock_0_buy' => $product['stock_0_buy'],
|
||
'wp' => $product['wp'],
|
||
'custom_label_0' => $product['custom_label_0'],
|
||
'custom_label_1' => $product['custom_label_1'],
|
||
'custom_label_2' => $product['custom_label_2'],
|
||
'custom_label_3' => $product['custom_label_3'],
|
||
'custom_label_4' => $product['custom_label_4'],
|
||
'additional_message' => $product['additional_message'],
|
||
] );
|
||
|
||
$newProductId = $this->db->id();
|
||
if ( !$newProductId ) {
|
||
return false;
|
||
}
|
||
|
||
// Atrybuty
|
||
$attributes = $this->db->select( 'pp_shop_products_attributes', '*', [ 'product_id' => $productId ] );
|
||
if ( \Shared\Helpers\Helpers::is_array_fix( $attributes ) ) {
|
||
foreach ( $attributes as $row ) {
|
||
$this->db->insert( 'pp_shop_products_attributes', [
|
||
'product_id' => $newProductId,
|
||
'attribute_id' => $row['attribute_id'],
|
||
'value_id' => $row['value_id'],
|
||
] );
|
||
}
|
||
}
|
||
|
||
// Kategorie
|
||
$categories = $this->db->select( 'pp_shop_products_categories', '*', [ 'product_id' => $productId ] );
|
||
if ( \Shared\Helpers\Helpers::is_array_fix( $categories ) ) {
|
||
foreach ( $categories as $row ) {
|
||
$this->db->insert( 'pp_shop_products_categories', [
|
||
'product_id' => $newProductId,
|
||
'category_id' => $row['category_id'],
|
||
'o' => $row['o'],
|
||
] );
|
||
}
|
||
}
|
||
|
||
// Języki
|
||
$langs = $this->db->select( 'pp_shop_products_langs', '*', [ 'product_id' => $productId ] );
|
||
if ( \Shared\Helpers\Helpers::is_array_fix( $langs ) ) {
|
||
foreach ( $langs as $row ) {
|
||
$this->db->insert( 'pp_shop_products_langs', [
|
||
'product_id' => $newProductId,
|
||
'lang_id' => $row['lang_id'],
|
||
'name' => $row['name'] . ' - kopia',
|
||
'short_description' => $row['short_description'],
|
||
'description' => $row['description'],
|
||
'tab_name_1' => $row['tab_name_1'],
|
||
'tab_description_1' => $row['tab_description_1'],
|
||
'tab_name_2' => $row['tab_name_2'],
|
||
'tab_description_2' => $row['tab_description_2'],
|
||
'meta_description' => $row['meta_description'],
|
||
'meta_keywords' => $row['meta_keywords'],
|
||
'copy_from' => $row['copy_from'],
|
||
'warehouse_message_zero' => $row['warehouse_message_zero'],
|
||
'warehouse_message_nonzero' => $row['warehouse_message_nonzero'],
|
||
] );
|
||
}
|
||
}
|
||
|
||
// Custom fields
|
||
$customFields = $this->db->select( 'pp_shop_products_custom_fields', '*', [ 'id_product' => $productId ] );
|
||
if ( \Shared\Helpers\Helpers::is_array_fix( $customFields ) ) {
|
||
foreach ( $customFields as $row ) {
|
||
$this->db->insert( 'pp_shop_products_custom_fields', [
|
||
'id_product' => $newProductId,
|
||
'name' => $row['name'],
|
||
] );
|
||
}
|
||
}
|
||
|
||
// Duplikowanie kombinacji
|
||
if ( $withCombinations ) {
|
||
$productCombinations = $this->db->select( 'pp_shop_products', '*', [ 'parent_id' => $productId ] );
|
||
if ( \Shared\Helpers\Helpers::is_array_fix( $productCombinations ) ) {
|
||
foreach ( $productCombinations as $comb ) {
|
||
$this->db->insert( 'pp_shop_products', [
|
||
'parent_id' => $newProductId,
|
||
'permutation_hash' => $comb['permutation_hash'],
|
||
'price_netto' => $comb['price_netto'],
|
||
'price_brutto' => $comb['price_brutto'],
|
||
'price_netto_promo' => $comb['price_netto_promo'],
|
||
'price_brutto_promo' => $comb['price_brutto_promo'],
|
||
'vat' => $comb['vat'],
|
||
'stock_0_buy' => $comb['stock_0_buy'],
|
||
'quantity' => $comb['quantity'],
|
||
'wp' => $comb['wp'],
|
||
'additional_message' => $comb['additional_message'],
|
||
'additional_message_text' => $comb['additional_message_text'],
|
||
'additional_message_required' => $comb['additional_message_required'],
|
||
] );
|
||
|
||
$combNewId = $this->db->id();
|
||
if ( $combNewId ) {
|
||
$combAttrs = $this->db->select( 'pp_shop_products_attributes', '*', [ 'product_id' => $comb['id'] ] );
|
||
if ( \Shared\Helpers\Helpers::is_array_fix( $combAttrs ) ) {
|
||
foreach ( $combAttrs as $attr ) {
|
||
$this->db->insert( 'pp_shop_products_attributes', [
|
||
'product_id' => $combNewId,
|
||
'attribute_id' => $attr['attribute_id'],
|
||
'value_id' => $attr['value_id'],
|
||
] );
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Przełącza status produktu (aktywny/nieaktywny).
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @return bool
|
||
*/
|
||
public function toggleStatus(int $productId): bool
|
||
{
|
||
$status = $this->db->get( 'pp_shop_products', 'status', [ 'id' => $productId ] );
|
||
$newStatus = $status == 1 ? 0 : 1;
|
||
$this->db->update( 'pp_shop_products', [ 'status' => $newStatus ], [ 'id' => $productId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Szybka zmiana ceny brutto produktu.
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param mixed $price Nowa cena brutto
|
||
* @return bool
|
||
*/
|
||
public function updatePriceBrutto(int $productId, $price): bool
|
||
{
|
||
$vat = (float) $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
|
||
$priceNetto = \Shared\Helpers\Helpers::normalize_decimal( (float) $price / ( 100 + $vat ) * 100, 2 );
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_brutto' => $price != 0.00 ? $price : null,
|
||
'price_netto' => $priceNetto != 0.00 ? $priceNetto : null,
|
||
], [ 'id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Szybka zmiana ceny promocyjnej brutto produktu.
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param mixed $price Nowa cena promo brutto
|
||
* @return bool
|
||
*/
|
||
public function updatePriceBruttoPromo(int $productId, $price): bool
|
||
{
|
||
$vat = (float) $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
|
||
$priceNetto = \Shared\Helpers\Helpers::normalize_decimal( (float) $price / ( 100 + $vat ) * 100, 2 );
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_brutto_promo' => $price != 0.00 ? $price : null,
|
||
'price_netto_promo' => $priceNetto != 0.00 ? $priceNetto : null,
|
||
], [ 'id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Szybka zmiana custom label produktu.
|
||
*
|
||
* @param int $productId ID produktu
|
||
* @param string $label Numer labela (0-4)
|
||
* @param mixed $value Wartość
|
||
* @return bool
|
||
*/
|
||
public function updateCustomLabel(int $productId, string $label, $value): bool
|
||
{
|
||
$allowed = ['0', '1', '2', '3', '4'];
|
||
if (!in_array($label, $allowed, true)) {
|
||
return false;
|
||
}
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'custom_label_' . $label => $value ? $value : null,
|
||
], [ 'id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
// ─── Koniec kroku 3 ──────────────────────────────────────────────
|
||
|
||
// ─── Krok 4: Kombinacje ──────────────────────────────────────────
|
||
|
||
/**
|
||
* Pobiera kombinacje produktu — lekka wersja do tabeli admin.
|
||
* Jedno zapytanie SQL zamiast N×7 (findForAdmin per wiersz).
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @return array<int, array{id: int, permutation_hash: string, sku: ?string, quantity: ?int, price_netto: ?float, stock_0_buy: int, parent_id: int}>
|
||
*/
|
||
public function getCombinationsForTable(int $productId): array
|
||
{
|
||
$stmt = $this->db->query(
|
||
'SELECT id, permutation_hash, sku, quantity, price_netto, stock_0_buy, parent_id
|
||
FROM pp_shop_products
|
||
WHERE parent_id = :pid
|
||
ORDER BY id ASC',
|
||
[ ':pid' => $productId ]
|
||
);
|
||
|
||
return $stmt ? $stmt->fetchAll( \PDO::FETCH_ASSOC ) : [];
|
||
}
|
||
|
||
/**
|
||
* Pobiera permutacje (kombinacje) produktu z pełnymi danymi.
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @return array
|
||
*/
|
||
public function getPermutations(int $productId): array
|
||
{
|
||
$products = [];
|
||
$results = $this->db->select( 'pp_shop_products', 'id', [ 'parent_id' => $productId ] );
|
||
if ( \Shared\Helpers\Helpers::is_array_fix( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
$detail = $this->findForAdmin( (int) $row );
|
||
if ( $detail ) {
|
||
$products[] = $detail;
|
||
}
|
||
}
|
||
}
|
||
return $products;
|
||
}
|
||
|
||
/**
|
||
* Generuje kombinacje produktu na podstawie atrybutów.
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @param array $attributes Tablica atrybutów [attribute_id => [value_id, ...], ...]
|
||
* @return bool
|
||
*/
|
||
public function generateCombinations(int $productId, array $attributes): bool
|
||
{
|
||
$vat = $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
|
||
$attributeRepository = new \Domain\Attribute\AttributeRepository( $this->db );
|
||
|
||
$permutations = self::arrayCartesian( $attributes );
|
||
if ( !\Shared\Helpers\Helpers::is_array_fix( $permutations ) ) {
|
||
return true;
|
||
}
|
||
|
||
foreach ( $permutations as $permutation ) {
|
||
$product = null;
|
||
ksort( $permutation );
|
||
|
||
$permutationHash = '';
|
||
|
||
if ( \Shared\Helpers\Helpers::is_array_fix( $permutation ) ) {
|
||
foreach ( $permutation as $key => $val ) {
|
||
if ( $permutationHash ) {
|
||
$permutationHash .= '|';
|
||
}
|
||
$permutationHash .= $key . '-' . $val;
|
||
|
||
$valueDetails = $attributeRepository->valueDetails( (int) $val );
|
||
$impactOnPrice = $valueDetails['impact_on_the_price'];
|
||
|
||
if ( $impactOnPrice > 0 ) {
|
||
if ( !$product ) {
|
||
$product = $this->findForAdmin( $productId );
|
||
}
|
||
|
||
$productPriceBrutto = $product['price_brutto'] + $impactOnPrice;
|
||
$productPriceNetto = $productPriceBrutto / ( 1 + ( $product['vat'] / 100 ) );
|
||
|
||
if ( $product['price_brutto_promo'] ) {
|
||
$productPriceBruttoPromo = $product['price_brutto_promo'] + $impactOnPrice;
|
||
$productPriceNettoPromo = $productPriceBruttoPromo / ( 1 + ( $product['vat'] / 100 ) );
|
||
} else {
|
||
$productPriceBruttoPromo = null;
|
||
$productPriceNettoPromo = null;
|
||
}
|
||
}
|
||
|
||
if ( $permutationHash && !$this->db->count( 'pp_shop_products', [ 'AND' => [ 'parent_id' => $productId, 'permutation_hash' => $permutationHash ] ] ) ) {
|
||
if ( $this->db->insert( 'pp_shop_products', [ 'parent_id' => $productId, 'permutation_hash' => $permutationHash, 'vat' => $vat ] ) ) {
|
||
$combinationId = $this->db->id();
|
||
if ( $product ) {
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_netto' => $productPriceNetto,
|
||
'vat' => $product['vat'],
|
||
'price_brutto' => $productPriceBrutto,
|
||
'price_netto_promo' => $productPriceNettoPromo ?? null,
|
||
'price_brutto_promo' => $productPriceBruttoPromo ?? null,
|
||
], [ 'id' => $combinationId ] );
|
||
}
|
||
|
||
$hashRows = explode( '|', $permutationHash );
|
||
foreach ( $hashRows as $hashRow ) {
|
||
$attrRev = explode( '-', $hashRow );
|
||
$this->db->insert( 'pp_shop_products_attributes', [
|
||
'product_id' => $combinationId,
|
||
'attribute_id' => $attrRev[0],
|
||
'value_id' => $attrRev[1],
|
||
] );
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Usuwa kombinację produktu.
|
||
*
|
||
* @param int $combinationId ID kombinacji
|
||
* @return bool
|
||
*/
|
||
public function deleteCombination(int $combinationId): bool
|
||
{
|
||
$this->db->delete( 'pp_shop_products', [ 'id' => $combinationId ] );
|
||
$this->db->delete( 'pp_shop_products_attributes', [ 'product_id' => $combinationId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Liczy kombinacje produktu.
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @return int
|
||
*/
|
||
public function countCombinations(int $productId): int
|
||
{
|
||
return (int) $this->db->count( 'pp_shop_products', [ 'parent_id' => $productId ] );
|
||
}
|
||
|
||
/**
|
||
* Zapisuje stock_0_buy kombinacji.
|
||
*/
|
||
public function saveCombinationStock0Buy(int $productId, $value): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [ 'stock_0_buy' => $value == 'true' ? 1 : 0 ], [ 'id' => $productId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zapisuje SKU kombinacji.
|
||
*/
|
||
public function saveCombinationSku(int $productId, $sku): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [ 'sku' => $sku ], [ 'id' => $productId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zapisuje ilość kombinacji.
|
||
*/
|
||
public function saveCombinationQuantity(int $productId, $quantity): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products', [
|
||
'quantity' => $quantity == '' ? null : (int) $quantity,
|
||
], [ 'id' => $productId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zapisuje cenę kombinacji (z przeliczeniem netto -> brutto).
|
||
*/
|
||
public function saveCombinationPrice(int $productId, $priceNetto): bool
|
||
{
|
||
$vat = $this->db->get( 'pp_shop_products', 'vat', [ 'id' => $productId ] );
|
||
$priceBrutto = (float) $priceNetto * ( 1 + ( (float) $vat / 100 ) );
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_netto' => $priceNetto == '' ? null : (float) $priceNetto,
|
||
'price_brutto' => $priceBrutto == '' ? null : (float) $priceBrutto,
|
||
], [ 'id' => $productId ] );
|
||
|
||
return true;
|
||
}
|
||
|
||
// ─── Koniec kroku 4 ──────────────────────────────────────────────
|
||
|
||
// ─── Krok 5: Zdjęcia / Pliki / XML ──────────────────────────────
|
||
|
||
/**
|
||
* Oznacza zdjęcie do usunięcia.
|
||
*/
|
||
public function deleteImage(int $imageId): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products_images', [ 'to_delete' => 1 ], [ 'id' => $imageId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zmienia atrybut alt zdjęcia.
|
||
*/
|
||
public function updateImageAlt(int $imageId, string $alt): bool
|
||
{
|
||
$result = $this->db->update( 'pp_shop_products_images', [ 'alt' => $alt ], [ 'id' => $imageId ] );
|
||
\Shared\Helpers\Helpers::delete_cache();
|
||
return (bool) $result;
|
||
}
|
||
|
||
/**
|
||
* Zapisuje kolejność zdjęć.
|
||
*/
|
||
public function saveImagesOrder(int $productId, string $order): bool
|
||
{
|
||
$orderArr = explode( ';', $order );
|
||
if ( is_array( $orderArr ) && !empty( $orderArr ) ) {
|
||
$i = 0;
|
||
foreach ( $orderArr as $imageId ) {
|
||
$this->db->update( 'pp_shop_products_images', [
|
||
'o' => $i++,
|
||
], [
|
||
'AND' => [
|
||
'product_id' => $productId,
|
||
'id' => $imageId,
|
||
],
|
||
] );
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Usuwa nieprzypisane zdjęcia (product_id = null).
|
||
*/
|
||
public function deleteNonassignedImages(): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_images', '*', [ 'product_id' => null ] );
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
$this->safeUnlink( $row['src'] );
|
||
}
|
||
}
|
||
$this->db->delete( 'pp_shop_products_images', [ 'product_id' => null ] );
|
||
}
|
||
|
||
/**
|
||
* Usuwa plik z dysku tylko jeśli ścieżka pozostaje wewnątrz katalogu upload/.
|
||
* Zapobiega path traversal przy danych z bazy.
|
||
*/
|
||
private function safeUnlink(string $src): void
|
||
{
|
||
$base = realpath('../upload');
|
||
if (!$base) {
|
||
return;
|
||
}
|
||
$full = realpath('../' . ltrim($src, '/'));
|
||
if ($full && strpos($full, $base . DIRECTORY_SEPARATOR) === 0 && is_file($full)) {
|
||
unlink($full);
|
||
} elseif ($full) {
|
||
error_log( '[shopPRO] safeUnlink: ścieżka poza upload/: ' . $src );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Oznacza plik do usunięcia.
|
||
*/
|
||
public function deleteFile(int $fileId): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products_files', [ 'to_delete' => 1 ], [ 'id' => $fileId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Zmienia nazwę pliku.
|
||
*/
|
||
public function updateFileName(int $fileId, string $name): bool
|
||
{
|
||
$this->db->update( 'pp_shop_products_files', [ 'name' => $name ], [ 'id' => $fileId ] );
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Usuwa nieprzypisane pliki (product_id = null).
|
||
*/
|
||
public function deleteNonassignedFiles(): void
|
||
{
|
||
$results = $this->db->select( 'pp_shop_products_files', '*', [ 'product_id' => null ] );
|
||
if ( is_array( $results ) ) {
|
||
foreach ( $results as $row ) {
|
||
if ( file_exists( '../' . $row['src'] ) ) {
|
||
unlink( '../' . $row['src'] );
|
||
}
|
||
}
|
||
}
|
||
$this->db->delete( 'pp_shop_products_files', [ 'product_id' => null ] );
|
||
}
|
||
|
||
/**
|
||
* Pobiera zdjęcia produktu.
|
||
*/
|
||
public function getProductImages(int $productId): array
|
||
{
|
||
return $this->db->select( 'pp_shop_products_images', 'src', [
|
||
'product_id' => $productId,
|
||
'ORDER' => [ 'o' => 'ASC', 'id' => 'ASC' ],
|
||
] ) ?: [];
|
||
}
|
||
|
||
/**
|
||
* Zapisuje nazwę XML produktu.
|
||
*/
|
||
public function saveXmlName(int $productId, string $xmlName, string $langId): bool
|
||
{
|
||
return (bool) $this->db->update( 'pp_shop_products_langs', [
|
||
'xml_name' => $xmlName,
|
||
], [
|
||
'AND' => [ 'product_id' => $productId, 'lang_id' => $langId ],
|
||
] );
|
||
}
|
||
|
||
/**
|
||
* Pobiera sugestie custom label.
|
||
*/
|
||
public function customLabelSuggestions(string $customLabel, string $labelType): array
|
||
{
|
||
$allowed = ['custom_label_0', 'custom_label_1', 'custom_label_2', 'custom_label_3', 'custom_label_4'];
|
||
if (!in_array($labelType, $allowed, true)) {
|
||
return [];
|
||
}
|
||
|
||
$output = [];
|
||
$results = $this->db->query(
|
||
'SELECT DISTINCT ' . $labelType . ' AS label FROM pp_shop_products WHERE ' . $labelType . ' LIKE :custom_label LIMIT 10',
|
||
[ ':custom_label' => '%' . $customLabel . '%' ]
|
||
);
|
||
if ( $results ) {
|
||
foreach ( $results->fetchAll( \PDO::FETCH_ASSOC ) as $row ) {
|
||
$output[] = $row;
|
||
}
|
||
}
|
||
return $output;
|
||
}
|
||
|
||
/**
|
||
* Zapisuje custom label produktu.
|
||
*/
|
||
public function saveCustomLabel(int $productId, string $customLabel, string $labelType): bool
|
||
{
|
||
$allowed = ['custom_label_0', 'custom_label_1', 'custom_label_2', 'custom_label_3', 'custom_label_4'];
|
||
if (!in_array($labelType, $allowed, true)) {
|
||
return false;
|
||
}
|
||
|
||
return (bool) $this->db->update( 'pp_shop_products', [ $labelType => $customLabel ], [ 'id' => $productId ] );
|
||
}
|
||
|
||
/**
|
||
* Generuje kod EAN.
|
||
*/
|
||
public function generateEAN(string $number): string
|
||
{
|
||
$code = '200' . str_pad( $number, 9, '0' );
|
||
$weightflag = true;
|
||
$sum = 0;
|
||
|
||
for ( $i = strlen( $code ) - 1; $i >= 0; $i-- ) {
|
||
$sum += (int) $code[$i] * ( $weightflag ? 3 : 1 );
|
||
$weightflag = !$weightflag;
|
||
}
|
||
|
||
$code .= ( 10 - ( $sum % 10 ) ) % 10;
|
||
return $code;
|
||
}
|
||
|
||
/**
|
||
* Generuje Google Feed XML.
|
||
*/
|
||
public function generateGoogleFeedXml(): void
|
||
{
|
||
global $lang_id;
|
||
|
||
$settings = ( new \Domain\Settings\SettingsRepository( $this->db ) )->allSettings( true );
|
||
$this->transportRepoForXml = new \Domain\Transport\TransportRepository( $this->db );
|
||
|
||
$domainPrefix = 'https';
|
||
$url = preg_replace( '#^(http(s)?://)?w{3}\.#', '$1', $_SERVER['SERVER_NAME'] );
|
||
|
||
$doc = new \DOMDocument( '1.0', 'UTF-8' );
|
||
$xmlRoot = $doc->createElement( 'rss' );
|
||
$doc->appendChild( $xmlRoot );
|
||
$xmlRoot->setAttribute( 'version', '2.0' );
|
||
$xmlRoot->setAttributeNS( 'http://www.w3.org/2000/xmlns/', 'xmlns:g', 'http://base.google.com/ns/1.0' );
|
||
$channelNode = $xmlRoot->appendChild( $doc->createElement( 'channel' ) );
|
||
$channelNode->appendChild( $doc->createElement( 'title', $settings['firm_name'] ) );
|
||
$channelNode->appendChild( $doc->createElement( 'link', $domainPrefix . '://' . $url ) );
|
||
|
||
$rows = $this->db->select( 'pp_shop_products', 'id', [
|
||
'AND' => [ 'status' => '1', 'archive' => 0, 'parent_id' => null ],
|
||
] );
|
||
|
||
if ( \Shared\Helpers\Helpers::is_array_fix( $rows ) ) {
|
||
foreach ( $rows as $productId ) {
|
||
$product = $this->findCached( $productId, $lang_id );
|
||
if ( !$product ) continue;
|
||
|
||
if ( is_array( $product['product_combinations'] ) && count( $product['product_combinations'] ) ) {
|
||
foreach ( $product['product_combinations'] as $productCombination ) {
|
||
if ( $productCombination['quantity'] !== null || $productCombination['stock_0_buy'] ) {
|
||
$this->appendCombinationToXml( $doc, $channelNode, $product, $productCombination, $domainPrefix, $url );
|
||
}
|
||
}
|
||
} else {
|
||
$this->appendProductToXml( $doc, $channelNode, $product, $domainPrefix, $url );
|
||
}
|
||
}
|
||
}
|
||
|
||
file_put_contents( '../google-feed.xml', $doc->saveXML() );
|
||
}
|
||
|
||
/**
|
||
* Dodaje kombinację produktu do XML feed.
|
||
*/
|
||
private function appendCombinationToXml(\DOMDocument $doc, \DOMElement $channelNode, $product, $combination, string $domainPrefix, string $url): void
|
||
{
|
||
$itemNode = $channelNode->appendChild( $doc->createElement( 'item' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:id', $combination['id'] ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:item_group_id', $product['id'] ) );
|
||
|
||
for ( $l = 0; $l <= 4; $l++ ) {
|
||
$label = 'custom_label_' . $l;
|
||
if ( isset( $product[$label] ) && $product[$label] ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:' . $label, $product[$label] ) );
|
||
}
|
||
}
|
||
|
||
$title = $product['language']['xml_name'] ?: $product['language']['name'];
|
||
$itemNode->appendChild( $doc->createElement( 'title', str_replace( '&', '&', $title ) . ' - ' . $this->generateSubtitleFromAttributes( $combination['permutation_hash'] ) ) );
|
||
|
||
$gtin = $product['ean'] ?: $this->generateEAN( $product['id'] );
|
||
$itemNode->appendChild( $doc->createElement( 'g:gtin', $gtin ) );
|
||
|
||
$desc = $product['language']['short_description'] ?: $product['language']['name'];
|
||
$itemNode->appendChild( $doc->createElement( 'g:description', html_entity_decode( strip_tags( $desc ) ) ) );
|
||
|
||
if ( $product['language']['seo_link'] ) {
|
||
$link = $domainPrefix . '://' . $url . '/' . \Shared\Helpers\Helpers::seo( $product['language']['seo_link'] ) . '/' . str_replace( '|', '/', $combination['permutation_hash'] );
|
||
} else {
|
||
$link = $domainPrefix . '://' . $url . '/p-' . $product['id'] . '-' . \Shared\Helpers\Helpers::seo( $product['language']['name'] ) . '/' . str_replace( '|', '/', $combination['permutation_hash'] );
|
||
}
|
||
$itemNode->appendChild( $doc->createElement( 'link', $link ) );
|
||
|
||
$this->appendImagesToXml( $doc, $itemNode, $product, $domainPrefix, $url );
|
||
|
||
$itemNode->appendChild( $doc->createElement( 'g:condition', 'new' ) );
|
||
|
||
if ( $combination['quantity'] !== null ) {
|
||
if ( $combination['quantity'] > 0 ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', $combination['quantity'] ) );
|
||
} else {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', $combination['stock_0_buy'] ? 'in stock' : 'out of stock' ) );
|
||
}
|
||
} else {
|
||
if ( $product['quantity'] > 0 ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', $product['quantity'] ) );
|
||
} else {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', $product['stock_0_buy'] ? 'in stock' : 'out of stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', $product['stock_0_buy'] ? 999 : 0 ) );
|
||
}
|
||
}
|
||
|
||
if ( $combination['price_brutto'] ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:price', $combination['price_brutto'] . ' PLN' ) );
|
||
if ( $combination['price_brutto_promo'] ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:sale_price', $combination['price_brutto_promo'] . ' PLN' ) );
|
||
}
|
||
} else {
|
||
$itemNode->appendChild( $doc->createElement( 'g:price', $product['price_brutto'] . ' PLN' ) );
|
||
if ( $product['price_brutto_promo'] ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:sale_price', $product['price_brutto_promo'] . ' PLN' ) );
|
||
}
|
||
}
|
||
|
||
$this->appendShippingToXml( $doc, $itemNode, $product );
|
||
}
|
||
|
||
/**
|
||
* Dodaje produkt (bez kombinacji) do XML feed.
|
||
*/
|
||
private function appendProductToXml(\DOMDocument $doc, \DOMElement $channelNode, $product, string $domainPrefix, string $url): void
|
||
{
|
||
$itemNode = $channelNode->appendChild( $doc->createElement( 'item' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:id', $product['id'] ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:item_group_id', $product['id'] ) );
|
||
|
||
if ( isset( $product['google_xml_label'] ) && $product['google_xml_label'] ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:custom_label_0', $product['google_xml_label'] ) );
|
||
}
|
||
|
||
$title = $product['language']['xml_name'] ?: $product['language']['name'];
|
||
$itemNode->appendChild( $doc->createElement( 'title', str_replace( '&', '&', $title ) ) );
|
||
|
||
$gtin = $product['ean'] ?: $this->generateEAN( $product['id'] );
|
||
$itemNode->appendChild( $doc->createElement( 'g:gtin', $gtin ) );
|
||
|
||
$desc = $product['language']['short_description'] ?: $product['language']['name'];
|
||
$itemNode->appendChild( $doc->createElement( 'g:description', html_entity_decode( strip_tags( $desc ) ) ) );
|
||
|
||
if ( $product['language']['seo_link'] ) {
|
||
$link = $domainPrefix . '://' . $url . '/' . \Shared\Helpers\Helpers::seo( $product['language']['seo_link'] );
|
||
} else {
|
||
$link = $domainPrefix . '://' . $url . '/p-' . $product['id'] . '-' . \Shared\Helpers\Helpers::seo( $product['language']['name'] );
|
||
}
|
||
$itemNode->appendChild( $doc->createElement( 'link', $link ) );
|
||
|
||
for ( $l = 0; $l <= 4; $l++ ) {
|
||
$label = 'custom_label_' . $l;
|
||
if ( isset( $product[$label] ) && $product[$label] ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:' . $label, $product[$label] ) );
|
||
}
|
||
}
|
||
|
||
$this->appendImagesToXml( $doc, $itemNode, $product, $domainPrefix, $url );
|
||
|
||
$itemNode->appendChild( $doc->createElement( 'g:condition', 'new' ) );
|
||
|
||
if ( $product['quantity'] ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', $product['quantity'] ) );
|
||
} else {
|
||
if ( $product['stock_0_buy'] ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', 'in stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', 999 ) );
|
||
} else {
|
||
$itemNode->appendChild( $doc->createElement( 'g:availability', 'out of stock' ) );
|
||
$itemNode->appendChild( $doc->createElement( 'g:quantity', 0 ) );
|
||
}
|
||
}
|
||
|
||
$itemNode->appendChild( $doc->createElement( 'g:price', $product['price_brutto'] . ' PLN' ) );
|
||
if ( $product['price_brutto_promo'] ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:sale_price', $product['price_brutto_promo'] . ' PLN' ) );
|
||
}
|
||
|
||
$this->appendShippingToXml( $doc, $itemNode, $product );
|
||
}
|
||
|
||
/**
|
||
* Dodaje zdjęcia do node XML.
|
||
*/
|
||
private function appendImagesToXml(\DOMDocument $doc, \DOMElement $itemNode, $product, string $domainPrefix, string $url): void
|
||
{
|
||
if ( isset( $product['images'][0] ) ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:image_link', $domainPrefix . '://' . $url . $product['images'][0]['src'] ) );
|
||
}
|
||
if ( is_array( $product['images'] ) && count( $product['images'] ) > 1 ) {
|
||
for ( $i = 1; $i < count( $product['images'] ); ++$i ) {
|
||
$itemNode->appendChild( $doc->createElement( 'g:additional_image_link', $domainPrefix . '://' . $url . $product['images'][$i]['src'] ) );
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Dodaje sekcję shipping do node XML.
|
||
*/
|
||
private function appendShippingToXml(\DOMDocument $doc, \DOMElement $itemNode, $product): void
|
||
{
|
||
$shippingNode = $itemNode->appendChild( $doc->createElement( 'g:shipping' ) );
|
||
$shippingNode->appendChild( $doc->createElement( 'g:country', 'PL' ) );
|
||
$shippingNode->appendChild( $doc->createElement( 'g:service', '1 dzień roboczy' ) );
|
||
$shippingNode->appendChild( $doc->createElement( 'g:price',
|
||
$this->transportRepoForXml->lowestTransportPrice( (int) $product['wp'] ) . ' PLN'
|
||
) );
|
||
}
|
||
|
||
// ─── Koniec kroku 5 ──────────────────────────────────────────────
|
||
|
||
/**
|
||
* Aktualizuje ceny kombinacji produktu na podstawie cen brutto bazowych.
|
||
* Wywoływane z saveProduct() i z cron.php.
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @param float $priceBrutto Cena brutto bazowa
|
||
* @param float $vat Stawka VAT
|
||
* @param float|null $priceBruttoPromo Cena promo brutto bazowa (null = brak)
|
||
*/
|
||
public function updateCombinationPricesFromBase(int $productId, $priceBrutto, $vat, $priceBruttoPromo): void
|
||
{
|
||
$priceBrutto = (float) $priceBrutto;
|
||
$vat = (float) $vat;
|
||
$priceBruttoPromo = $priceBruttoPromo ? (float) $priceBruttoPromo : null;
|
||
|
||
$combinations = $this->db->query(
|
||
'SELECT psp.id '
|
||
. 'FROM pp_shop_products AS psp '
|
||
. 'INNER JOIN pp_shop_products_attributes AS pspa ON psp.id = pspa.product_id '
|
||
. 'INNER JOIN pp_shop_attributes_values AS psav ON pspa.value_id = psav.id '
|
||
. 'WHERE psav.impact_on_the_price > 0 AND psp.parent_id = :product_id',
|
||
[ ':product_id' => $productId ]
|
||
);
|
||
|
||
if ( !$combinations ) {
|
||
return;
|
||
}
|
||
|
||
$rows = $combinations->fetchAll( \PDO::FETCH_ASSOC );
|
||
foreach ( $rows as $row ) {
|
||
$combBrutto = $priceBrutto;
|
||
$combBruttoPromo = $priceBruttoPromo;
|
||
|
||
$values = $this->db->query(
|
||
'SELECT impact_on_the_price FROM pp_shop_attributes_values AS psav '
|
||
. 'INNER JOIN pp_shop_products_attributes AS pspa ON pspa.value_id = psav.id '
|
||
. 'WHERE impact_on_the_price IS NOT NULL AND product_id = :product_id',
|
||
[ ':product_id' => $row['id'] ]
|
||
);
|
||
|
||
if ( $values ) {
|
||
foreach ( $values->fetchAll( \PDO::FETCH_ASSOC ) as $value ) {
|
||
$combBrutto += $value['impact_on_the_price'];
|
||
if ( $combBruttoPromo !== null ) {
|
||
$combBruttoPromo += $value['impact_on_the_price'];
|
||
} else {
|
||
$combBruttoPromo = null;
|
||
}
|
||
}
|
||
}
|
||
|
||
$combNetto = \Shared\Helpers\Helpers::normalize_decimal( $combBrutto / ( 100 + $vat ) * 100, 2 );
|
||
$combNettoPromo = $combBruttoPromo !== null
|
||
? \Shared\Helpers\Helpers::normalize_decimal( $combBruttoPromo / ( 100 + $vat ) * 100, 2 )
|
||
: null;
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_netto' => $combNetto,
|
||
'price_brutto' => $combBrutto,
|
||
'price_netto_promo' => $combNettoPromo,
|
||
'price_brutto_promo' => $combBruttoPromo
|
||
], [ 'id' => $row['id'] ] );
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Aktualizuje ceny kombinacji produktu uwzględniając wpływ na cenę (impact_on_the_price).
|
||
* Wersja prywatna — przyjmuje ceny netto (dla mass_edit / applyDiscountPercent).
|
||
*
|
||
* @param int $productId ID produktu nadrzędnego
|
||
* @param float $priceNetto Cena netto bazowa
|
||
* @param float $vat Stawka VAT
|
||
* @param float|null $priceNettoPromo Cena promo netto bazowa (null = brak)
|
||
*/
|
||
private function updateCombinationPrices(int $productId, float $priceNetto, float $vat, ?float $priceNettoPromo): void
|
||
{
|
||
$priceBrutto = \Shared\Helpers\Helpers::normalize_decimal( $priceNetto * ( 100 + $vat ) / 100, 2 );
|
||
$priceBruttoPromo = $priceNettoPromo !== null
|
||
? \Shared\Helpers\Helpers::normalize_decimal( $priceNettoPromo * ( 100 + $vat ) / 100, 2 )
|
||
: null;
|
||
|
||
$combinations = $this->db->query(
|
||
'SELECT psp.id '
|
||
. 'FROM pp_shop_products AS psp '
|
||
. 'INNER JOIN pp_shop_products_attributes AS pspa ON psp.id = pspa.product_id '
|
||
. 'INNER JOIN pp_shop_attributes_values AS psav ON pspa.value_id = psav.id '
|
||
. 'WHERE psav.impact_on_the_price > 0 AND psp.parent_id = :product_id',
|
||
[ ':product_id' => $productId ]
|
||
);
|
||
|
||
if ( !$combinations ) {
|
||
return;
|
||
}
|
||
|
||
$rows = $combinations->fetchAll( \PDO::FETCH_ASSOC );
|
||
foreach ( $rows as $row ) {
|
||
$combBrutto = $priceBrutto;
|
||
$combBruttoPromo = $priceBruttoPromo;
|
||
|
||
$values = $this->db->query(
|
||
'SELECT impact_on_the_price FROM pp_shop_attributes_values AS psav '
|
||
. 'INNER JOIN pp_shop_products_attributes AS pspa ON pspa.value_id = psav.id '
|
||
. 'WHERE impact_on_the_price IS NOT NULL AND product_id = :product_id',
|
||
[ ':product_id' => $row['id'] ]
|
||
);
|
||
|
||
if ( $values ) {
|
||
foreach ( $values->fetchAll( \PDO::FETCH_ASSOC ) as $value ) {
|
||
$combBrutto += $value['impact_on_the_price'];
|
||
if ( $combBruttoPromo !== null ) {
|
||
$combBruttoPromo += $value['impact_on_the_price'];
|
||
}
|
||
}
|
||
}
|
||
|
||
$combNetto = \Shared\Helpers\Helpers::normalize_decimal( $combBrutto / ( 100 + $vat ) * 100, 2 );
|
||
$combNettoPromo = $combBruttoPromo !== null
|
||
? \Shared\Helpers\Helpers::normalize_decimal( $combBruttoPromo / ( 100 + $vat ) * 100, 2 )
|
||
: null;
|
||
|
||
$this->db->update( 'pp_shop_products', [
|
||
'price_netto' => $combNetto,
|
||
'price_brutto' => $combBrutto,
|
||
'price_netto_promo' => $combNettoPromo,
|
||
'price_brutto_promo' => $combBruttoPromo
|
||
], [ 'id' => $row['id'] ] );
|
||
}
|
||
}
|
||
|
||
// =========================================================================
|
||
// Frontend methods (migrated from front\factory\ShopProduct)
|
||
// =========================================================================
|
||
|
||
/**
|
||
* @return string|null
|
||
*/
|
||
public function getSkuWithFallback(int $productId, bool $withParentFallback = false)
|
||
{
|
||
if ($productId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$sku = $this->db->get('pp_shop_products', 'sku', ['id' => $productId]);
|
||
|
||
if (!$sku && $withParentFallback) {
|
||
$parentId = $this->db->get('pp_shop_products', 'parent_id', ['id' => $productId]);
|
||
if ($parentId) {
|
||
return $this->getSkuWithFallback((int)$parentId, true);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
return $sku ? (string)$sku : null;
|
||
}
|
||
|
||
/**
|
||
* @return string|null
|
||
*/
|
||
public function getEanWithFallback(int $productId, bool $withParentFallback = false)
|
||
{
|
||
if ($productId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$ean = $this->db->get('pp_shop_products', 'ean', ['id' => $productId]);
|
||
|
||
if (!$ean && $withParentFallback) {
|
||
$parentId = $this->db->get('pp_shop_products', 'parent_id', ['id' => $productId]);
|
||
if ($parentId) {
|
||
return $this->getEanWithFallback((int)$parentId, true);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
return $ean ? (string)$ean : null;
|
||
}
|
||
|
||
public function isProductActiveCached(int $productId): int
|
||
{
|
||
if ($productId <= 0) {
|
||
return 0;
|
||
}
|
||
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cacheKey = 'is_product_active:' . $productId;
|
||
$cached = $cacheHandler->get($cacheKey);
|
||
|
||
if ($cached) {
|
||
return (int)unserialize($cached) === 1 ? 1 : 0;
|
||
}
|
||
|
||
$status = $this->db->get('pp_shop_products', 'status', ['id' => $productId]);
|
||
$cacheHandler->set($cacheKey, $status);
|
||
|
||
return (int)$status === 1 ? 1 : 0;
|
||
}
|
||
|
||
/**
|
||
* @return string|null
|
||
*/
|
||
public function getMinimalPriceCached(int $productId, $priceBruttoPromo = null)
|
||
{
|
||
if ($productId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cacheKey = 'get_minimal_price:' . $productId;
|
||
$cached = $cacheHandler->get($cacheKey);
|
||
|
||
if ($cached) {
|
||
return unserialize($cached);
|
||
}
|
||
|
||
$price = $this->db->min('pp_shop_product_price_history', 'price', [
|
||
'AND' => [
|
||
'id_product' => $productId,
|
||
'price[!]' => str_replace(',', '.', $priceBruttoPromo),
|
||
],
|
||
]);
|
||
|
||
$cacheHandler->set($cacheKey, $price);
|
||
|
||
return $price;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, array<string, mixed>>
|
||
*/
|
||
public function productCategoriesFront(int $productId): array
|
||
{
|
||
if ($productId <= 0) {
|
||
return [];
|
||
}
|
||
|
||
$parentId = $this->db->get('pp_shop_products', 'parent_id', ['id' => $productId]);
|
||
$targetId = $parentId ? (int)$parentId : $productId;
|
||
|
||
$stmt = $this->db->query(
|
||
'SELECT category_id FROM pp_shop_products_categories WHERE product_id = :pid',
|
||
[':pid' => $targetId]
|
||
);
|
||
|
||
return $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||
}
|
||
|
||
/**
|
||
* @return string|null
|
||
*/
|
||
public function getProductNameCached(int $productId, string $langId)
|
||
{
|
||
if ($productId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cacheKey = 'product_name' . $langId . '_' . $productId;
|
||
$cached = $cacheHandler->get($cacheKey);
|
||
|
||
if ($cached) {
|
||
return unserialize($cached);
|
||
}
|
||
|
||
$name = $this->db->get('pp_shop_products_langs', 'name', [
|
||
'AND' => ['product_id' => $productId, 'lang_id' => $langId],
|
||
]);
|
||
|
||
$cacheHandler->set($cacheKey, $name);
|
||
|
||
return $name;
|
||
}
|
||
|
||
/**
|
||
* @return string|null
|
||
*/
|
||
public function getFirstImageCached(int $productId)
|
||
{
|
||
if ($productId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cacheKey = 'product_image:' . $productId;
|
||
$cached = $cacheHandler->get($cacheKey);
|
||
|
||
if ($cached) {
|
||
return unserialize($cached);
|
||
}
|
||
|
||
$stmt = $this->db->query(
|
||
'SELECT src FROM pp_shop_products_images WHERE product_id = :pid ORDER BY o ASC LIMIT 1',
|
||
[':pid' => $productId]
|
||
);
|
||
$rows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||
$image = isset($rows[0]['src']) ? $rows[0]['src'] : null;
|
||
|
||
$cacheHandler->set($cacheKey, $image);
|
||
|
||
return $image;
|
||
}
|
||
|
||
public function getWeightCached(int $productId)
|
||
{
|
||
if ($productId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cacheKey = 'product_wp:' . $productId;
|
||
$cached = $cacheHandler->get($cacheKey);
|
||
|
||
if ($cached) {
|
||
return unserialize($cached);
|
||
}
|
||
|
||
$wp = $this->db->get('pp_shop_products', 'wp', ['id' => $productId]);
|
||
$cacheHandler->set($cacheKey, $wp);
|
||
|
||
return $wp;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, int>
|
||
*/
|
||
public function promotedProductIdsCached(int $limit = 6): array
|
||
{
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cacheKey = 'promoted_products-' . $limit;
|
||
$cached = $cacheHandler->get($cacheKey);
|
||
|
||
if ($cached) {
|
||
return unserialize($cached);
|
||
}
|
||
|
||
$stmt = $this->db->query(
|
||
'SELECT id FROM pp_shop_products WHERE status = 1 AND promoted = 1 ORDER BY RAND() LIMIT ' . (int)$limit
|
||
);
|
||
$rows = $stmt ? $stmt->fetchAll() : [];
|
||
$products = [];
|
||
if (is_array($rows)) {
|
||
foreach ($rows as $row) {
|
||
$products[] = (int)$row['id'];
|
||
}
|
||
}
|
||
|
||
$cacheHandler->set($cacheKey, $products);
|
||
|
||
return $products;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, int>
|
||
*/
|
||
public function topProductIds(int $limit = 6): array
|
||
{
|
||
$date30 = date('Y-m-d', strtotime('-30 days'));
|
||
|
||
$stmt = $this->db->query(
|
||
"SELECT COUNT(0) AS sell_count, psop.parent_product_id
|
||
FROM pp_shop_order_products AS psop
|
||
INNER JOIN pp_shop_orders AS pso ON pso.id = psop.order_id
|
||
WHERE pso.date_order >= :d
|
||
GROUP BY parent_product_id
|
||
ORDER BY sell_count DESC",
|
||
[':d' => $date30]
|
||
);
|
||
$rows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||
|
||
$ids = [];
|
||
foreach ($rows as $row) {
|
||
if ($this->isProductActiveCached((int)$row['parent_product_id'])) {
|
||
$ids[] = (int)$row['parent_product_id'];
|
||
}
|
||
}
|
||
|
||
return $ids;
|
||
}
|
||
|
||
/**
|
||
* @return array<int, int>
|
||
*/
|
||
public function newProductIds(int $limit = 10): array
|
||
{
|
||
$stmt = $this->db->query(
|
||
'SELECT id FROM pp_shop_products WHERE status = 1 ORDER BY date_add DESC LIMIT ' . (int)$limit
|
||
);
|
||
$rows = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||
|
||
return array_column($rows, 'id');
|
||
}
|
||
|
||
/**
|
||
* @return array|null
|
||
*/
|
||
public function productDetailsFrontCached(int $productId, string $langId)
|
||
{
|
||
if ($productId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cacheKey = 'product_details_front:' . $productId . ':' . $langId;
|
||
$cached = $cacheHandler->get($cacheKey);
|
||
|
||
if ($cached) {
|
||
return unserialize($cached);
|
||
}
|
||
|
||
$product = $this->db->get('pp_shop_products', '*', ['id' => $productId]);
|
||
if (!is_array($product)) {
|
||
return null;
|
||
}
|
||
|
||
// language
|
||
$langRows = $this->db->select('pp_shop_products_langs', '*', [
|
||
'AND' => ['product_id' => $productId, 'lang_id' => $langId],
|
||
]);
|
||
if (is_array($langRows)) {
|
||
foreach ($langRows as $row) {
|
||
if ($row['copy_from']) {
|
||
$copyRows = $this->db->select('pp_shop_products_langs', '*', [
|
||
'AND' => ['product_id' => $productId, 'lang_id' => $row['copy_from']],
|
||
]);
|
||
if (is_array($copyRows)) {
|
||
foreach ($copyRows as $row2) {
|
||
$product['language'] = $row2;
|
||
}
|
||
}
|
||
} else {
|
||
$product['language'] = $row;
|
||
}
|
||
}
|
||
}
|
||
|
||
// attributes
|
||
$attrStmt = $this->db->query(
|
||
'SELECT DISTINCT(attribute_id) FROM pp_shop_products_attributes AS pspa '
|
||
. 'INNER JOIN pp_shop_attributes AS psa ON psa.id = pspa.attribute_id '
|
||
. 'WHERE product_id = ' . $productId . ' ORDER BY o ASC'
|
||
);
|
||
$attrRows = $attrStmt ? $attrStmt->fetchAll() : [];
|
||
if (is_array($attrRows)) {
|
||
foreach ($attrRows as $row) {
|
||
$row['type'] = $this->db->get('pp_shop_attributes', 'type', ['id' => $row['attribute_id']]);
|
||
$row['language'] = $this->db->get('pp_shop_attributes_langs', ['name'], [
|
||
'AND' => ['attribute_id' => $row['attribute_id'], 'lang_id' => $langId],
|
||
]);
|
||
|
||
$valStmt = $this->db->query(
|
||
'SELECT value_id, is_default FROM pp_shop_products_attributes AS pspa '
|
||
. 'INNER JOIN pp_shop_attributes_values AS psav ON psav.id = pspa.value_id '
|
||
. 'WHERE product_id = :pid AND pspa.attribute_id = :aid',
|
||
[':pid' => $productId, ':aid' => $row['attribute_id']]
|
||
);
|
||
$valRows = $valStmt ? $valStmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||
if (is_array($valRows)) {
|
||
foreach ($valRows as $row2) {
|
||
$row2['language'] = $this->db->get('pp_shop_attributes_values_langs', ['name', 'value'], [
|
||
'AND' => ['value_id' => $row2['value_id'], 'lang_id' => $langId],
|
||
]);
|
||
$row['values'][] = $row2;
|
||
}
|
||
}
|
||
|
||
$product['attributes'][] = $row;
|
||
}
|
||
}
|
||
|
||
$product['images'] = $this->db->select('pp_shop_products_images', '*', [
|
||
'product_id' => $productId,
|
||
'ORDER' => ['o' => 'ASC', 'id' => 'ASC'],
|
||
]);
|
||
$product['files'] = $this->db->select('pp_shop_products_files', '*', ['product_id' => $productId]);
|
||
$product['categories'] = $this->db->select('pp_shop_products_categories', 'category_id', ['product_id' => $productId]);
|
||
$product['products_related'] = $this->db->select('pp_shop_products_related', 'product_related_id', ['product_id' => $productId]);
|
||
|
||
$setId = (int)($product['set_id'] ?? 0);
|
||
$productsSets = $this->db->select('pp_shop_product_sets_products', 'product_id', ['set_id' => $setId]);
|
||
$product['products_sets'] = is_array($productsSets) ? array_unique($productsSets) : [];
|
||
|
||
$attributes = $this->db->select('pp_shop_products_attributes', ['attribute_id', 'value_id'], ['product_id' => $productId]);
|
||
$attributesTmp = [];
|
||
if (is_array($attributes)) {
|
||
foreach ($attributes as $attr) {
|
||
$attributesTmp[$attr['attribute_id']][] = $attr['value_id'];
|
||
}
|
||
}
|
||
if (!empty($attributesTmp)) {
|
||
$product['permutations'] = \Shared\Helpers\Helpers::array_cartesian_product($attributesTmp);
|
||
}
|
||
|
||
$cacheHandler->set($cacheKey, $product);
|
||
|
||
return $product;
|
||
}
|
||
|
||
/**
|
||
* @return string|null
|
||
*/
|
||
public function getWarehouseMessageZero(int $productId, string $langId)
|
||
{
|
||
if ($productId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
return $this->db->get('pp_shop_products_langs', 'warehouse_message_zero', [
|
||
'AND' => ['product_id' => $productId, 'lang_id' => $langId],
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* @return string|null
|
||
*/
|
||
public function getWarehouseMessageNonzero(int $productId, string $langId)
|
||
{
|
||
if ($productId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
return $this->db->get('pp_shop_products_langs', 'warehouse_message_nonzero', [
|
||
'AND' => ['product_id' => $productId, 'lang_id' => $langId],
|
||
]);
|
||
}
|
||
|
||
public function findCustomFieldCached(int $customFieldId)
|
||
{
|
||
if ($customFieldId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cacheKey = 'custom_field:' . $customFieldId;
|
||
$cached = $cacheHandler->get($cacheKey);
|
||
|
||
if ($cached) {
|
||
return unserialize($cached);
|
||
}
|
||
|
||
$result = $this->db->get('pp_shop_products_custom_fields', '*', ['id_additional_field' => $customFieldId]);
|
||
if (!is_array($result)) {
|
||
return null;
|
||
}
|
||
|
||
$cacheHandler->set($cacheKey, $result, 60 * 60 * 24);
|
||
|
||
return $result;
|
||
}
|
||
|
||
// =========================================================================
|
||
// Migrated from \shop\Product
|
||
// =========================================================================
|
||
|
||
/**
|
||
* Replacement for $this->findCached() — returns array instead of Product object.
|
||
* Uses same Redis cache key pattern: shop\product:{id}:{lang}:{hash}
|
||
*/
|
||
public function findCached(int $productId, string $langId = null, string $permutationHash = null)
|
||
{
|
||
if ($productId <= 0) {
|
||
return null;
|
||
}
|
||
|
||
if (!$langId) {
|
||
$langId = (new \Domain\Languages\LanguagesRepository($this->db))->defaultLanguage();
|
||
}
|
||
|
||
$cacheKey = "shop\\product:$productId:$langId:$permutationHash";
|
||
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cached = $cacheHandler->get($cacheKey);
|
||
|
||
if ($cached) {
|
||
$data = unserialize($cached);
|
||
// Legacy cache may contain old \shop\Product objects — invalidate and re-fetch
|
||
if (is_object($data)) {
|
||
$cacheHandler->delete($cacheKey);
|
||
// Fall through to re-fetch from DB
|
||
} elseif (is_array($data)) {
|
||
return $data;
|
||
}
|
||
}
|
||
|
||
$product = $this->db->get('pp_shop_products', '*', ['id' => $productId]);
|
||
if (!is_array($product)) {
|
||
return null;
|
||
}
|
||
|
||
$effectiveId = $productId;
|
||
|
||
// Combination — load data from parent
|
||
if ($product['parent_id']) {
|
||
$effectiveId = (int) $product['parent_id'];
|
||
|
||
if (!$product['price_netto'] || !$product['price_brutto']) {
|
||
$parentPrices = $this->db->get('pp_shop_products', ['price_netto', 'price_brutto', 'price_netto_promo', 'price_brutto_promo', 'vat', 'wp'], ['id' => $effectiveId]);
|
||
if (is_array($parentPrices)) {
|
||
foreach ($parentPrices as $k => $v) {
|
||
$product[$k] = $v;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Language
|
||
$langRows = $this->db->select('pp_shop_products_langs', '*', ['AND' => ['product_id' => $effectiveId, 'lang_id' => $langId]]);
|
||
if (\Shared\Helpers\Helpers::is_array_fix($langRows)) {
|
||
foreach ($langRows as $row) {
|
||
if ($row['copy_from']) {
|
||
$copyRows = $this->db->select('pp_shop_products_langs', '*', ['AND' => ['product_id' => $effectiveId, 'lang_id' => $row['copy_from']]]);
|
||
if (is_array($copyRows)) {
|
||
foreach ($copyRows as $row2) {
|
||
$product['language'] = $row2;
|
||
}
|
||
}
|
||
} else {
|
||
$product['language'] = $row;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Images, files, categories, related, sets
|
||
$product['images'] = $this->db->select('pp_shop_products_images', '*', ['product_id' => $effectiveId, 'ORDER' => ['o' => 'ASC', 'id' => 'ASC']]);
|
||
$product['files'] = $this->db->select('pp_shop_products_files', '*', ['product_id' => $effectiveId]);
|
||
$product['categories'] = $this->db->select('pp_shop_products_categories', 'category_id', ['product_id' => $effectiveId]);
|
||
$product['products_related'] = $this->db->select('pp_shop_products_related', 'product_related_id', ['product_id' => $effectiveId]);
|
||
$product['products_sets'] = $this->db->select('pp_shop_product_sets_products', 'product_id', ['AND' => ['set_id' => (int) ($product['set_id'] ?? 0), 'product_id[!]' => $effectiveId]]);
|
||
|
||
// Product combinations (only for main products)
|
||
if (!$product['parent_id']) {
|
||
$combIds = $this->db->select('pp_shop_products', 'id', ['parent_id' => $productId]);
|
||
if (\Shared\Helpers\Helpers::is_array_fix($combIds)) {
|
||
$combos = [];
|
||
foreach ($combIds as $combId) {
|
||
$combos[] = $this->findCached((int) $combId, $langId);
|
||
}
|
||
$product['product_combinations'] = $combos;
|
||
}
|
||
}
|
||
|
||
// Producer
|
||
$producer = $this->db->get('pp_shop_producer', '*', ['id' => (int) ($product['producer_id'] ?? 0)]);
|
||
$producerLang = $this->db->get('pp_shop_producer_lang', '*', ['AND' => ['producer_id' => (int) ($product['producer_id'] ?? 0), 'lang_id' => $langId]]);
|
||
if (is_array($producer)) {
|
||
$producer['description'] = is_array($producerLang) ? ($producerLang['description'] ?? null) : null;
|
||
$producer['data'] = is_array($producerLang) ? ($producerLang['data'] ?? null) : null;
|
||
$producer['meta_title'] = is_array($producerLang) ? ($producerLang['meta_title'] ?? null) : null;
|
||
}
|
||
$product['producer'] = $producer;
|
||
|
||
// Permutation price overrides
|
||
if ($permutationHash) {
|
||
$permPrices = $this->db->get('pp_shop_products', ['price_netto', 'price_brutto', 'price_netto_promo', 'price_brutto_promo'], ['AND' => ['permutation_hash' => $permutationHash, 'parent_id' => $productId]]);
|
||
if (is_array($permPrices)) {
|
||
if ($permPrices['price_netto'] !== null) $product['price_netto'] = $permPrices['price_netto'];
|
||
if ($permPrices['price_brutto'] !== null) $product['price_brutto'] = $permPrices['price_brutto'];
|
||
if ($permPrices['price_netto_promo'] !== null) $product['price_netto_promo'] = $permPrices['price_netto_promo'];
|
||
if ($permPrices['price_brutto_promo'] !== null) $product['price_brutto_promo'] = $permPrices['price_brutto_promo'];
|
||
}
|
||
}
|
||
|
||
// Custom fields
|
||
$product['custom_fields'] = $this->db->select('pp_shop_products_custom_fields', '*', ['id_product' => $productId]);
|
||
|
||
$cacheHandler->set($cacheKey, $product);
|
||
|
||
return $product;
|
||
}
|
||
|
||
public function isProductOnPromotion(int $productId): bool
|
||
{
|
||
return $this->db->get('pp_shop_products', 'price_netto_promo', ['id' => $productId]) !== null;
|
||
}
|
||
|
||
public function productSetsWhenAddToBasket(int $productId): string
|
||
{
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cacheKey = "ProductRepository::productSetsWhenAddToBasket:$productId";
|
||
|
||
$objectData = $cacheHandler->get($cacheKey);
|
||
|
||
if (!$objectData) {
|
||
$parentId = $this->db->get('pp_shop_products', 'parent_id', ['id' => $productId]);
|
||
if ($parentId) {
|
||
$setId = $this->db->get('pp_shop_products', 'set_id', ['id' => $parentId]);
|
||
} else {
|
||
$setId = $this->db->get('pp_shop_products', 'set_id', ['id' => $productId]);
|
||
}
|
||
|
||
$products = $this->db->select('pp_shop_product_sets_products', 'product_id', ['set_id' => $setId]);
|
||
if (!$products) {
|
||
$products_intersection = $this->db->select('pp_shop_orders_products_intersection', ['product_1_id', 'product_2_id'], ['OR' => ['product_1_id' => $productId, 'product_2_id' => $productId], 'ORDER' => ['count' => 'DESC'], 'LIMIT' => 5]);
|
||
if (!count($products_intersection)) {
|
||
$parentId2 = $this->db->get('pp_shop_products', 'parent_id', ['id' => $productId]);
|
||
$products_intersection = $this->db->select('pp_shop_orders_products_intersection', ['product_1_id', 'product_2_id'], ['OR' => ['product_1_id' => $parentId2, 'product_2_id' => $parentId2], 'ORDER' => ['count' => 'DESC'], 'LIMIT' => 5]);
|
||
}
|
||
|
||
$products = [];
|
||
foreach ($products_intersection as $pi) {
|
||
if ($pi['product_1_id'] != $productId) {
|
||
$products[] = $pi['product_1_id'];
|
||
} else {
|
||
$products[] = $pi['product_2_id'];
|
||
}
|
||
}
|
||
$products = array_unique($products);
|
||
}
|
||
|
||
$cacheHandler->set($cacheKey, $products);
|
||
} else {
|
||
$products = unserialize($objectData);
|
||
}
|
||
|
||
if (is_array($products)) {
|
||
foreach ($products as $k => $pid) {
|
||
if (!$this->isProductActiveCached((int) $pid)) {
|
||
unset($products[$k]);
|
||
}
|
||
}
|
||
}
|
||
|
||
return \Shared\Tpl\Tpl::view('shop-basket/alert-product-sets', [
|
||
'products' => $products,
|
||
]);
|
||
}
|
||
|
||
public function addVisit(int $productId): void
|
||
{
|
||
$this->db->update('pp_shop_products', ['visits[+]' => 1], ['id' => $productId]);
|
||
}
|
||
|
||
public function getProductImg(int $productId)
|
||
{
|
||
$rows = $this->db->select('pp_shop_products_images', 'src', ['product_id' => $productId, 'ORDER' => ['o' => 'ASC']]);
|
||
if (\Shared\Helpers\Helpers::is_array_fix($rows)) {
|
||
foreach ($rows as $row) {
|
||
return $row;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
public function getProductUrl(int $productId): string
|
||
{
|
||
$langId = (new \Domain\Languages\LanguagesRepository($this->db))->defaultLanguage();
|
||
|
||
$langRows = $this->db->select('pp_shop_products_langs', '*', ['AND' => ['product_id' => $productId, 'lang_id' => $langId]]);
|
||
$language = null;
|
||
if (\Shared\Helpers\Helpers::is_array_fix($langRows)) {
|
||
foreach ($langRows as $row) {
|
||
if ($row['copy_from']) {
|
||
$copyRows = $this->db->select('pp_shop_products_langs', '*', ['AND' => ['product_id' => $productId, 'lang_id' => $row['copy_from']]]);
|
||
if (is_array($copyRows)) {
|
||
foreach ($copyRows as $row2) {
|
||
$language = $row2;
|
||
}
|
||
}
|
||
} else {
|
||
$language = $row;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($language && $language['seo_link']) {
|
||
return '/' . $language['seo_link'];
|
||
}
|
||
return '/p-' . $productId . '-' . \Shared\Helpers\Helpers::seo($language ? $language['name'] : '');
|
||
}
|
||
|
||
public function searchProductsByNameCount(string $query, string $langId): int
|
||
{
|
||
$stmt = $this->db->query('SELECT COUNT(0) AS c FROM ( '
|
||
. 'SELECT psp.id, '
|
||
. '( CASE '
|
||
. 'WHEN copy_from IS NULL THEN name '
|
||
. 'WHEN copy_from IS NOT NULL THEN ( '
|
||
. 'SELECT name FROM pp_shop_products_langs WHERE lang_id = pspl.copy_from AND product_id = psp.id '
|
||
. ') '
|
||
. 'END ) AS name '
|
||
. 'FROM pp_shop_products AS psp '
|
||
. 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = psp.id '
|
||
. 'WHERE status = 1 AND name LIKE :query AND lang_id = :lang_id '
|
||
. ') AS q1', [
|
||
':query' => '%' . $query . '%',
|
||
':lang_id' => $langId,
|
||
]);
|
||
$results = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||
|
||
return (int) ($results[0]['c'] ?? 0);
|
||
}
|
||
|
||
public function getProductsIdByName(string $query, string $langId, int $limit, int $from): array
|
||
{
|
||
$stmt = $this->db->query('SELECT psp.id, '
|
||
. '( CASE '
|
||
. 'WHEN copy_from IS NULL THEN name '
|
||
. 'WHEN copy_from IS NOT NULL THEN ( '
|
||
. 'SELECT name FROM pp_shop_products_langs WHERE lang_id = pspl.copy_from AND product_id = psp.id '
|
||
. ') '
|
||
. 'END ) AS name '
|
||
. 'FROM pp_shop_products AS psp '
|
||
. 'INNER JOIN pp_shop_products_langs AS pspl ON pspl.product_id = psp.id '
|
||
. 'WHERE status = 1 AND name LIKE :query AND lang_id = :lang_id '
|
||
. 'ORDER BY name ASC '
|
||
. 'LIMIT ' . (int) $from . ',' . (int) $limit, [
|
||
':query' => '%' . $query . '%',
|
||
':lang_id' => $langId,
|
||
]);
|
||
$results = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||
|
||
$output = [];
|
||
if (is_array($results)) {
|
||
foreach ($results as $row) {
|
||
$output[] = $row['id'];
|
||
}
|
||
}
|
||
return $output;
|
||
}
|
||
|
||
public function searchProductsByName(string $query, string $langId, int $page = 0): array
|
||
{
|
||
$count = $this->searchProductsByNameCount($query, $langId);
|
||
$ls = ceil($count / 12);
|
||
|
||
if ($page < 1) {
|
||
$page = 1;
|
||
} elseif ($page > $ls) {
|
||
$page = (int) $ls;
|
||
}
|
||
|
||
$from = 12 * ($page - 1);
|
||
if ($from < 0) {
|
||
$from = 0;
|
||
}
|
||
|
||
return [
|
||
'products' => $this->getProductsIdByName($query, $langId, 12, $from),
|
||
'count' => $count,
|
||
'ls' => $ls,
|
||
];
|
||
}
|
||
|
||
public function searchProductByNameAjax(string $query, string $langId): array
|
||
{
|
||
$stmt = $this->db->query(
|
||
'SELECT product_id FROM pp_shop_products_langs AS pspl '
|
||
. 'INNER JOIN pp_shop_products AS psp ON psp.id = pspl.product_id '
|
||
. 'WHERE status = 1 AND lang_id = :lang_id AND LOWER(name) LIKE :query '
|
||
. 'ORDER BY visits DESC LIMIT 12',
|
||
[':query' => '%' . $query . '%', ':lang_id' => $langId]
|
||
);
|
||
$results = $stmt ? $stmt->fetchAll(\PDO::FETCH_ASSOC) : [];
|
||
|
||
return is_array($results) ? $results : [];
|
||
}
|
||
|
||
public function isStock0Buy(int $productId)
|
||
{
|
||
$parentId = $this->db->get('pp_shop_products', 'parent_id', ['id' => $productId]);
|
||
if ($parentId) {
|
||
return $this->db->get('pp_shop_products', 'stock_0_buy', ['id' => $parentId]);
|
||
}
|
||
return $this->db->get('pp_shop_products', 'stock_0_buy', ['id' => $productId]);
|
||
}
|
||
|
||
public function getProductPermutationQuantityOptions(int $productId, $permutation)
|
||
{
|
||
global $settings;
|
||
|
||
$cacheHandler = new \Shared\Cache\CacheHandler();
|
||
$cacheKey = "ProductRepository::getProductPermutationQuantityOptions:v2:$productId:$permutation";
|
||
|
||
$objectData = $cacheHandler->get($cacheKey);
|
||
|
||
if ($objectData) {
|
||
return unserialize($objectData);
|
||
}
|
||
|
||
$result = [];
|
||
|
||
if ($this->db->count('pp_shop_products', ['AND' => ['parent_id' => $productId, 'permutation_hash' => $permutation]])) {
|
||
$result['quantity'] = $this->db->get('pp_shop_products', 'quantity', ['AND' => ['parent_id' => $productId, 'permutation_hash' => $permutation]]);
|
||
$result['stock_0_buy'] = $this->db->get('pp_shop_products', 'stock_0_buy', ['AND' => ['parent_id' => $productId, 'permutation_hash' => $permutation]]);
|
||
|
||
if ($result['quantity'] === null) {
|
||
$result['quantity'] = $this->db->get('pp_shop_products', 'quantity', ['id' => $productId]);
|
||
if ($result['stock_0_buy'] === null) {
|
||
$result['stock_0_buy'] = $this->db->get('pp_shop_products', 'stock_0_buy', ['id' => $productId]);
|
||
}
|
||
}
|
||
} else {
|
||
$result['quantity'] = $this->db->get('pp_shop_products', 'quantity', ['id' => $productId]);
|
||
$result['stock_0_buy'] = $this->db->get('pp_shop_products', 'stock_0_buy', ['id' => $productId]);
|
||
}
|
||
|
||
$result['messages'] = $this->db->get('pp_shop_products_langs', ['warehouse_message_zero', 'warehouse_message_nonzero'], ['AND' => ['product_id' => $productId, 'lang_id' => 'pl']]);
|
||
if (!isset($result['messages']['warehouse_message_zero']) || !$result['messages']['warehouse_message_zero']) {
|
||
$result['messages']['warehouse_message_zero'] = isset($settings['warehouse_message_zero_pl']) ? $settings['warehouse_message_zero_pl'] : '';
|
||
}
|
||
if (!isset($result['messages']['warehouse_message_nonzero']) || !$result['messages']['warehouse_message_nonzero']) {
|
||
$result['messages']['warehouse_message_nonzero'] = isset($settings['warehouse_message_nonzero_pl']) ? $settings['warehouse_message_nonzero_pl'] : '';
|
||
}
|
||
|
||
$cacheHandler->set($cacheKey, $result);
|
||
|
||
return $result;
|
||
}
|
||
|
||
public function getProductIdByAttributes(int $parentId, array $attributes)
|
||
{
|
||
return $this->db->get('pp_shop_products', 'id', ['AND' => ['parent_id' => $parentId, 'permutation_hash' => implode('|', $attributes)]]);
|
||
}
|
||
|
||
public function getProductPermutationHash(int $productId)
|
||
{
|
||
return $this->db->get('pp_shop_products', 'permutation_hash', ['id' => $productId]);
|
||
}
|
||
|
||
/**
|
||
* Build attribute list from product combinations (array of arrays with permutation_hash).
|
||
*/
|
||
public function getProductAttributes($products)
|
||
{
|
||
if (!is_array($products) || !count($products)) {
|
||
return false;
|
||
}
|
||
|
||
$attrRepo = new \Domain\Attribute\AttributeRepository($this->db);
|
||
$attributes = [];
|
||
|
||
foreach ($products as $product) {
|
||
$hash = is_array($product) ? ($product['permutation_hash'] ?? '') : (is_object($product) ? $product->permutation_hash : '');
|
||
$permutations = explode('|', $hash);
|
||
foreach ($permutations as $permutation) {
|
||
$parts = explode('-', $permutation);
|
||
if (count($parts) < 2) continue;
|
||
|
||
$value = [];
|
||
$value['id'] = $parts[1];
|
||
$value['is_default'] = $attrRepo->isValueDefault((int) $parts[1]);
|
||
|
||
if (array_search($parts[1], array_column($attributes, 'id')) === false) {
|
||
$attributes[$parts[0]][] = $value;
|
||
}
|
||
}
|
||
}
|
||
|
||
$attributes = \Shared\Helpers\Helpers::removeDuplicates($attributes, 'id');
|
||
|
||
$toSort = [];
|
||
foreach ($attributes as $key => $val) {
|
||
$row = [];
|
||
$row['id'] = $key;
|
||
$row['values'] = $val;
|
||
$toSort[] = ['order' => (int) $attrRepo->getAttributeOrder((int) $key), 'data' => $row];
|
||
}
|
||
usort($toSort, function ($a, $b) { return $a['order'] - $b['order']; });
|
||
|
||
$sorted = [];
|
||
foreach ($toSort as $i => $item) {
|
||
$sorted[$i + 1] = $item['data'];
|
||
}
|
||
|
||
return $sorted;
|
||
}
|
||
|
||
public function generateSkuCode(): string
|
||
{
|
||
$skus = $this->db->select('pp_shop_products', 'sku', ['sku[~]' => 'PP-']);
|
||
$codes = [];
|
||
if (is_array($skus)) {
|
||
foreach ($skus as $sku) {
|
||
$codes[] = (int) substr($sku, 3);
|
||
}
|
||
}
|
||
|
||
if (!empty($codes)) {
|
||
return 'PP-' . str_pad(max($codes) + 1, 6, '0', STR_PAD_LEFT);
|
||
}
|
||
return 'PP-000001';
|
||
}
|
||
|
||
public function productMeta(int $productId): array
|
||
{
|
||
$result = $this->db->select(
|
||
'pp_shop_products_categories (ppc)',
|
||
['[>]pp_shop_categories_langs (pcl)' => ['ppc.category_id' => 'category_id']],
|
||
['pcl.title', 'pcl.seo_link'],
|
||
['ppc.product_id' => $productId]
|
||
);
|
||
return is_array($result) ? $result : [];
|
||
}
|
||
|
||
public function generateSubtitleFromAttributes(string $permutationHash, string $langId = null): string
|
||
{
|
||
if (!$langId) {
|
||
global $lang_id;
|
||
$langId = $lang_id;
|
||
}
|
||
|
||
$subtitle = '';
|
||
$attrRepo = new \Domain\Attribute\AttributeRepository($this->db);
|
||
$parts = explode('|', $permutationHash);
|
||
|
||
foreach ($parts as $part) {
|
||
$attr = explode('-', $part);
|
||
if (count($attr) < 2) continue;
|
||
|
||
if ($subtitle) {
|
||
$subtitle .= ', ';
|
||
}
|
||
$subtitle .= $attrRepo->getAttributeNameById((int) $attr[0], $langId) . ': ' . $attrRepo->getAttributeValueById((int) $attr[1], $langId);
|
||
}
|
||
|
||
return $subtitle;
|
||
}
|
||
|
||
public function getDefaultCombinationPrices(array $product): array
|
||
{
|
||
$prices = [];
|
||
if (!isset($product['product_combinations']) || !is_array($product['product_combinations'])) {
|
||
return $prices;
|
||
}
|
||
|
||
$permutationHash = '';
|
||
$attributes = $this->getProductAttributes($product['product_combinations']);
|
||
if (is_array($attributes)) {
|
||
foreach ($attributes as $attribute) {
|
||
foreach ($attribute['values'] as $value) {
|
||
if ($value['is_default']) {
|
||
if ($permutationHash) {
|
||
$permutationHash .= '|';
|
||
}
|
||
$permutationHash .= $attribute['id'] . '-' . $value['id'];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
foreach ($product['product_combinations'] as $combo) {
|
||
$comboHash = is_array($combo) ? ($combo['permutation_hash'] ?? '') : (is_object($combo) ? $combo->permutation_hash : '');
|
||
if ($comboHash == $permutationHash) {
|
||
$prices['price_netto'] = is_array($combo) ? ($combo['price_netto'] ?? null) : $combo->price_netto;
|
||
$prices['price_brutto'] = is_array($combo) ? ($combo['price_brutto'] ?? null) : $combo->price_brutto;
|
||
$prices['price_netto_promo'] = is_array($combo) ? ($combo['price_netto_promo'] ?? null) : $combo->price_netto_promo;
|
||
$prices['price_brutto_promo'] = is_array($combo) ? ($combo['price_brutto_promo'] ?? null) : $combo->price_brutto_promo;
|
||
}
|
||
}
|
||
|
||
return $prices;
|
||
}
|
||
|
||
public function getProductDataBySelectedAttributes(array $product, string $selectedAttribute): array
|
||
{
|
||
global $settings;
|
||
|
||
$result = [];
|
||
|
||
if (isset($product['product_combinations']) && is_array($product['product_combinations'])) {
|
||
foreach ($product['product_combinations'] as $combo) {
|
||
$comboHash = is_array($combo) ? ($combo['permutation_hash'] ?? '') : (is_object($combo) ? $combo->permutation_hash : '');
|
||
if ($comboHash == $selectedAttribute) {
|
||
$comboQty = is_array($combo) ? ($combo['quantity'] ?? null) : $combo->quantity;
|
||
$comboS0B = is_array($combo) ? ($combo['stock_0_buy'] ?? null) : $combo->stock_0_buy;
|
||
|
||
if ($comboQty !== null || $comboS0B) {
|
||
$result['quantity'] = $comboQty;
|
||
$result['stock_0_buy'] = $comboS0B;
|
||
$result['price_netto'] = \Shared\Helpers\Helpers::shortPrice(is_array($combo) ? $combo['price_netto'] : $combo->price_netto);
|
||
$result['price_brutto'] = \Shared\Helpers\Helpers::shortPrice(is_array($combo) ? $combo['price_brutto'] : $combo->price_brutto);
|
||
$pnp = is_array($combo) ? ($combo['price_netto_promo'] ?? null) : $combo->price_netto_promo;
|
||
$pbp = is_array($combo) ? ($combo['price_brutto_promo'] ?? null) : $combo->price_brutto_promo;
|
||
$result['price_netto_promo'] = $pnp ? \Shared\Helpers\Helpers::shortPrice($pnp) : null;
|
||
$result['price_brutto_promo'] = $pbp ? \Shared\Helpers\Helpers::shortPrice($pbp) : null;
|
||
} else {
|
||
$result['quantity'] = $product['quantity'] ?? null;
|
||
$result['stock_0_buy'] = $product['stock_0_buy'] ?? null;
|
||
$result['price_netto'] = \Shared\Helpers\Helpers::shortPrice($product['price_netto'] ?? 0);
|
||
$result['price_brutto'] = \Shared\Helpers\Helpers::shortPrice($product['price_brutto'] ?? 0);
|
||
$result['price_netto_promo'] = ($product['price_netto_promo'] ?? null) ? \Shared\Helpers\Helpers::shortPrice($product['price_netto_promo']) : null;
|
||
$result['price_brutto_promo'] = ($product['price_brutto_promo'] ?? null) ? \Shared\Helpers\Helpers::shortPrice($product['price_brutto_promo']) : null;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
$lang = isset($product['language']) ? $product['language'] : [];
|
||
$result['messages']['warehouse_message_zero'] = !empty($lang['warehouse_message_zero']) ? $lang['warehouse_message_zero'] : (isset($settings['warehouse_message_zero_pl']) ? $settings['warehouse_message_zero_pl'] : '');
|
||
$result['messages']['warehouse_message_nonzero'] = !empty($lang['warehouse_message_nonzero']) ? $lang['warehouse_message_nonzero'] : (isset($settings['warehouse_message_nonzero_pl']) ? $settings['warehouse_message_nonzero_pl'] : '');
|
||
$result['permutation_hash'] = $selectedAttribute;
|
||
|
||
return $result;
|
||
}
|
||
|
||
public function productCategories(int $productId): array
|
||
{
|
||
$result = $this->db->select('pp_shop_products_categories', 'category_id', ['product_id' => $productId]);
|
||
return is_array($result) ? $result : [];
|
||
}
|
||
|
||
public static function arrayCartesian(array $input): array
|
||
{
|
||
$result = [];
|
||
|
||
foreach ($input as $key => $values) {
|
||
if (empty($values)) {
|
||
continue;
|
||
}
|
||
|
||
if (empty($result)) {
|
||
foreach ($values as $value) {
|
||
$result[] = [$key => $value];
|
||
}
|
||
} else {
|
||
$append = [];
|
||
foreach ($result as &$product) {
|
||
$product[$key] = array_shift($values);
|
||
$copy = $product;
|
||
|
||
foreach ($values as $item) {
|
||
$copy[$key] = $item;
|
||
$append[] = $copy;
|
||
}
|
||
|
||
array_unshift($values, $product[$key]);
|
||
}
|
||
$result = array_merge($result, $append);
|
||
}
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
}
|