Files
kupshop/class/class.ProductList.php
2025-08-02 16:30:27 +02:00

653 lines
22 KiB
PHP

<?php
use KupShop\CatalogBundle\ProductList\MultiFetch;
use KupShop\CatalogBundle\ProductList\ProductCollection;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\PricelistBundle\Context\PricelistContext;
use Query\Operator;
class ProductListBase
{
/** @var callable[] */
protected $specs = [];
/** @var callable[] */
protected $resultModifiers = [];
/** @var bool */
protected $variationsAsResult;
/** @var MultiFetch */
protected $multiFetch;
public function __construct($variationsAsResult = false)
{
$this->variationsAsResult = $variationsAsResult;
$this->multiFetch = ServiceContainer::getService(MultiFetch::class);
$this->addResultModifiers(fn (ProductCollection $products) => $this->multiFetch->fetchDeliveryText($products));
}
public function fetchSets($showOutOfStock = null)
{
$this->resultModifiers[] = function (ProductCollection $products) use ($showOutOfStock) {
$this->multiFetch->fetchSets($products, $showOutOfStock);
};
return $this;
}
public function fetchStoresInStore(bool $frontedQuantity = true): ProductListBase
{
if (findModule(Modules::STORES)) {
$this->resultModifiers[] = function (ProductCollection $products) use ($frontedQuantity) {
$this->multiFetch->fetchStoresInStore($products, $this->variationsAsResult, frontedQuantity: $frontedQuantity);
};
}
return $this;
}
public function fetchPriceLevelPrice($priceLevelID): ProductListBase
{
if (findModule(Modules::PRICE_LEVELS)) {
$this->resultModifiers[] = function (ProductCollection $products) use ($priceLevelID) {
$this->multiFetch->fetchPriceLevelPrice($products, $priceLevelID);
};
}
return $this;
}
/** Create default FilterParams and attach it to ProductList.
* @return FilterParams
*/
public function applyDefaultFilterParams()
{
$filterParams = \FilterParams::createDefault($this->variationsAsResult ? \FilterParams::ENTITY_VARIATION : \FilterParams::ENTITY_PRODUCT);
$this->andSpec(function () use ($filterParams) {
return $filterParams->getSpec();
});
return $filterParams;
}
public function fetchImages($mainSize, $imageKind = null, bool $fallbackToProductPhoto = false)
{
$this->andSpec(function (Query\QueryBuilder $qb) use ($imageKind, $fallbackToProductPhoto) {
$qb->addSelect(\Query\Product::withProductPhotoId($this->variationsAsResult, $imageKind, $fallbackToProductPhoto));
});
$this->resultModifiers[] = function (ProductCollection $products) use ($mainSize) {
/** @var Product $product */
foreach ($products as $product) {
$product->fetchImages($mainSize);
}
};
return $this;
}
public function fetchOtherImages($size, $type = null)
{
$this->resultModifiers[] = function (ProductCollection $products) use ($size, $type) {
$this->multiFetch->fetchOtherImages($products, $size, $type);
};
return $this;
}
public function fetchVariations($labels, bool $filterOnlyVisible = true)
{
$this->resultModifiers[] = function (ProductCollection $products) use ($labels, $filterOnlyVisible) {
$this->multiFetch->fetchVariations($products, $labels, $filterOnlyVisible);
};
return $this;
}
public function fetchVariationsEntities(bool $filterOnlyVisible = true)
{
$this->resultModifiers[] = function (ProductCollection $products) use ($filterOnlyVisible) {
$this->multiFetch->fetchVariationsEntities($products, $filterOnlyVisible);
};
return $this;
}
public function fetchParameters($parameters = null)
{
$this->resultModifiers[] = function (ProductCollection $products) use ($parameters) {
$this->multiFetch->fetchParameters($products, $parameters);
};
return $this;
}
public function fetchProductOfSuppliersInStore()
{
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchProductOfSuppliersInStore($products);
};
return $this;
}
public function fetchSections()
{
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchSections($products);
};
return $this;
}
public function fetchActiveSellerInStore()
{
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchActiveSellerInStore($products);
};
return $this;
}
public function fetchProductLabels(?string $visibility = null)
{
$this->resultModifiers[] = function (ProductCollection $products) use ($visibility) {
$this->multiFetch->fetchProductLabels($products, $visibility);
};
return $this;
}
public function fetchCollections()
{
$this->resultModifiers[] = function (ProductCollection $products) {
/* @var Product $product */
$this->multiFetch->fetchCollections($products);
};
return $this;
}
public function fetchProductsRelated()
{
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchProductsRelated($products);
};
}
public function fetchProducers()
{
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchProducers($products);
};
return $this;
}
public function fetchReviews()
{
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchRating($products);
};
}
public function fetchPhotos($size, $type = null)
{
$this->resultModifiers[] = function (ProductCollection $products) use ($type, $size) {
$this->multiFetch->fetchOtherImages($products, $size, $type);
};
}
public function fetchLinks()
{
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchLinks($products);
};
return $this;
}
public function fetchCharges()
{
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchProductsCharges($products);
};
return $this;
}
public function fetchDescriptionPlus(): ProductListBase
{
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchDescriptionPlus($products);
};
return $this;
}
public function fetchAttachments(): ProductListBase
{
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchAttachments($products);
};
return $this;
}
public function fetchVariationImages(int $size, ?array $type = null): self
{
$this->resultModifiers[] = function (ProductCollection $products) use ($size, $type) {
$this->multiFetch->fetchVariationImages($products, $size, $type);
};
return $this;
}
public function fetchConvertorsValues($ids = [], bool $showHiddenVariations = false)
{
if (findModule(Modules::CONVERTORS)) {
$this->resultModifiers[] = function (ProductCollection $products) use ($ids, $showHiddenVariations) {
$this->multiFetch->fetchConvertorsValues($products, $ids, showHiddenVariations: $showHiddenVariations);
};
}
}
public function fetchMeasureUnits()
{
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS)) {
$this->resultModifiers[] = function (ProductCollection $products) {
$this->multiFetch->fetchMeasureUnits($products);
};
}
}
public function orderBy($sort, $order = null)
{
$this->andSpec(function (Query\QueryBuilder $qb) use ($sort, $order) {
$qb->orderBy($sort, $order);
});
return $this;
}
public function limit($count, $offset = null)
{
$this->andSpec(function (Query\QueryBuilder $qb) use ($count, $offset) {
if (isset($offset)) {
$qb->setFirstResult($offset);
}
$qb->setMaxResults($count);
});
return $this;
}
/**
* @param int $totalCount if provided, $totalCount will be set to total count of found rows limit applied
*
* @return ProductCollection
*/
public function getProducts(&$totalCount = null)
{
$useTotalCount = count(func_get_args());
$products = [];
$dataAll = [];
$qb = $this->createQueryBuilder($useTotalCount);
$result = $qb->execute();
if ($useTotalCount) {
$totalCount = (int) sqlFetchAssoc(sqlQuery('SELECT FOUND_ROWS() as total_count'))['total_count'];
}
if (!$this->variationsAsResult) {
$result = sqlFetchAll($result, 'id');
$variations = array_filter($result, function ($p) { return !empty($p['variationsIds']); });
if ($variations) {
$minVariations = sqlQueryBuilder()->select('pv.id_product')
->fromProducts()
->joinVariationsOnProducts()
->andWhere(Operator::inIntArray(array_keys($variations), 'p.id'));
foreach ($this->getVariationSelectFields() as $alias => $expr) {
$minVariations->addSelect("{$expr} as `{$alias}`");
}
$usePriceListPrice = false;
if (findModule(Modules::PRICELISTS)) {
$pricelistContext = Contexts::get(PricelistContext::class);
if ($pricelist = $pricelistContext->getActiveId()) {
$usePriceListPrice = true;
// add pricelist price
$minVariations->andWhere(\KupShop\PricelistBundle\Query\Product::applyPricelistOnVariation($pricelist));
}
}
$varSpecs = [];
foreach ($variations as $id_product => $product) {
// Pokud je aktivní ceník, tak je potřeba brát všechny varianty i s různou cenou. Minimální cenu zajistí applyMinVariationsOrderBy a následný GROUP BY p.id
if ($usePriceListPrice) {
$varSpecs[] = Operator::andX(
"pv.id IN ({$product['variationsIds']})",
'pv.price IS NOT NULL');
} else {
$varSpecs[] = Operator::andX(
"pv.id IN ({$product['variationsIds']})",
"pv.price = {$product['price']}");
}
}
$minVariations->andWhere(Operator::orX($varSpecs));
$this->applyMinVariationsOrderBy($minVariations);
$variations = $minVariations->execute()->fetchAllAssociativeIndexed();
foreach ($variations as $id_product => $variation) {
$result[$id_product]['minVariation'] = $variation;
// $variation = array_filter($variation);
unset($variation['bonus_points']);
$result[$id_product] = array_replace($result[$id_product], $variation);
}
}
}
foreach ($result as $row) {
if ($this->variationsAsResult && $row['id_variation'] !== null) {
$product = new Variation();
} else {
$product = new Product();
}
$product->createFromArray($row);
$id = $product->id;
if ($product instanceof Variation) {
$id .= '/'.$product->variationId;
}
$products[$id] = $product;
$dataAll[$id] = $row;
}
$productCollection = (new ProductCollection($products))
->setEntityType($this->variationsAsResult ? FilterParams::ENTITY_VARIATION : FilterParams::ENTITY_PRODUCT);
foreach ($this->resultModifiers as $callback) {
$callback($productCollection, $dataAll);
}
return $productCollection;
}
public function getProductsCount()
{
$count = $this->variationsAsResult ? '*' : 'distinct p.id';
$query = $this->createQueryBuilder(false)
->select("count({$count}) c")
->resetQueryPart('groupBy');
return (int) $query->execute()->fetch()['c'];
}
public function andSpec(?callable $spec = null)
{
$this->specs[] = $spec;
return $this;
}
public function getVariationSelectFields($qb = null)
{
$fields = [
'id_variation' => 'pv.id',
'variation_title' => 'pv.title',
'price' => 'pv.price',
'matched_id_variation' => 'pv.id',
'variationVisible' => 'pv.figure',
'variationData' => 'pv.data',
];
if (findModule(\Modules::PRICE_HISTORY)) {
$fields['price_for_discount'] = 'COALESCE(pv.price_for_discount, p.price_for_discount)';
}
if (findModule(\Modules::PRODUCTS, \Modules::SUB_PRICE_COMMON)) {
$fields['price_common'] = 'COALESCE(pv.price_common, p.price_common)';
}
if (findModule(\Modules::PRODUCTS, \Modules::SUB_PRICE_BUY)) {
$fields['price_buy'] = 'COALESCE(pv.price_buy, p.price_buy)';
}
if (findModule(\Modules::PRODUCTS, \Modules::SUB_WEIGHT)) {
$fields['weight'] = 'COALESCE(pv.weight, p.weight)';
}
if (findModule(\Modules::BONUS_PROGRAM)) {
$fields['bonus_points'] = 'COALESCE(pv.bonus_points, p.bonus_points)';
}
return $fields;
}
public function getSelectFields($qb = null)
{
$fields = [
'id' => 'p.id',
'show_in_search' => 'p.show_in_search',
'id_variation' => 'pv.id',
'variation_title' => 'pv.title',
'code' => 'p.code',
'productCode' => 'p.code',
'in_store' => \Query\Product::getInStoreField($this->variationsAsResult, $qb),
'price' => 'MIN(COALESCE(pv.price, p.price))',
'priceMax' => 'MAX(COALESCE(pv.price, p.price))',
'priceRaw' => 'p.price',
'price_common' => $this->variationsAsResult ? 'COALESCE(pv.price_common, p.price_common)' : 'p.price_common',
'discount' => 'p.discount',
'delivery_time' => $this->variationsAsResult ? 'COALESCE(pv.delivery_time, p.delivery_time)' : 'p.delivery_time',
'guarantee' => 'p.guarantee',
'campaign' => 'p.campaign',
'ean' => $this->variationsAsResult ? 'COALESCE(pv.ean, p.ean)' : 'p.ean',
'producer' => 'p.producer',
'matched_id_variation' => 'pv.id',
'figure' => $this->variationsAsResult ? 'pv.figure' : 'p.figure',
'data' => 'p.data',
'variationVisible' => 'pv.figure',
'variationData' => 'pv.data',
'width' => $this->variationsAsResult ? 'COALESCE(pv.width, p.width)' : 'p.width',
'height' => $this->variationsAsResult ? 'COALESCE(pv.height, p.height)' : 'p.height',
'depth' => $this->variationsAsResult ? 'COALESCE(pv.depth, p.depth)' : 'p.depth',
'parameters' => 'p.parameters',
'variationsIds' => 'GROUP_CONCAT(DISTINCT pv.id)',
'has_variations' => '(pv.id IS NOT NULL)',
'date_added' => $this->variationsAsResult ? 'COALESCE(pv.date_added, p.date_added)' : 'p.date_added',
'meta_title' => 'p.meta_title',
'meta_description' => 'p.meta_description',
'meta_keywords' => 'p.meta_keywords',
];
if (findModule(Modules::PRODUCTS_VARIATIONS, Modules::SUB_CODE)) {
$fields['variationCode'] = 'pv.code';
}
if (findModule(Modules::PRODUCTS, Modules::SUB_SHOW_MAX)) {
$field = \Query\Product::getInStoreField($this->variationsAsResult);
$fields['in_store'] = 'LEAST('.$field.', COALESCE(p.in_store_show_max, '.findModule('products', 'showMax').'))';
}
if (findModule(\Modules::PRODUCTS, \Modules::SUB_PRICE_BUY)) {
$fields['price_buy'] = $this->variationsAsResult ? 'COALESCE(pv.price_buy, p.price_buy)' : 'p.price_buy';
}
if (findModule(\Modules::PRODUCTS, \Modules::SUB_WEIGHT)) {
$fields['weight'] = $this->variationsAsResult ? 'COALESCE(pv.weight, p.weight)' : 'p.weight';
}
if (findModule(\Modules::BONUS_PROGRAM)) {
$fields['bonus_points'] = $this->variationsAsResult ? 'COALESCE(pv.bonus_points, p.bonus_points)' : 'p.bonus_points';
}
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS)) {
$fields['unit'] = 'p.unit';
$fields['short_name'] = 'pu.short_name';
$fields['short_name_admin'] = 'pu.short_name_admin';
$fields['unit_long_name'] = 'pu.long_name';
$fields['recalculate_to'] = 'pu2.recalculate_to';
$fields['measure_quantity'] = 'p.measure_quantity';
}
if (findModule(Modules::PRODUCTS, Modules::SUB_DESCR_PLUS)) {
$fields['id_block'] = 'p.id_block';
}
if (findModule(\Modules::OSS_VATS)) {
$fields['id_cn'] = 'p.id_cn';
}
if (findModule(\Modules::PRICE_HISTORY)) {
$fields['price_for_discount'] = 'MIN(COALESCE(pv.price_for_discount, p.price_for_discount))';
}
if (findModule(Modules::STOCK_IN)) {
$fields['date_stock_in'] = 'p.date_stock_in';
}
if (findModule(Modules::PRODUCTS_SERIAL_NUMBERS)) {
$fields['serial_number_require'] = 'p.serial_number_require';
}
return $fields;
}
public function getQueryBuilder()
{
return $this->createQueryBuilder(null);
}
/**
* @return Query\QueryBuilder
*/
protected function createQueryBuilder($useTotalCount)
{
/** @var \Query\QueryBuilder $query */
$query = sqlQueryBuilder()
->fromProducts()
->joinVariationsOnProducts()
->where(Operator::not(Operator::equals(['p.id' => 0]))) // Kvuli produktu pro pokladnu s ID 0
->groupBy('p.id');
$selectedFields = $this->getSelectFields($query);
// Jinak to do qb nacpat nejde.
$selectedFields['id'] = 'SQL_CALC_FOUND_ROWS '.$selectedFields['id'];
foreach ($selectedFields as $alias => $expr) {
$query->addSelect("{$expr} as `{$alias}`");
}
if ($this->variationsAsResult) {
$query->addGroupBy('pv.id');
}
// add pricelist price
if (findModule(Modules::PRICELISTS)) {
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
if ($pricelistContext->getActiveId()) {
$query->andWhere(
\KupShop\PricelistBundle\Query\Product::applyPricelist($pricelistContext->getActiveId(), null, true)
);
}
}
$query->andWhere(
Query\Translation::coalesceTranslatedFields(
\KupShop\I18nBundle\Translations\ProductsTranslation::class,
['title', 'short_descr', 'long_descr', 'parameters', 'figure', 'meta_title', 'meta_description']
)
);
if (findModule(Modules::PRODUCTS_VARIATIONS) && $this->variationsAsResult) {
$query->andWhere(
Query\Translation::coalesceTranslatedFields(
\KupShop\I18nBundle\Translations\VariationsTranslation::class,
['title' => 'variation_title']
)
);
}
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS)) {
$query->leftJoin('p', 'products_units', 'pu', 'p.unit=pu.id');
$query->andWhere(
Query\Translation::coalesceTranslatedFields(
\KupShop\I18nBundle\Translations\ProductsUnitsTranslation::class
)
);
$query->leftJoin('p', 'products_units', 'pu2', 'p.measure_unit=pu2.id')
->addSelect('pu2.short_name as measure_unit_name, pu2.recalculate_to');
}
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS_FLOAT)) {
$query->addSelect('pu.pieces_precision');
}
$query->addSelect(\Query\Product::withVat());
$query->andWhere(Operator::andX($this->specs));
return $query;
}
/**
* @return $this
*/
public function setVariationsAsResult($variationsAsResult)
{
$this->variationsAsResult = $variationsAsResult;
return $this;
}
/**
* @return bool
*/
public function getVariationsAsResult()
{
return $this->variationsAsResult;
}
public function addResultModifiers(callable $resultModifier, $prepend = false): ProductListBase
{
if ($prepend) {
array_unshift($this->resultModifiers, $resultModifier);
} else {
$this->resultModifiers[] = $resultModifier;
}
return $this;
}
protected function applyMinVariationsOrderBy(Query\QueryBuilder $qb): void
{
if (findModule(\Modules::PRICE_HISTORY)) {
$qb->orderBy('COALESCE(pv.price_for_discount, p.price_for_discount)', 'ASC');
}
if (findModule(Modules::PRICELISTS)) {
$pricelistContext = Contexts::get(PricelistContext::class);
if ($pricelistContext->getActiveId()) {
$qb->orderBy('COALESCE(prlv.price, prlp.price, pv.price, p.price) * ((100 - COALESCE(prlv.discount, prlp.discount, p.discount))/100)',
'DESC');
}
}
}
}
if (empty($subclass)) {
class ProductList extends ProductListBase
{
}
}