Files
pomysloweprezenty.pl/autoload/Domain/Product/ProductRepository.php
2026-03-19 19:35:09 +01:00

3598 lines
140 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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'],
'type' => $row['type'] ?? 'text',
'is_required' => $row['is_required'] ?? 0,
] );
}
}
// 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( '&', '&amp;', $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( '&', '&amp;', $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;
}
}