first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

656
class/Query/Filter.php Normal file
View File

@@ -0,0 +1,656 @@
<?php
namespace Query;
use Doctrine\DBAL\Connection;
use FilterBase as FilterBaseAlias;
use KupShop\CatalogBundle\Query\Search;
use KupShop\CatalogBundle\Util\FavoriteProductsUtil;
use KupShop\I18nBundle\Translations\ProductsTranslation;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\LabelsBundle\Util\LabelFilterSpecs;
use KupShop\QuantityDiscountBundle\Context\QuantityDiscountContext;
use KupShop\QuantityDiscountBundle\Query\QuantityDiscount;
use KupShop\StoresBundle\Query\StoresQuery;
class FilterBase
{
public static $paramColumn = [
\Filter::PARAM_INT => 'value_float',
\Filter::PARAM_FLOAT => 'value_float',
\Filter::PARAM_FLOAT_LIST => 'value_float',
\Filter::PARAM_CHAR => 'value_char',
\Filter::PARAM_LIST => 'value_list',
];
public static function inSale($inSale)
{
if ($inSale === true) {
return Operator::andX('p.discount > 0');
}
}
/**
* @return \Closure|null
*
* @throws \Exception
*/
public static function parametersFilterUtil(array $parameters, ?callable $filterConditionCallback = null, bool $useIntersect = true)
{
$filterConditionCallback = $filterConditionCallback ?: function ($alias, $paramId, $paramData) {
return Operator::andX(
self::rangeOrIn($alias, $paramData, $paramId),
($paramData['type'] != \Filter::PARAM_LIST) ? Operator::equals(["{$alias}.id_parameter" => $paramId]) : null // id_parameter is determined by value_list
);
};
static $counter = 0;
$qbs = [];
foreach ($parameters as $paramId => $paramData) {
$alias = 'pp_'.$counter++;
$select = sqlQueryBuilder()
->select($alias.'.id_product')
->from('parameters_products', $alias)
->where($filterConditionCallback($alias, $paramId, $paramData));
$qbs[] = $select;
}
if (empty($qbs)) {
return null;
}
$existsSubquery = sqlQueryBuilder()->select('1')->where('fil_insct.id_product = p.id');
$resultOperator = ($useIntersect) ? 'INTERSECT' : 'UNION';
$intersectSubquery = implode(
" {$resultOperator} ",
array_map(
function ($sql) use ($existsSubquery) {
$existsSubquery->addParameters($sql->getParameters(), $sql->getParameterTypes());
return '('.$sql->getSql().')';
},
$qbs
)
);
return Operator::exists(
$existsSubquery->from("({$intersectSubquery})", 'fil_insct')
);
}
/**
* @throws \Exception
*/
public static function variationsFilterUtil(
array $variationIds,
?callable $filterConditionCallback = null,
bool $useIntersect = true,
): ?\Closure {
$filterConditionCallback = $filterConditionCallback ?: function ($alias, $variationValueID) {
return Operator::andX(
Operator::equals(["{$alias}.id_value" => $variationValueID])
);
};
$qbs = [];
static $counter = 0;
foreach ($variationIds as $variationValueID) {
$alias = 'pvc_'.$counter++;
$select = sqlQueryBuilder()
->select($alias.'.id_variation')
->from('products_variations_combination', $alias)
->where($filterConditionCallback($alias, $variationValueID));
$qbs[] = $select;
}
if (empty($qbs)) {
return null;
}
$existsSubquery = sqlQueryBuilder()->select('1')->where('fil_insct.id_variation = pv.id');
$resultOperator = ($useIntersect) ? 'INTERSECT' : 'UNION';
$intersectSubquery = implode(
" {$resultOperator} ",
array_map(
function ($sql) use ($existsSubquery) {
$existsSubquery->addParameters($sql->getParameters(), $sql->getParameterTypes());
return '('.$sql->getSql().')';
},
$qbs
)
);
return Operator::exists(
$existsSubquery->from("({$intersectSubquery})", 'fil_insct')
);
}
/**
* @param array $parameters
* [paramId => ['type' => \Filter::PARAM_(LIST|CHAR), 'values' => [value1, ...]]]
* [paramId => ['type' => \Filter::PARAM_(INT|FLOAT), 'values' => [min => x, max => y] ]]
* @param int $excludeParamId
*
* @return callable
*/
public static function byParameters(array $parameters, $excludeParamId = null)
{
if ($excludeParamId) {
unset($parameters[$excludeParamId]);
}
return function (QueryBuilder $qb) use ($parameters) {
return self::parametersFilterUtil($parameters);
};
}
public static function byAndParameters(array $parameters): ?\Closure
{
$operators = [];
foreach ($parameters as $paramId => $paramData) {
$operators[] = self::andParameter($paramData, $paramId);
}
return Operator::andX($operators);
}
public static function byInvertParameters(array $parameters)
{
return function (QueryBuilder $qb) use ($parameters) {
return Operator::not(self::parametersFilterUtil($parameters));
};
}
protected static function andParameter($paramInfo, $paramId)
{
$expressionSpec = [
FilterBaseAlias::PARAM_CHAR => 'self::andSpec',
FilterBaseAlias::PARAM_LIST => 'self::andSpec',
FilterBaseAlias::PARAM_FLOAT_LIST => 'self::andSpec',
];
if (!is_string($paramInfo['type'] ?? null) || !array_key_exists($paramInfo['type'], $expressionSpec)) {
return '';
}
$fieldName = self::$paramColumn[$paramInfo['type']];
return call_user_func_array($expressionSpec[$paramInfo['type']], [$paramInfo, $paramId, $fieldName]);
}
protected static function rangeOrIn($alias, $paramInfo, $paramId)
{
$expressionSpec = [
\Filter::PARAM_INT => 'self::minMaxSpec',
\Filter::PARAM_FLOAT => 'self::minMaxSpec',
\Filter::PARAM_CHAR => 'self::inSpec',
\Filter::PARAM_LIST => 'self::inSpec',
\Filter::PARAM_FLOAT_LIST => 'self::inSpec',
];
if (!is_string($paramInfo['type'] ?? null) || !array_key_exists($paramInfo['type'], $expressionSpec)) {
return '';
}
$fieldName = $alias.'.'.self::$paramColumn[$paramInfo['type']];
return call_user_func_array($expressionSpec[$paramInfo['type']], [$paramInfo, $paramId, $fieldName]);
}
protected static function inSpec(array $paramInfo, $paramId, $fieldName)
{
return function (QueryBuilder $qb) use ($paramInfo, $paramId, $fieldName) {
// float-list => float_list
// can't have '-' in param name (An exception occurred: Value for :float not found in params array.)
$param_type = str_replace('-', '_', $paramInfo['type']);
$sqlParamName = ':'.$param_type.$paramId;
$bindType = $paramInfo['type'] == \Filter::PARAM_LIST ? Connection::PARAM_INT_ARRAY : Connection::PARAM_STR_ARRAY;
$qb->setParameter($sqlParamName, $paramInfo['values'], $bindType);
return $qb->expr()->in($fieldName, $sqlParamName);
};
}
protected static function andSpec(array $paramInfo, $paramId, $fieldName): \Closure
{
return function () use ($paramInfo, $fieldName) {
$operands = [];
$counter = 0;
foreach ($paramInfo['values'] as $value) {
$subQbAlias = 'pp_sub_'.$counter++;
$existsSubQb = sqlQueryBuilder()
->select('1')
->from('parameters_products', $subQbAlias)
->andWhere(Operator::andX(
Operator::equals([$subQbAlias.'.'.$fieldName => $value]),
'p.id ='.$subQbAlias.'.id_product'
));
$operands[] = Operator::exists($existsSubQb);
}
return Operator::andX($operands);
};
}
protected static function minMaxSpec(array $paramInfo, $paramId, $fieldName)
{
return Operator::between($fieldName, new \Range($paramInfo['values']['min'], $paramInfo['values']['max']));
}
/**
* Filters by coalesce(variant.price, product.price).
*
* @return callable
*/
public static function byPriceRange(\Range $priceRange)
{
return self::doFilterByPriceRange($priceRange, 'COALESCE(pv.price, p.price)');
}
public static function byDiscountRange(\Range $discountRange)
{
return function (QueryBuilder $qb) use ($discountRange) {
$discountFieldDefinition = Contexts::get(UserContext::class)->getOriginalPriceType()->getDiscountFieldDefinition();
$qb->andWhere($discountFieldDefinition->getSpec());
return self::minMaxSpec(['values' => [
'min' => $discountRange->min(),
'max' => $discountRange->max(),
]], null, "ROUND({$discountFieldDefinition->getField()}, 1)");
};
}
/**
* Filters by product.price only.
*
* @return callable
*/
public static function byProductPriceRange(\Range $priceRange)
{
return self::doFilterByPriceRange($priceRange, 'p.price');
}
protected static function doFilterByPriceRange(\Range $priceRange, $priceField)
{
return function (QueryBuilder $qb) use ($priceRange, $priceField) {
if ($priceRange->isNotNull()) {
$qb->joinVatsOnProducts();
}
return Operator::between(Product::withVatAndDiscount($qb, $priceField), $priceRange);
};
}
/**
* Filter in_store range by coalesce(variant.in_store, product.in_store).
*
* @return callable
*/
public static function byInStoreRange(\Range $inStoreRange)
{
return self::doFilterByInStoreRange($inStoreRange, 'COALESCE(pv.in_store, p.in_store)');
}
/**
* Filters by product.in_store only.
*
* @return callable
*/
public static function byProductInStoreRange(\Range $inStoreRange)
{
return self::doFilterByInStoreRange($inStoreRange, 'p.in_store');
}
protected static function doFilterByInStoreRange(\Range $inStoreRange, $inStoreField)
{
return function (QueryBuilder $qb) use ($inStoreRange, $inStoreField) {
if ($inStoreRange->isNotNull()) {
$qb->joinVariationsOnProducts();
}
return Operator::between($inStoreField, $inStoreRange);
};
}
public static function byInStore($inStoreMethod, $useVariations = true)
{
if ($inStoreMethod == \Filter::IN_STORE_SUPPLIER) {
return Operator::orX(Product::inStore($useVariations), Product::inStoreSupplier($useVariations));
}
return Product::$inStoreMethod($useVariations);
}
public static function byProductInStore($inStoreMethod)
{
return self::byInStore($inStoreMethod, false);
}
public static function byProducers(array $producerIds)
{
static $counter = 0;
return function (QueryBuilder $qb) use ($producerIds, &$counter) {
$paramName = 'producerIds_'.$counter++;
$qb->joinProducersOnProducts();
$qb->setParameter($paramName, $producerIds, Connection::PARAM_INT_ARRAY);
return $qb->expr()->in('COALESCE(pr.id, 0)', ":{$paramName}");
};
}
public static function byTemplates(array $templateIds)
{
return function (QueryBuilder $qb) use ($templateIds) {
$qb->joinTemplatesOnProducts();
$qb->andWhere(Operator::inIntArray($templateIds, 't.id'));
};
}
/**
* @param array $variations
* [labelId => [valueId1, valueId2, ...]]
* @param int $excludeLabelId
*
* @return callable
*
* @todo jedna funkce pro parametry i varianty?
*/
public static function byVariations(array $variations, $excludeLabelId = null)
{
if ($excludeLabelId) {
unset($variations[$excludeLabelId]);
}
return function (QueryBuilder $qb) use ($variations) {
$qb->joinVariationsOnProducts();
$andExpressions = [];
foreach ($variations as $labelId => $valueIds) {
$alias = 'pvc_'.$labelId;
$sqlParamLabelId = ':labelId_'.$labelId;
$sqlParamValueIds = ':valueIds_'.$labelId;
$qb->join('pv', 'products_variations_combination', $alias, "pv.id = {$alias}.id_variation");
$andExpressions[] = $qb->expr()->andX(
$qb->expr()->in("{$alias}.id_value", $sqlParamValueIds),
$qb->expr()->eq("{$alias}.id_label", $sqlParamLabelId)
);
$qb->setParameter($sqlParamValueIds, [...$valueIds, -1], Connection::PARAM_INT_ARRAY);
$qb->setParameter($sqlParamLabelId, $labelId);
}
return Operator::andX($andExpressions);
};
}
public static function byCampaigns(array $campaigns, $operator = 'AND')
{
return function (QueryBuilder $qb) use ($campaigns, $operator) {
$conditions = [];
// Favorites
if (($index = array_search('F', $campaigns)) !== false) {
$userContext = Contexts::get(UserContext::class);
if ($userContext->getActiveId()) {
$qb->join('p', 'products_favorites', 'pf', 'p.id = pf.id_product');
$qb->setParameter('id_user', $userContext->getActiveId());
$conditions[] = 'pf.id_user = :id_user';
} else {
$conditions[] = Operator::inIntArray(FavoriteProductsUtil::getCookieProducts(), 'p.id');
}
unset($campaigns[$index]);
}
// Remaining campaigns
if ($campaigns) {
$conditions[] = Operator::findInSet($campaigns, \Filter::getCampaignField(), $operator);
}
return Operator::andX($conditions);
};
}
public static function byLabels(array $labelIds, bool|array $filterActiveParams = false)
{
$null_cat = array_search(-1, $labelIds);
$labels = sqlQueryBuilder()
->select('*')
->from('product_labels_relation', 'plr')
->andWhere('p.id = plr.id_product');
if ($filterActiveParams) {
$labels->leftJoin('plr', 'labels', 'l', 'plr.id_label = l.id');
$filterActiveParams = array_merge(
['active' => true],
findModule(\Modules::TRANSLATIONS) ? ['translation' => true] : [],
is_array($filterActiveParams) ? $filterActiveParams : []);
$productsFilterSpecs = ServiceContainer::getService(LabelFilterSpecs::class);
$filterSpecs = $productsFilterSpecs->getSpecs($filterActiveParams);
$labels->andWhere($filterSpecs);
}
if ($null_cat !== false) {
unset($labelIds[$null_cat]);
$specs[] = Operator::not(Operator::exists($labels));
} else {
$labels
->andWhere(Operator::inIntArray($labelIds, 'plr.id_label'));
$specs[] = Operator::exists($labels);
}
return Operator::orX($specs);
}
public static function byProductCodes(array $productCodes): callable
{
return Operator::inStringArray($productCodes, 'p.code');
}
public static function byRelatedProducts(array $relatedProducts)
{
return function (QueryBuilder $qb) use ($relatedProducts) {
$qb->join('p', 'products_related', 'pr', 'pr.id_rel_product = p.id');
return Operator::inIntArray($relatedProducts, 'pr.id_top_product');
};
}
public static function byStores(array $stores): callable
{
return StoresQuery::filterByStoresInStore($stores);
}
public static function byFigure(string $figure): callable
{
static $counter = 0;
return function (QueryBuilder $qb) use ($figure, &$counter) {
$paramName = 'visible'.$counter++;
$expression = null;
$qb->setParameter($paramName, $figure);
$qb->andWhere(
Translation::joinTranslatedFields(
ProductsTranslation::class,
function (QueryBuilder $qb, $columnName, $translatedField) use (&$expression, $paramName) {
if ($translatedField != null) {
$expression = "COALESCE({$translatedField}, p.{$columnName}) = :{$paramName}";
} else {
$expression = "p.figure = :{$paramName}";
}
return false;
},
['figure']
)
);
return $expression;
};
}
public static function byVariationsSizeConvertion(array $variationsSizeConvertion)
{
$variations = [];
$products = [];
$showOnlyInStore = \Settings::getDefault()['prod_show_not_in_store'] == 'N';
foreach ($variationsSizeConvertion as $item) {
foreach ($item['value'] ?? [] as $value) {
$qb = sqlQueryBuilder()->select('pv.id')
->from('convertors_table_cache', 'ctc')
->join('ctc', 'convertors_table_values', 'ctv', 'ctc.id_table = ctv.id_table')
->join('ctc', 'products_variations', 'pv', 'pv.id_product = ctc.id_product')
->join('pv', 'products_variations_combination', 'pvc', 'pvc.id_variation = pv.id and pvc.id_value = ctv.id_label_value')
->join('pv', 'products', 'p', 'p.id = pv.id_product')
->where(Operator::equals(['ctv.id_parameter_value' => $value]))
->groupBy('pv.id');
$qbProducts = sqlQueryBuilder()->select('pp.id_product')
->from('parameters_list', 'pl')
->join('pl', 'parameters_products', 'pp', 'pl.id_parameter = pp.id_parameter')
->join('pp', 'products', 'p', 'pp.id_product = p.id')
->andWhere(Operator::equals(['pp.value_list' => $value]))
->groupBy('pp.id_product');
$ids = sqlFetchAll($qb->execute(), 'id');
$productsIds = sqlFetchAll($qbProducts->execute(), 'id_product');
$variations = array_merge($variations, array_keys($ids));
$products = array_merge($products, array_keys($productsIds));
}
}
return function (QueryBuilder $qb) use ($variations, $products) {
$qb->joinVariationsOnProducts();
return Operator::OrX(
Operator::inIntArray($variations, 'pv.id'),
Operator::inIntArray($products, 'p.id')
);
};
}
/**
* @return callable
*/
public static function isVisible()
{
$expressions = [Product::isVisible(), Variation::isVisible()];
return Operator::andX($expressions);
}
/**
* @param bool Filter only products having any category
*
* @return callable
*/
public static function hasCategory(bool $requiresCategory, bool $virtualSectionsIncluded = true)
{
if (!$requiresCategory) {
return null;
}
return function (QueryBuilder $qb) use ($virtualSectionsIncluded) {
$onlyWithSectionQb = sqlQueryBuilder()->select('*')
->from('products_in_sections');
$spec = ['p.id = products_in_sections.id_product'];
if (!$virtualSectionsIncluded) {
$spec[] = Operator::equals(['products_in_sections.generated' => 0]);
}
$onlyWithSectionQb->andWhere(Operator::andX($spec));
$qb->andWhere(\Query\Operator::exists($onlyWithSectionQb));
};
}
public static function search(string $searchTerm, array $additionalFields = []): callable
{
return static function (QueryBuilder $qb) use ($searchTerm, $additionalFields) {
// kopie fallbacku pro adminy ze SearchView
$fields = [
['field' => 'p.title', 'match' => 'both', 'order' => true],
['field' => 'p.short_descr', 'match' => 'both'],
['field' => 'p.code', 'match' => 'left', 'order' => true],
['field' => 'pr.name', 'match' => 'both'],
];
$qb->joinProducersOnProducts();
if (is_numeric($searchTerm)) {
$fields[] = ['field' => 'p.ean', 'match' => 'numeric', 'order' => true];
if (findModule(\Modules::PRODUCTS_VARIATIONS)) {
$fields[] = ['field' => 'pv.ean', 'match' => 'numeric', 'order' => true];
}
}
if (findModule(\Modules::PRODUCTS_VARIATIONS, 'variationCode')) {
$qb->joinVariationsOnProducts();
$fields[] = ['field' => 'pv.code', 'match' => 'left', 'order' => true];
}
foreach ($additionalFields as $field) {
$fields[] = $field;
}
return Search::searchFields($searchTerm, $fields);
};
}
public static function byQuantityDiscounts(array $quantityDiscounts): ?callable
{
if (!findModule(\Modules::QUANTITY_DISCOUNT)) {
return null;
}
if (!($groupId = Contexts::get(QuantityDiscountContext::class)->getActive())) {
return null;
}
return function (QueryBuilder $qb) use ($groupId, $quantityDiscounts) {
$qb->join('p', 'products_quantity_discounts', 'pqd', 'p.id = pqd.id_product');
$specs = [];
foreach ($quantityDiscounts as $piecesFrom) {
$specs[] = Operator::equals(['pqd.pieces' => $piecesFrom]);
}
return Operator::andX(
QuantityDiscount::byGroup($groupId),
Operator::orX($specs)
);
};
}
public static function byShowInSearch(bool $showInSearch): ?callable
{
if (!$showInSearch) {
return null;
}
return function (QueryBuilder $qb) {
$qb->andWhere("p.show_in_search = 'Y'");
};
}
}
if (empty($subclass)) {
class Filter extends FilterBase
{
}
}

333
class/Query/Operator.php Normal file
View File

@@ -0,0 +1,333 @@
<?php
namespace Query;
use Doctrine\DBAL\Connection;
use KupShop\KupShopBundle\Exception\ForceEmptyResultException;
class Operator
{
/**
* @return callable
*/
public static function andX($operand1, $operand2 = null)
{
if (func_num_args() === 1 && is_array($operand1)) {
$operands = $operand1;
} else {
$operands = func_get_args();
}
$operands = array_filter($operands);
if (!$operands) {
return null;
}
return function (QueryBuilder $qb) use ($operands) {
$evaluatedOperands = array_filter($qb->evaluateClosures($operands));
if (!$evaluatedOperands) {
return null;
}
return call_user_func_array([$qb->expr(), 'andX'], array_values($evaluatedOperands));
};
}
/**
* @return callable
*/
public static function orX($operand1, $operand2 = null)
{
if (func_num_args() === 1 && is_array($operand1)) {
$operands = $operand1;
} else {
$operands = func_get_args();
}
$operands = array_filter($operands);
if (!$operands) {
return '0';
}
return function (QueryBuilder $qb) use ($operands) {
return call_user_func_array([$qb->expr(), 'orX'], $qb->evaluateClosures($operands));
};
}
public static function between($field, \Range $range)
{
static $counter = 0;
return function (QueryBuilder $qb) use ($field, $range, &$counter) {
$e = $qb->expr();
$andXArgs = [];
if (($range->min() ?? '') !== '') {
$minParam = ':between_min_'.$counter;
$qb->setParameter($minParam, $range->min());
$andXArgs[] = $e->gte($field, $minParam);
}
if (($range->max() ?? '') !== '') {
$maxParam = ':between_max_'.$counter;
$qb->setParameter($maxParam, $range->max());
$andXArgs[] = $e->lte($field, $maxParam);
}
$counter++;
return static::andX($andXArgs);
};
}
/**
* @param string $set
* @param string $operator AND/OR
*
* @return callable
*/
public static function findInSet(array $needles, $set, $operator = 'AND')
{
if (!$needles) {
throw new \InvalidArgumentException('Array $needles must not be empty');
}
if (!$set) {
throw new \InvalidArgumentException('Parameter $set must not be empty');
}
if (!in_array($operator, ['AND', 'OR'])) {
throw new \InvalidArgumentException('Parameter $operator must be AND or OR');
}
static $counter = 0;
return function (QueryBuilder $qb) use ($needles, $set, $operator, &$counter) {
$expressions = [];
foreach ($needles as $needle) {
$paramName = ':needle_'.$counter++;
$quotedSet = $qb->getConnection()->quoteIdentifier($set);
$expressions[] = "FIND_IN_SET({$paramName}, {$quotedSet})";
$qb->setParameter($paramName, $needle);
}
return call_user_func_array([$qb->expr(), $operator.'X'], $expressions);
};
}
/**
* @param array $ids array of integers to search for in $field
* @param string $field field name to search
*
* @return callable
*/
public static function inIntArray(array $ids, $field)
{
static $counter = 0;
return function (QueryBuilder $qb) use ($ids, $field, &$counter) {
if (!$ids) {
return 'FALSE';
}
$paramName = 'inIntArray_'.$counter++;
$qb->setParameter($paramName, $ids, Connection::PARAM_INT_ARRAY);
return "{$field} IN (:{$paramName})";
};
}
public static function inSubQuery(string $field, QueryBuilder $subQuery): \Closure
{
return function (QueryBuilder $qb) use ($subQuery, $field) {
$qb->addParameters($subQuery->getParameters(), $subQuery->getParameterTypes());
return " {$field} IN ({$subQuery->getSQL()})";
};
}
/**
* @param array $ids array of integers to search for in $field
* @param string $field field name to search
*
* @return callable
*/
public static function inStringArray(array $ids, $field)
{
static $counter = 0;
return function (QueryBuilder $qb) use ($ids, $field, &$counter) {
$paramName = 'inStringArray_'.$counter++;
$qb->setParameter($paramName, $ids, Connection::PARAM_STR_ARRAY);
return "{$field} IN (:{$paramName})";
};
}
/**
* @param array $mapping field => value mapping
* @param string $operator operator used for joining
*
* @return callable
*/
public static function equals($mapping, $operator = 'AND')
{
static $counter = 0;
return function (QueryBuilder $qb) use ($mapping, $operator, &$counter) {
$parts = [];
foreach ($mapping as $field => $value) {
$paramName = 'equals_'.$counter++;
$qb->setParameter($paramName, $value);
$parts[] = " {$field} = :{$paramName} ";
}
return join($operator, $parts);
};
}
/**
* @param array $mapping field => value mapping
* @param string $operator operator used for joining
*
* @return callable
*/
public static function like($mapping, $operator = 'AND')
{
static $counter = 0;
return function (QueryBuilder $qb) use ($mapping, $operator, &$counter) {
$parts = [];
foreach ($mapping as $field => $value) {
$paramName = 'like_'.$counter++;
$qb->setParameter($paramName, $value);
$parts[] = " {$field} LIKE :{$paramName} ";
}
return join($operator, $parts);
};
}
/**
* @param array $mapping field => value mapping
* @param string $operator operator used for joining
*
* @return callable
*/
public static function equalsNullable($mapping, $operator = 'AND')
{
static $counter = 0;
return function (QueryBuilder $qb) use ($mapping, $operator, &$counter) {
$parts = [];
foreach ($mapping as $field => $value) {
$paramName = 'equalsNullable_'.$counter++;
if (is_null($value)) {
$parts[] = " {$field} IS NULL ";
} else {
$qb->setParameter($paramName, $value);
$parts[] = " {$field} = :{$paramName} ";
}
}
return join($operator, $parts);
};
}
public static function not($operand)
{
return function (QueryBuilder $qb) use ($operand) {
$expression = $qb->evaluateClosures([$operand])[0];
if (is_null($expression)) {
return null;
}
return "NOT ({$expression})";
};
}
public static function isNull($field)
{
return function (QueryBuilder $qb) use ($field) {
return $qb->expr()->isNull($field);
};
}
public static function isNotNull($field)
{
return function (QueryBuilder $qb) use ($field) {
return $qb->expr()->isNotNull($field);
};
}
public static function notOrNull($operand, $nullableField)
{
return function (QueryBuilder $qb) use ($operand, $nullableField) {
return $qb->evaluateClosures([Operator::not($operand)])[0].' OR '.$nullableField.' IS NULL';
};
}
public static function equalsToOrNullable($fieldName1, $fieldName2)
{
return "({$fieldName1} = {$fieldName2} OR ({$fieldName1} IS NULL AND {$fieldName2} IS NULL))";
}
public static function coalesce($fieldName1, $fieldName2 = null)
{
$columns = array_filter(func_get_args());
// nothing to coalesce, return column without coalesce
if (count($columns) === 1) {
return reset($columns);
}
$columns = join(',', $columns);
return "COALESCE({$columns})";
}
/**
* @param QueryBuilder[] $queryBuilders
*/
public static function union(array $queryBuilders): callable
{
return function (QueryBuilder $qb) use ($queryBuilders) {
$closures = [];
foreach ($queryBuilders as $subQb) {
$closures[] = Operator::subquery($subQb);
}
return '('.implode(' UNION ', $qb->evaluateClosures($closures)).')';
};
}
public static function subquery(QueryBuilder $subQb): callable
{
return function (QueryBuilder $qb) use ($subQb) {
$qb->addQueryBuilderParameters($subQb);
return '('.$subQb->getSQL().')';
};
}
public static function exists(QueryBuilder $qbToTest)
{
return function (QueryBuilder $qb) use ($qbToTest) {
$qb->addParameters($qbToTest->getParameters(), $qbToTest->getParameterTypes());
return "EXISTS ({$qbToTest->getSQL()})";
};
}
// Zbpůsobí, aby se query vůbec nevykonala. Je třeba zajistit, že to parent handluje.
// Nebylo by lepší to vyřešit memberem na QB, který by vracel prázdný statement?
public static function forceEmptyResult()
{
return function (QueryBuilder $qb) {
throw new ForceEmptyResultException();
};
}
}

191
class/Query/Order.php Normal file
View File

@@ -0,0 +1,191 @@
<?php
namespace Query;
use Doctrine\DBAL\Connection;
use KupShop\KupShopBundle\Query\JsonOperator;
class Order
{
/**
* @param array $paymentMethods ['METHOD_ONLINE', 'METHOD_TRANSFER', ...] constants from class.Payment.php
*
* @return \Closure
*/
public static function byPaymentMethods(array $paymentMethods)
{
static $counter = 0;
return function (QueryBuilder $qb) use ($paymentMethods, &$counter) {
$filteredPaymentClasses = [];
$paymentReflection = new \ReflectionClass('Payment');
foreach (\Payment::listClasses() as $class => $className) {
try {
$tmpMethod = \Payment::getClass($class)->getPayMethod();
foreach ($paymentMethods as $method) {
if (is_string($method)) {
$method = $paymentReflection->getConstant($method);
}
if ($method === $tmpMethod) {
$filteredPaymentClasses[] = $class;
}
}
} catch (\Exception $e) {
// don't kill me for eating this exception
}
}
$parameter = 'dtp_filtered_classes'.$counter++;
$qb->join('o', 'delivery_type', 'dt', 'dt.id = o.id_delivery')
->join('dt', 'delivery_type_payment', 'dtp', 'dtp.id = dt.id_payment');
$qb->setParameter($parameter, $filteredPaymentClasses, Connection::PARAM_STR_ARRAY);
return $qb->expr()->in('COALESCE(dtp.class, \'\')', ":{$parameter}");
};
}
public static function byVirtualDelivery(): callable
{
static $counter = 0;
return function (QueryBuilder $qb) use (&$counter) {
$filteredDeliveryClasses = [];
foreach (\Delivery::listClasses() as $class => $className) {
try {
if (\Delivery::getClass($class)->getType() === \Delivery::TYPE_VIRTUAL) {
$filteredDeliveryClasses[] = $class;
}
} catch (\Exception $e) {
// don't kill me for eating this exception
}
}
$parameter = 'dtd_filtered_classes_virtual'.$counter;
$qb->join('o', 'delivery_type', 'dt', 'dt.id = o.id_delivery')
->join('dt', 'delivery_type_delivery', 'dtd', 'dtd.id = dt.id_delivery');
$qb->setParameter($parameter, $filteredDeliveryClasses, Connection::PARAM_STR_ARRAY);
return $qb->expr()->in('COALESCE(dtd.class, \'\')', ":{$parameter}");
};
}
public static function byInPersonDelivery(bool $inPersonDelivery = true)
{
static $counter = 0;
return function (QueryBuilder $qb) use ($inPersonDelivery, &$counter) {
$filteredPaymentClasses = [];
foreach (\Delivery::listClasses() as $class => $className) {
try {
if (\Delivery::getClass($class)->isInPerson() === $inPersonDelivery) {
$filteredPaymentClasses[] = $class;
}
} catch (\Exception $e) {
// don't kill me for eating this exception
}
}
$parameter = 'dtd_filtered_classes'.$counter;
$qb->join('o', 'delivery_type', 'dt', 'dt.id = o.id_delivery')
->join('dt', 'delivery_type_delivery', 'dtd', 'dtd.id = dt.id_delivery');
$qb->setParameter($parameter, $filteredPaymentClasses, Connection::PARAM_STR_ARRAY);
return $qb->expr()->in('COALESCE(dtd.class, \'\')', ":{$parameter}");
};
}
public static function byStatus(array $statuses)
{
return Operator::inIntArray($statuses, 'o.status');
}
/**
* @param array of allowed delivery IDs
* @param string $alias
*
* @return \Closure
*/
public static function byDeliveryIDs(array $deliveryIDs, $alias = 'o')
{
static $counter = 0;
return function (QueryBuilder $qb) use ($deliveryIDs, &$counter, $alias) {
$parameter = 'dtd_filtered_ids'.$counter++;
$qb->join($alias, 'delivery_type', 'dt', "dt.id = {$alias}.id_delivery")
->setParameter($parameter, $deliveryIDs, Connection::PARAM_INT_ARRAY);
return $qb->expr()->in('COALESCE(dt.id_delivery, \'\')', ":{$parameter}");
};
}
public static function byPaymentIDs(array $paymentIDs, string $alias = 'o'): callable
{
static $counter = 0;
return function (QueryBuilder $qb) use ($paymentIDs, &$counter, $alias) {
$parameter = 'dtp_filtered_ids'.$counter++;
$qb->join($alias, 'delivery_type', 'dt', "dt.id = {$alias}.id_delivery")
->setParameter($parameter, $paymentIDs, Connection::PARAM_INT_ARRAY);
return $qb->expr()->in('COALESCE(dt.id_payment, \'\')', ":{$parameter}");
};
}
public static function itemType($alias = 'oi'): string
{
return JsonOperator::value("{$alias}.note", 'item_type');
}
/**
* @param string $alias
*
* @return \Closure
*/
public static function byItemType($itemType, $alias = 'oi')
{
static $counter = 0;
return function (QueryBuilder $qb) use ($itemType, &$counter, $alias) {
if (is_null($itemType)) {
return $qb->expr()->isNull(self::itemType($alias));
} else {
$parameter = 'order_item_type'.$counter++;
$qb->setParameter($parameter, $itemType);
return $qb->expr()->eq(Operator::coalesce(self::itemType($alias), "''"), ":{$parameter}");
}
};
}
public static function byPaidStatus(bool $isPaid = true): string
{
$paid = $isPaid ? '1' : '0';
return "o.status_payed = {$paid}";
}
/**
* @param $code string|array
*
* @return callable|void
*/
public static function byOrderNo($code, $alias = 'o')
{
return Operator::orX(array_map(function ($val) use ($alias) {
return Operator::like(["{$alias}.order_no" => "{$val}%", "{$alias}.order_no_reverse" => strrev($val).'%'], 'OR');
}, is_array($code) ? $code : [$code]));
}
public static function activeOnly()
{
return 'o.status_storno = 0';
}
public static function notPackedOrders(): callable
{
return Operator::andX(
Operator::inIntArray(getStatuses('notpacked'), 'o.status'),
Operator::equals(['o.status_storno' => 0]));
}
}

28
class/Query/Parameter.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
namespace Query;
use Doctrine\DBAL\Connection;
class Parameter
{
public static function inSections(array $sectionIds)
{
return function (QueryBuilder $qb) use ($sectionIds) {
$qb->leftJoin('pa', 'parameters_sections', 'pas', 'pa.id = pas.id_parameter');
$qb->setParameter('sectionIds', $sectionIds, Connection::PARAM_INT_ARRAY);
return $qb->expr()->in('pas.id_section', ':sectionIds');
};
}
public static function inProducers(array $producerIds)
{
return function (QueryBuilder $qb) use ($producerIds) {
$qb->leftJoin('pa', 'parameters_producers', 'pap', 'pa.id = pap.id_parameter');
$qb->setParameter('producerIds', $producerIds, Connection::PARAM_INT_ARRAY);
return $qb->expr()->in('pap.id_producer', ':producerIds');
};
}
}

41
class/Query/Price.php Normal file
View File

@@ -0,0 +1,41 @@
<?php
/**
* Created by PhpStorm.
* User: filip
* Date: 5/16/16
* Time: 1:13 PM.
*/
namespace Query;
class Price
{
public static function selectRange($minAlias = 'min', $maxAlias = 'max')
{
return self::doSelectRange($minAlias, $maxAlias, 'COALESCE(pv.price, p.price)');
}
public static function selectProductRange($minAlias = 'min', $maxAlias = 'max')
{
return self::doSelectRange($minAlias, $maxAlias, 'p.price');
}
private static function doSelectRange($minAlias, $maxAlias, $priceField)
{
return function (QueryBuilder $qb) use ($minAlias, $maxAlias, $priceField) {
$select = Product::withVatAndDiscount($qb, $priceField);
$qb->joinVatsOnProducts();
$qb->andWhere("{$priceField} > 0");
return sprintf(
'min(%s) %s, max(%s) %s',
$select,
$minAlias,
$select,
$maxAlias
);
};
}
}

458
class/Query/Product.php Normal file
View File

@@ -0,0 +1,458 @@
<?php
namespace Query;
use Doctrine\DBAL\Connection;
use KupShop\I18nBundle\Translations\PhotosTranslation;
use KupShop\KupShopBundle\Config;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\Context\VatContext;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\PricelistBundle\Context\PricelistContext;
use KupShop\SellerBundle\Context\SellerContext;
class Product
{
public static $fieldInStore = 'p.in_store';
public static function inCollectionsProducts(QueryBuilder $qb, array $ids)
{
$main_products = sqlQueryBuilder()->select('pc.id_product AS id')
->from('products_collections', 'pc')
->where('pc.id_product_related IN (:ids) OR pc.id_product IN (:ids)');
$related_products = sqlQueryBuilder()->select('pc.id_product_related AS id')
->from('products_collections', 'pc')
->leftJoin('pc', 'products_collections', 'pc_main', 'pc_main.id_product = pc.id_product')
->where('pc_main.id_product IN (:ids) OR pc_main.id_product_related IN (:ids)');
$qb->setParameter('ids', $ids, Connection::PARAM_INT_ARRAY);
return $main_products->getSQL().' UNION '.$related_products->getSQL();
}
public static function collectionsProductsSubQuery($productIds)
{
// tabulka [id, main_product, related_product]
// produkt neni v kolekci -> [id, null, null]
// produkt je hlavni v kolekci -> [id, id, related_product_1], [id, id, related_product_2], ...
// produkt je v kolekci -> [id, main_product, related_product_1], [id, main_product, related_product_2], ...
$products = sqlQueryBuilder()->select('id')->fromProducts()->where(Operator::inIntArray($productIds, 'id'));
return sqlQueryBuilder()
->select('DISTINCT p.id,
COALESCE(pc_main.id_product, pc_related.id_product) AS main_product,
COALESCE(pc_main.id_product_related, pc_related_2.id_product_related) AS related_product')
->from("({$products->getSQL()})", 'p')->addParameters($products->getParameters(), $products->getParameterTypes())
->leftJoin('p', 'products_collections', 'pc_main', 'pc_main.id_product = p.id')
->leftJoin('p', 'products_collections', 'pc_related', 'pc_related.id_product_related = p.id')
->leftJoin('p', 'products_collections', 'pc_related_2', 'pc_related_2.id_product = pc_related.id_product');
}
public static function isVisible()
{
return Filter::byFigure('Y');
}
public static function inSection($sectionId)
{
return self::inSections([$sectionId]);
}
public static function inSections(array $sectionIds, $alias = 'ps')
{
return function (QueryBuilder $qb) use ($sectionIds) {
$sectionIds = array_merge($sectionIds, [-1000]); // nonsense id, so there are always at least two ids - hack for better queryplan
$existsSubquery = sqlQueryBuilder()->select('id_product')
->from('products_in_sections')
->where('id_product = p.id')
->andWhere(Operator::inIntArray($sectionIds, 'id_section'));
return Operator::exists(
$existsSubquery
->addParameters($existsSubquery->getParameters(), $existsSubquery->getParameterTypes())
);
};
}
public static function inSectionsRecursive(array $sectionIds)
{
return self::inSections(
call_user_func_array('array_merge', array_map('getDescendantCategories', $sectionIds))
);
}
public static function inSectionsByLevel(array $ancestorAndLevel, array $excludeOptions = [])
{
if (isset($excludeOptions['ancestorID']) && isset($excludeOptions['level'])) {
unset($ancestorAndLevel[$excludeOptions['ancestorID']][$excludeOptions['level']]);
}
$sections = [];
foreach ($ancestorAndLevel as $ancestorID => $data) {
foreach ($data as $level => $rawSectionIDs) {
$sections[] = self::inSections(
call_user_func_array('array_merge', array_map('getDescendantCategories', $rawSectionIDs)),
'ps_'.$ancestorID.'_'.$level
);
}
}
return Operator::andX($sections);
}
public static function inStore($useVariations = false)
{
return function (QueryBuilder $qb) use ($useVariations) {
if ($useVariations) {
$qb->joinVariationsOnProducts();
$inStore = $qb->expr()->gt(static::getInStoreField(true, $qb), 0);
} else {
$inStore = $qb->expr()->gt(static::getInStoreField(false, $qb), 0);
}
return $inStore;
};
}
public static function inStoreVariations()
{
return function (QueryBuilder $qb) {
$qb->joinVariationsOnProducts();
// $qb->andWhere('pv.id IS NOT NULL AND pv.in_store > 0');
$inStore = $qb->expr()->gt('COALESCE(pv.in_store, 1)', 0);
return $inStore;
};
}
public static function inStoreSupplier($useVariations = false)
{
if (!findModule(\Modules::PRODUCTS_SUPPLIERS)) {
return null;
}
return function (QueryBuilder $qb) use ($useVariations) {
$suppliers = sqlQueryBuilder()->select('1')
->from('products_of_suppliers', 'pf_pos')
->andWhere('p.id = pf_pos.id_product')
->andWhere('pf_pos.in_store > 0');
if ($useVariations) {
$qb->joinVariationsOnProducts();
$suppliers->andWhere('(pv.id IS NULL and pf_pos.id_variation IS NULL) OR (pv.id=pf_pos.id_variation)');
}
return Operator::exists($suppliers);
};
}
public static function inStoreSeller(): callable
{
if (!findModule(\Modules::SELLERS) || !($storeId = Contexts::get(SellerContext::class)->getActive()['id_store'] ?? null)) {
return self::inStore();
}
return Filter::byStores([$storeId]);
}
public static function withOneProductPhotoId($variationsAsResult = false, $imageKind = null, bool $fallbackToProductPhoto = true)
{
return function (QueryBuilder $qb) use ($variationsAsResult, $imageKind, $fallbackToProductPhoto) {
$photoQb = sqlQueryBuilder()
->from('products', 'p2')
->leftJoin('p2', 'products_variations', 'pv2', 'pv2.id_product = p2.id')
->where('p2.id = p.id AND ((pv.id IS NULL) OR pv.id = pv2.id)')
->addSelect(self::withProductPhotoId($variationsAsResult, $imageKind, $fallbackToProductPhoto, 'p2', 'pv2', false))
->setMaxResults(1);
$qb->addQueryBuilderParameters($photoQb);
return '('.$photoQb->getSQL().') as id_photo';
};
}
public static function withProductPhotoId($variationsAsResult = false, $imageKind = null, bool $fallbackToProductPhoto = true, $aP = 'p', $aPV = 'pv', $selectAdditional = true)
{
return function (QueryBuilder $qb) use ($selectAdditional, $variationsAsResult, $imageKind, $fallbackToProductPhoto, $aP, $aPV) {
$addSelects = [];
if ($selectAdditional) {
$addSelects = [['date_update', 'id_photo_update']];
}
$joinCondition = "{$aP}.id=ppr.id_product";
if (findModule(\Modules::PRODUCTS_VARIATIONS_PHOTOS)) {
if (is_null($imageKind)) {
$imageKind = $variationsAsResult ? 'N' : 'Y';
}
if ($variationsAsResult) {
// use variation photo if present
// fallback to product's main photo in case product has no variation
$joinCondition .= " AND (ppr.id_variation = {$aPV}.id OR ({$aPV}.id IS NULL AND ppr.show_in_lead='Y'))";
} else {
$joinCondition .= " AND ppr.show_in_lead='{$imageKind}'";
}
if ($fallbackToProductPhoto) {
// fallback to product photo when variation exists, but has no photo
$qb->leftJoin($aP, 'photos_products_relation', 'ppr2', "{$aP}.id=ppr2.id_product AND ppr2.show_in_lead='Y'");
$qb->leftJoin('ppr2', 'photos', 'ph2', 'ph2.id = ppr2.id_photo');
$aliasesToSelect = ['ph', 'ph2'];
} else {
$aliasesToSelect = ['ph'];
}
} else {
$joinCondition .= " AND ppr.show_in_lead='Y'";
$aliasesToSelect = ['ph'];
}
$qb->leftJoin($aP, 'photos_products_relation', 'ppr', $joinCondition); // + ppr.id_variation
$qb->leftJoin('ppr', 'photos', 'ph', 'ph.id = ppr.id_photo');
$qb->setForceIndexForJoin('ph', 'PRIMARY');
$createSelect = function ($field, $alias = null) use ($aliasesToSelect) {
return 'COALESCE('.implode(
',',
array_map(
function ($val) use ($field) {
return $val.".{$field}";
},
$aliasesToSelect
)
).') as '.($alias ?? $field);
};
$selects = $createSelect('id', 'id_photo');
foreach ($addSelects as $aS) {
$selects .= ', '.$createSelect($aS[0], $aS[1] ?? null);
}
if ($selectAdditional) {
$qb->andWhere(Translation::coalesceTranslatedFields(PhotosTranslation::class, ['descr' => 'descr_photo']));
}
return $selects;
};
}
public static function withVatAndDiscount(QueryBuilder $qb, $priceField = 'p.price')
{
$qb->joinVariationsOnProducts();
$discountField = 'p.discount';
if (findModule(\Modules::PRICELISTS)) {
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
if ($pricelistContext->getActiveId()) {
$qb->andWhere(\KupShop\PricelistBundle\Query\Product::applyPricelist($pricelistContext->getActiveId()))
->leftJoin('prl', 'currencies', 'c', 'c.id=prl.currency');
$priceField = 'COALESCE(prlv.price*c.rate, pv.price, prlp.price*c.rate, p.price)';
$discountField = 'COALESCE(IF(prlv.price IS NULL, NULL, prlp.discount), IF(pv.price IS NULL, NULL, p.discount), IF(prlp.price IS NULL, NULL, prlp.discount), IF(p.price IS NULL, NULL, p.discount))';
}
}
return "{$priceField} * (1+v.vat/100) * (1-{$discountField}/100)";
}
public static function productsAndVariationsIds(array $items): ?callable
{
$specs = [];
foreach ($items as $productId => $variationId) {
if (is_array($variationId)) {
foreach ($variationId as $varId) {
$specs[] = Operator::equalsNullable(['p.id' => $productId, 'pv.id' => $varId]);
}
} elseif (is_null($variationId)) {
$specs[] = Operator::equalsNullable(['p.id' => $productId]);
} else {
$specs[] = Operator::equalsNullable(['p.id' => $productId, 'pv.id' => $variationId]);
}
}
if (!$specs) {
return Operator::andX('1 = 0');
}
return Operator::orX($specs);
}
public static function productsIds($products)
{
return function (QueryBuilder $qb) use ($products) {
$qb->setParameter('products', $products, Connection::PARAM_INT_ARRAY)
->andWhere(Operator::inIntArray($products, 'p.id'))
->addOrderBy('FIELD(p.id, :products)');
};
}
public static function getProductInStock(QueryBuilder $qb)
{
$query = static::getInStoreField(true, $qb);
if (findModule(\Modules::PRODUCTS_SUPPLIERS) && in_array(\Settings::getDefault()->order_availability, [\Settings::ORDER_AVAILABILITY_IN_STORE_OR_SUPPLIER_STORE, \Settings::ORDER_AVAILABILITY_ALL])) {
// in stock = in_store (pokud je > 0) + skladem u dodavatele (pokud je > 0)
// aby nedochazelo k dvojitemu odecitani skladovosti (sklad + pos)
if (!findModule(\Modules::PRODUCTS_SUPPLIERS, \Modules::SUB_ALLOW_NEGATIVE_IN_STORE)) {
$query = "GREATEST(0, {$query})";
}
$query .= ' + (SELECT GREATEST(0, COALESCE(SUM(pos.in_store), 0)) FROM products_of_suppliers pos WHERE p.id = pos.id_product AND ((pv.id IS NULL and pos.id_variation IS NULL) OR (pv.id=pos.id_variation)))';
}
if (findModule('products', 'showMax')) {
$cfg = Config::get();
$query = 'LEAST('.$query.', COALESCE(pv.in_store_show_max, p.in_store_show_max, '.$cfg['Modules']['products']['showMax'].'))';
}
// aby se na FE nedostala zaporna skladovost, minimum je 0
if (findModule(\Modules::PRODUCTS_SUPPLIERS, \Modules::SUB_ALLOW_NEGATIVE_IN_STORE)) {
$query = "GREATEST(0, {$query})";
}
return $query;
}
public static function getInStoreField($useVariations = true, $qb = null)
{
$field = 'in_store';
if ($callable = findModule('products', 'in_store_field')) {
if (is_callable($callable)) {
$field = call_user_func($callable, $useVariations, $qb);
}
}
if ($callable = findModule('products', 'in_store_spec')) {
if (is_callable($callable)) {
return call_user_func($callable, $useVariations, $qb);
}
}
return static::applyProductReservationsOnInStoreField(
$useVariations ? "COALESCE(pv.{$field}, p.{$field})" : 'p.'.$field,
$useVariations
);
}
public static function getStoresQuantityField(QueryBuilder $qb): string
{
if ($callable = findModule('products', 'stores_quantity_spec')) {
if (is_callable($callable)) {
return call_user_func($callable, $qb);
}
}
return 'SUM(si.quantity)';
}
public static function getDiscountField(&$spec): string
{
$spec = null;
$discountField = 'p.discount';
if (findModule(\Modules::PRICE_HISTORY)) {
$price = 'p.price';
$price_for_discount = 'p.price_for_discount';
$price_common = 'p.price_common';
if (findModule(\Modules::PRODUCTS_VARIATIONS)) {
$spec = function (QueryBuilder $qb) {
$qb->joinVariationsOnProducts();
};
$price = 'COALESCE(pv.price, p.price)';
$price_for_discount = 'COALESCE(pv.price_for_discount, p.price_for_discount)';
$price_common = 'COALESCE(pv.price_common, p.price_common)';
}
if (findModule(\Modules::PRODUCTS, \Modules::SUB_PRICE_COMMON)) {
// pokud je price_for_discount deaktivovana, vypocitat slevu z price_common
$price_for_discount = "IF({$price_for_discount} = -1, {$price_common}, {$price_for_discount})";
}
$discountField = "IF({$price_for_discount} > 0, (100 - {$price}*(100-p.discount)/{$price_for_discount}), p.discount)";
$display_discount = \Settings::getDefault()['prod_display_discount'] ?? null;
if ($display_discount == 'Y') {
$discountField = "IF(p.discount > 0, {$discountField}, 0)";
}
$display_discount = \Settings::getDefault()['prod_display_discount_price_common'] ?? null;
if ($display_discount == 'Y') {
$discountField = "IF({$price_common} > 0, {$discountField}, 0)";
}
}
return $discountField;
}
public static function withVat()
{
$vatContext = Contexts::get(VatContext::class);
if ($vatContext->isOssActive()) {
return function (QueryBuilder $qb) {
$defaultVat = Contexts::get(VatContext::class)->getDefault();
$qb->joinVatsOnProducts()
->addSelect('COALESCE(v.id, :default_vat) vat, p.vat AS original_vat')
->setParameter('default_vat', $defaultVat['id']);
};
} else {
return function (QueryBuilder $qb) {
$qb->addSelect('p.vat');
};
}
}
public static function withVats($countries)
{
return function (QueryBuilder $qb) use ($countries) {
$defaultCn = \Settings::getDefault()['oss_vats']['default'] ?? null;
foreach ($countries as $country) {
$qbVat = sqlQueryBuilder()->select('vv.vat')->from('vats', 'vv')
->leftJoin('vv', 'vats_cns', 'vatcns', 'vv.id = vatcns.id_vat')
->andWhere('id_cn = COALESCE(p.id_cn, :defaultCn) AND id_country = :country_'.$country)
->setParameter('defaultCn', $defaultCn)
->setParameter('country_'.$country, $country);
$qb->addSubselect($qbVat, 'vat_'.$country);
}
};
}
public static function allowBuyNotInStore()
{
// vraci spec s delivery_time, ktere nemaji povolen nakup produktu a variant co nejsou skladem (in_store = 0)
$deliveryTimeCfg = Config::get()['Products']['DeliveryTimeConfig'] ?? [];
if (!empty($deliveryTimeCfg)) {
$notAllowed = array_filter($deliveryTimeCfg, function ($key) {
if (!array_key_exists('allowBuy', $key)) {
return false;
}
return !$key['allowBuy'];
});
return Operator::orX(
Operator::inIntArray(array_keys($notAllowed), 'p.delivery_time'),
Operator::inIntArray(array_keys($notAllowed), 'pv.delivery_time'));
}
return Operator::andX('0=0');
}
protected static function applyProductReservationsOnInStoreField(string $inStoreField, bool $useVariations): string
{
if (!findModule(\Modules::PRODUCT_RESERVATIONS)) {
return $inStoreField;
}
$subquery = sqlQueryBuilder()
->select('COALESCE(SUM(pr.quantity), 0)')
->from('product_reservations', 'pr')
->andWhere($useVariations ? 'pv.id = pr.id_variation' : 'p.id = pr.id_product');
if ($ignoredTypes = Contexts::get(UserContext::class)->getIgnoredReservationTypes()) {
$subquery->andWhere(
Operator::not('pr.type IN ('.implode(',', array_map(fn ($x) => '"'.$x.'"', $ignoredTypes)).')')
);
}
return "({$inStoreField} - ({$subquery->getSQL()}))";
}
public static function generatedCoupons()
{
return Operator::equals([JsonOperator::value('p.data', 'generate_coupon') => 'Y']);
}
}

View File

@@ -0,0 +1,766 @@
<?php
namespace Query;
use Doctrine\Bundle\DoctrineBundle\Twig\DoctrineExtension;
use Doctrine\DBAL\Connection;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Context\VatContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\PricelistBundle\Context\PricelistContext;
class QueryBuilderBase extends \Doctrine\DBAL\Query\QueryBuilder
{
private $joins = [];
protected $calcRows = false;
protected $onDuplicateKeyUpdate = [];
protected $sendToMaster = false;
protected $forUpdate = false;
protected bool $multiInsert = false;
protected array $forceIndexes = [];
protected array $withExpressions = [];
/**
* @param mixed|null $selects
*
* @return $this
*/
public function select($selects = null)
{
$selects = func_get_args();
return call_user_func_array([parent::class, __FUNCTION__], $this->evaluateClosures($selects));
}
public function from($from, $alias = null)
{
$from = $this->evaluateClosures([$from])[0];
return call_user_func_array([parent::class, __FUNCTION__], [$from, $alias]);
}
public function addSelect($selects = null)
{
$selects = func_get_args();
return call_user_func_array([parent::class, __FUNCTION__], $this->evaluateClosures($selects));
}
public function set($key, $value)
{
[$key, $value] = $this->evaluateClosures([$key, $value]);
return call_user_func_array([parent::class, __FUNCTION__], [$key, $value]);
}
/**
* @return $this
*/
public function where($predicates)
{
$predicates = func_get_args();
if (isDevelopment()) {
if ($this->getQueryPart('where') !== null) {
/* @noinspection PhpUnhandledExceptionInspection */
throw new \Exception('This call of \'where\' method will reset where query part! Use \'andWhere\' instead.');
}
}
return call_user_func_array([parent::class, __FUNCTION__], $this->evaluateClosures($predicates));
}
/**
* @return $this
*/
public function andWhere($where)
{
$where = func_get_args();
$where = $this->evaluateClosures($where);
// Remove null wheres - caused by specs
$where = array_filter($where, function ($x) {
return $x !== null;
});
if (!count($where)) {
return $this;
}
return call_user_func_array([parent::class, __FUNCTION__], $where);
}
/**
* @return $this
*/
public function orWhere($where)
{
$where = func_get_args();
return call_user_func_array([parent::class, __FUNCTION__], $this->evaluateClosures($where));
}
/**
* @param string $productsAlias
*
* @return \Query\QueryBuilder
*/
public function fromProducts($productsAlias = 'p')
{
return $this->from('products', $productsAlias);
}
/**
* @return array
*
* @internal
*/
public function evaluateClosures(array $predicates)
{
foreach ($predicates as &$predicate) {
while ($predicate instanceof \Closure) {
$predicate = $predicate($this);
}
}
return $predicates;
}
public function add($sqlPartName, $sqlPart, $append = false)
{
if ($sqlPartName === 'join') {
return $this->addJoin(current($sqlPart)['joinAlias'], $sqlPart, $append);
}
if ($sqlPartName === 'select' && $append) {
$selects = $this->getQueryPart('select');
foreach ($sqlPart as $part) {
if (in_array($part, $selects)) {
return $this;
}
}
}
return parent::add($sqlPartName, $sqlPart, $append);
}
public function addParameters(array $parameters, array $types = [])
{
foreach ($parameters as $name => $value) {
$this->setParameter($name, $value, $types[$name] ?? null);
}
return $this;
}
public function orderBySql($orderBySql)
{
return $this->add('orderBy', $orderBySql);
}
public function addSubselect(QueryBuilder $qb, $alias, $template = '{}')
{
$fields = str_replace('{}', "({$qb->getSQL()})", $template);
return $this->addSelect("{$fields} AS {$alias}")
->addParameters($qb->getParameters(), $qb->getParameterTypes());
}
public function joinSubQuery($fromAlias, QueryBuilder $qb, $alias, $condition)
{
return $this->join($fromAlias, '('.$qb->getSQL().')', $alias, $condition)
->addParameters($qb->getParameters(), $qb->getParameterTypes());
}
public function leftJoinSubQuery($fromAlias, QueryBuilder $qb, $alias, $condition)
{
return $this->leftJoin($fromAlias, '('.$qb->getSQL().')', $alias, $condition)
->addParameters($qb->getParameters(), $qb->getParameterTypes());
}
public function crossJoin(string $fromAlias, string $table, string $alias): self
{
return $this->add('join', [
$fromAlias => [
'joinType' => 'cross',
'joinTable' => $table,
'joinAlias' => $alias,
'joinCondition' => null,
],
], true);
}
private function addJoin($joinAlias, $joinDeclaration, $append)
{
$normalizedJoin = mb_strtolower(preg_replace('/\s/', '', implode(current($joinDeclaration))));
if ($this->isJoinPresent($joinAlias, $normalizedJoin)) {
return $this;
}
$this->joins[$joinAlias] = $normalizedJoin;
return parent::add('join', $joinDeclaration, $append);
}
private function isJoinPresent($joinAlias, $normalizedJoin)
{
if (isset($this->joins[$joinAlias])) {
if ($this->joins[$joinAlias] !== $normalizedJoin) {
$existingNormalizedJoin = $this->joins[$joinAlias];
throw new \LogicException("Alias '{$joinAlias}' has already been set with the declaration '{$existingNormalizedJoin}'. Now trying: '{$normalizedJoin}'");
}
return true;
}
return false;
}
public function isAliasPresent($alias)
{
$fromAlias = $this->getQueryPart('from')[0]['alias'] ?? $this->getQueryPart('from')['alias'] ?? '';
return ($fromAlias == $alias) || isset($this->joins[$alias]);
}
public function execute()
{
wpj_debug([$this->getSQL(), $this->getParameters()]);
return parent::execute();
}
public function setUnbufferedMode(): QueryBuilder
{
$this->getConnection()->getWrappedConnection()->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
return $this;
}
public function joinVatsOnProducts()
{
$vatContext = Contexts::get(VatContext::class);
if ($vatContext->isCountryOssActive()) {
$countryContext = Contexts::get(CountryContext::class);
$defaultCn = \Settings::getDefault()['oss_vats']['default'] ?? null;
$this->leftJoin('p', 'vats_cns', 'vatcns', 'COALESCE(p.id_cn, :defaultCn) = vatcns.id_cn')
->leftJoin('vatcns', 'vats', 'v', 'vatcns.id_vat = v.id')
->andWhere(
Operator::orX(
Operator::equals(['v.id_country ' => $countryContext->getActiveId()]),
'vatcns.id_cn IS NULL'
)
)
->setParameter('defaultCn', $defaultCn);
} else {
$this->leftJoin('p', 'vats', 'v', 'p.vat = v.id');
}
return $this;
}
public function joinVariationsOnProducts()
{
$this->leftJoin('p', 'products_variations', 'pv', 'p.id = pv.id_product');
return $this;
}
public function joinSectionsOnProducts()
{
$this->join('p', 'products_in_sections', 'ps', 'p.id = ps.id_product');
return $this;
}
public function joinProducersOnProducts()
{
$this->leftJoin('p', 'producers', 'pr', 'p.producer = pr.id');
return $this;
}
public function joinTemplatesOnProducts()
{
$this->leftJoin('p', 'templates_products', 'tp', 'p.id = tp.id_product');
$this->join('tp', 'templates', 't', 't.id = tp.id_template');
return $this;
}
public function joinVariationsLabelsAndValues()
{
$this->join('pv', 'products_variations_combination', 'pvc', 'pv.id = pvc.id_variation')
->join('pvc', 'products_variations_choices_labels', 'pvcl', 'pvc.id_label=pvcl.id')
->join('pvc', 'products_variations_choices_values', 'pvcv', 'pvc.id_value=pvcv.id')
->join('pvc', 'products_variations_choices_categorization', 'pvcc', 'pvc.id_label=pvcc.id_label and pv.id_product=pvcc.id_product')
->orderBy('pvcc.list_order')
->addOrderBy('pvcv.sort');
return $this;
}
public function joinParametersOnProducts($parametersAlias = 'pap')
{
$this->join('p', 'parameters_products', $parametersAlias, "p.id = {$parametersAlias}.id_product");
return $this;
}
public function joinProductsOfSuppliers(string $productAlias = 'p')
{
// if the spaceship operator causes problems, return it to ((pv.id=pos.id_variation) OR (pv.id IS NULL and pos.id_variation IS NULL))
$this->leftJoin($productAlias, 'products_of_suppliers', 'pos', "{$productAlias}.id = pos.id_product AND pv.id <=> pos.id_variation");
return $this;
}
public function joinPriceListsProducts(?int $priceListId = null, string $productAlias = 'p', string $variationAlias = 'pv'): self
{
$priceListId ??= Contexts::get(PricelistContext::class)->getActiveId();
if ($priceListId) {
$priceListIds = \KupShop\PricelistBundle\Query\Product::getActivePriceLists((int) $priceListId);
$this->setParameter('id_pricelist', $priceListIds, Connection::PARAM_INT_ARRAY)
->leftJoin('p', 'pricelists_products', 'prlp', "prlp.id_product = {$productAlias}.id AND prlp.id_variation IS null AND prlp.id_pricelist IN (:id_pricelist)")
->leftJoin('p', 'pricelists_products', 'prlv', "prlv.id_product = {$productAlias}.id AND prlv.id_variation = {$variationAlias}.id AND prlv.id_pricelist IN (:id_pricelist)")
->leftJoin('prlv', 'pricelists', 'prl', 'prlp.id_pricelist = prl.id')
->leftJoin('prlv', 'pricelists', 'prl_v', 'prlv.id_pricelist = prl_v.id');
if (count($priceListIds) <= 1) {
$this->setForceIndexForJoin('prlp', 'pricelist_product')
->setForceIndexForJoin('prlv', 'pricelist_product');
}
}
return $this;
}
/**
* @return QueryBuilder
* Specifies values for an multi insert query indexed by column names.
* Replaces any previous values, if any.
*
* <code>
* $array =[
* ['name' => '?', 'password' => '?'],
* ['name' => '?', 'password' => '?']
* ];
* </code>
*/
public function multiValues(array $values): QueryBuilder
{
if ($this->getType() !== self::INSERT) {
throw new \InvalidArgumentException('multiValues without INSERT');
}
$this->setMultiInsert(true);
return $this->values($values);
}
/**
* @return QueryBuilder
* Specifies values for a multi insert query indexed by column names.
* One row insert per call
* <code>
* $values =['name' => '?', 'password' => '?']
* </code>
*/
public function multiDirectValues(array $values, array $types = []): QueryBuilder
{
$sqlParts = $this->getQueryParts();
if ($this->getType() !== self::INSERT) {
throw new \InvalidArgumentException('multiValues without INSERT');
}
if (!$this->getMultiInsert()) {
$this->multiValues([]); // vyčistíme values
}
$pf = 'multi'.count($sqlParts['values']).'_';
$array = [];
foreach ($values as $param => $value) {
$key = ":{$pf}{$param}";
$this->setParameter($key, $value, isset($types[$param]) ? $types[$param] : null);
$array['`'.$param.'`'] = $key;
}
$sqlParts['values'][] = $array;
return $this->multiValues($sqlParts['values']);
}
protected function setMultiInsert(bool $multiInsert)
{
$this->multiInsert = $multiInsert;
}
protected function getMultiInsert(): bool
{
return $this->multiInsert;
}
/**
* Specifies values for an insert query indexed by column names
* without need to use parameters()
* Replaces any previous parameters with same name.
*
* <code>
* $qb = $conn->createQueryBuilder()
* ->insert('users')
* ->directValues(
* array(
* 'name' => 'test',
* 'password' => 10
* )
* );
* </code>
*
* @param array $values the values to specify for the insert query indexed by column names
*
* @return $this
*/
public function directValues(array $values, array $types = [])
{
if ($this->getMultiInsert()) {
throw new \InvalidArgumentException('directValues mixed with multiInsertValues');
}
foreach ($values as $param => $value) {
$key = ":{$param}";
if ($this->getType() == self::INSERT) {
$this->setValue('`'.$param.'`', $key);
} elseif ($this->getType() == self::UPDATE) {
$this->set('`'.$param.'`', $key);
} elseif ($this->getType() == self::DELETE) {
throw new \InvalidArgumentException('directValues with DELETE not implemented');
}
$this->setParameter($param, $value, isset($types[$param]) ? $types[$param] : null);
}
return $this;
}
public function getSQL()
{
$hasLimit = $this->getMaxResults() !== null && $this->getMaxResults() > 0;
// custom support for JOINs in UPDATE statements
if ($this->getType() == self::UPDATE && (!empty($this->getQueryPart('join')) || $hasLimit)) {
return $this->getSQLForUpdate();
}
if ($this->getType() === self::INSERT) {
return $this->getSQLForInsert();
}
// custom support for JOINs in DELETE statements
$tableAlias = $this->getQueryPart('from')[0]['alias'] ?? $this->getQueryPart('from')['alias'] ?? '';
// we should use custom supports in DELETE statements even if there is table alias without JOINs
if ($this->getType() == self::DELETE && (!empty($this->getQueryPart('join')) || !empty($tableAlias) || $hasLimit)) {
return $this->getSQLForDelete();
}
if ($this->getType() == self::SELECT && $this->calcRows) {
$selectPart = $this->getQueryPart('select');
if (!empty($selectPart) && strpos($selectPart[0], 'SQL_CALC_FOUND_ROWS') === false) {
$selectPart[0] = 'SQL_CALC_FOUND_ROWS '.$selectPart[0];
$this->select($selectPart);
}
}
$sql = parent::getSQL();
if ($this->forceIndexes) {
$sql = $this->modifySqlAddForceIndexes($sql);
}
if ($this->withExpressions) {
$sql = $this->modifySqlAddWithExpressions($sql);
}
return $sql.($this->forUpdate ? ' FOR UPDATE' : '').($this->sendToMaster ? ' -- maxscale route to master' : '');
}
private function getSQLForUpdate()
{
$sqlParts = $this->getQueryParts();
// Hack: Njncsdd, je to vsechno private a radsi reflection nez kopirovat pulku QB
$reflector = new \ReflectionClass(\Doctrine\DBAL\Query\QueryBuilder::class);
$method = $reflector->getMethod('getFromClauses');
$method->setAccessible(true);
// UPDATE has different "from" structure than SELECT
$from = $this->getQueryPart('from');
if (!isset($from[0])) {
$this->resetQueryPart('from')
->add('from', $from, true);
}
$table = implode(', ', $method->invoke($this));
$query = 'UPDATE '.$table
.' SET '.implode(', ', $sqlParts['set'])
.($sqlParts['where'] !== null ? ' WHERE '.((string) $sqlParts['where']) : '');
if ($orderBy = $sqlParts['orderBy'] ?? []) {
$query .= ' ORDER BY '.implode(', ', $orderBy);
}
if (null !== ($limit = $this->getMaxResults()) && $limit > 0) {
$query .= ' LIMIT '.$limit;
}
return $query;
}
private function getSQLForDelete()
{
$sqlParts = $this->getQueryParts();
// Hack: Njncsdd, je to vsechno private a radsi reflection nez kopirovat pulku QB
$reflector = new \ReflectionClass(\Doctrine\DBAL\Query\QueryBuilder::class);
$method = $reflector->getMethod('getFromClauses');
$method->setAccessible(true);
$tableAlias = $this->getQueryPart('from')[0]['alias'] ?? '';
$isLimitQuery = $this->getMaxResults() !== null && $this->getMaxResults() > 0;
// https://mariadb.com/kb/en/delete/#description
if (($isLimitQuery || !empty($sqlParts['orderBy'])) && (
!empty($sqlParts['join'])
|| $tableAlias
|| (isset($from['table'], $from[0]) && $from['table'] !== $from[0]['table'])
)) {
throw new \Doctrine\DBAL\Exception('Cannot use multi-table syntax with LIMIT or ORDER BY. Remove any joins, multi-table expressions or table aliases in DELETE FROM.');
}
// UPDATE has different "from" structure than SELECT
$from = $this->getQueryPart('from');
if (!isset($from[0])) {
$this->resetQueryPart('from')
->add('from', $from, true);
}
$fromTable = implode(', ', $method->invoke($this));
$query = 'DELETE'.($tableAlias ? " {$tableAlias}" : '').' FROM '.$fromTable.($sqlParts['where'] !== null ? ' WHERE '.((string) $sqlParts['where']) : '');
if ($orderBy = $sqlParts['orderBy'] ?? []) {
$query .= ' ORDER BY '.implode(', ', $orderBy);
}
if ($isLimitQuery) {
$query .= ' LIMIT '.$this->getMaxResults();
}
return $query;
}
private function getSQLForInsert()
{
$sqlParts = $this->getQueryParts();
return 'INSERT INTO '.$sqlParts['from']['table'].$this->getSQLForValues().$this->getSQLForOnDuplicateKeyUpdate();
}
/**
* Implements support for multi-insert.
*
* @return string
*/
private function getSQLForValues()
{
$sqlParts = $this->getQueryParts();
$values = [];
$columnsToUpdate = [];
if ($this->getMultiInsert()) {
$first = reset($sqlParts['values']);
$columnsToUpdate = array_keys($first);
foreach ($sqlParts['values'] as $vls) {
$values[] = '('.implode(', ', $vls).')';
}
} else {
$columnsToUpdate = array_keys($sqlParts['values']);
$values[] = '('.implode(', ', $sqlParts['values']).')';
}
return ' ('.implode(',', $columnsToUpdate).') VALUES '.implode(',', $values).' ';
}
protected function modifySqlAddForceIndexes($sql)
{
$sqlParts = $this->getQueryParts()['join'];
$joinsToModify = [];
foreach ($sqlParts as $tableJoins) {
foreach ($tableJoins as $join) {
if (array_key_exists($join['joinAlias'], $this->forceIndexes)) {
$joinsToModify[] = $join;
}
}
}
$getJoin = function ($join, $index = '') {
$sql = 'JOIN '.$join['joinTable'].' '.$join['joinAlias'];
if ($index) {
$sql .= " FORCE INDEX ({$index})";
}
return $sql.' ON';
};
foreach ($joinsToModify as $join) {
$replacedCount = 0;
$sql = str_replace($getJoin($join), $getJoin($join, $this->forceIndexes[$join['joinAlias']]), $sql, $replacedCount);
if ($replacedCount > 1) {
throw new \LogicException('QueryBuilder tried to add FORCE UPDATE clause to join, but multiple matching joins were found for the alias '.$join['joinAlias']);
}
}
return $sql;
}
public function setForceIndexForJoin($joinAlias, $indexName)
{
$this->forceIndexes[$joinAlias] = $indexName;
return $this;
}
public function withExpression(string $alias, QueryBuilder $qb, ?int $priority = null)
{
$data = [
'sql' => $qb->getSQL(),
'alias' => $alias,
];
if ($priority) {
$this->withExpressions[$priority] = $data;
} else {
$this->withExpressions[] = $data;
}
$this->addParameters($qb->getParameters(), $qb->getParameterTypes());
return $this;
}
protected function modifySqlAddWithExpressions($sql): string
{
$withSql = 'WITH ';
foreach ($this->withExpressions as $with) {
$withSql .= $with['alias'].' AS ('.$with['sql'].')';
if (next($this->withExpressions)) {
$withSql .= ', ';
}
}
return $withSql.' '.$sql;
}
/**
* Implements support for ON DUPLICATE KEY UPDATE.
*
* @return string
*/
private function getSQLForOnDuplicateKeyUpdate()
{
$onDuplicateKeyUpdate = '';
// build on duplicate key update
if ($this->onDuplicateKeyUpdate) {
$onDuplicateKeyUpdate = ' ON DUPLICATE KEY UPDATE ';
$duplicateKeyValues = [];
foreach ($this->onDuplicateKeyUpdate as $column => $value) {
$duplicateKeyValues[] = is_numeric($column) ? $value.'=VALUES('.$value.')' : $column.'='.$value;
}
$onDuplicateKeyUpdate .= implode(', ', $duplicateKeyValues).' ';
}
return $onDuplicateKeyUpdate;
}
/** Používat jen v krajní nouzi */
public function removeJoin($table, $table2)
{
unset($this->joins[$table2]);
$parts = $this->getQueryPart('join');
$this->resetQueryPart('join');
foreach ($parts as $tmp_table => $tmp_part) {
foreach ($tmp_part as $part) {
if ($part['joinAlias'] != $table2) {
parent::add('join', [$tmp_table => $part], true);
}
}
}
}
public function addQueryBuilderParameters(QueryBuilder $qb): QueryBuilder
{
$this->addParameters($qb->getParameters(), $qb->getParameterTypes());
return $this;
}
public function addCalcRows()
{
$this->calcRows = true;
return $this;
}
public function onDuplicateKeyUpdate(array $values): QueryBuilder
{
if ($this->getType() !== self::INSERT) {
throw new \InvalidArgumentException('Call of "onDuplicateKeyUpdate" is allowed only with insert query');
}
$this->onDuplicateKeyUpdate = $values;
return $this;
}
public function sendToMaster()
{
$this->sendToMaster = true;
return $this;
}
public function forUpdate()
{
$this->forUpdate = true;
return $this;
}
/**
* Using on queries with ? may not work.
*
* @return string SQL with inserted params
*/
public function getRunnableSQL(): string
{
$util = new DoctrineExtension();
$sql = $util->replaceQueryParameters($this->getSQL(), $this->getParameters());
return $util->formatSql($sql, false);
}
}
if (empty($subclass)) {
class QueryBuilder extends QueryBuilderBase
{
}
}

245
class/Query/Translation.php Normal file
View File

@@ -0,0 +1,245 @@
<?php
namespace Query;
use KupShop\I18nBundle\Translations\ITranslation;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Util\ArrayUtil;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
class Translation
{
public const COALESCE_STYLE_INHERITANCE = 'inheritance';
public const COALESCE_STYLE_NONE = 'none';
/**
* @return \Closure
*/
public static function adminAlwaysVisible($spec)
{
if (!getAdminUser()) {
return $spec;
}
return function (QueryBuilder $qb) use ($spec) {
$dbcfg = \Settings::getDefault();
$tmp = $dbcfg->hide_not_translated_objects ?? null;
$dbcfg->hide_not_translated_objects = 'N';
$qb->andWhere($spec);
$dbcfg->hide_not_translated_objects = $tmp;
};
}
/**
* @param array|callable|null $columns list of column names ['name', 'title', ...]
*/
public static function withTranslatedFields(
array $languages,
ITranslation $translation,
$columns = null,
bool $frontend = false,
string $coalesceStyle = self::COALESCE_STYLE_NONE,
): callable {
// TODO: Hack
$first = false;
return self::joinTranslations($languages, $translation, function (QueryBuilder $qb, $columnName, $translatedColumn, $langID) use (&$first, $translation, $frontend, $coalesceStyle) {
$translationAlias = $translation->getTableAlias().'_'.$langID;
if (!$frontend && $coalesceStyle == self::COALESCE_STYLE_NONE) {
$qb->addSelect("{$translationAlias}.{$columnName} AS {$langID}_{$columnName}");
// TODO: Rozdelit na specku pouze pro admin
if ($first == false) {
$qb->leftJoin($translationAlias, 'admins', "{$translationAlias}_a", "{$translationAlias}_a.id = {$translationAlias}.id_admin");
$qb->addSelect("{$translationAlias}.created AS {$langID}_created")
->addSelect("{$translationAlias}.updated AS {$langID}_updated")
->addSelect("{$translationAlias}_a.id AS {$langID}_admin_id")
->addSelect("{$translationAlias}_a.email AS {$langID}_admin_email");
$first = true;
}
return false;
} elseif ($coalesceStyle == self::COALESCE_STYLE_INHERITANCE) {
$qb->addSelect("{$translatedColumn} AS {$langID}_{$columnName}");
return false;
}
}, $columns);
}
/**
* @param array|callable|null $columns list of column names ['name', 'title', ...]
*/
public static function joinTranslations(
array $languages,
ITranslation $translation,
callable $fieldsCallback,
$columns = null,
): callable {
return function (QueryBuilder $qb) use ($fieldsCallback, $languages, $translation, $columns) {
if (!findModule(\Modules::TRANSLATIONS)) {
return '1';
}
if (!$languages) {
return '1';
}
$tableAlias = $translation->getTableAlias();
$foreignKeyColumn = $translation->getForeignKeyColumn();
$keyColumn = $translation->getKeyColumn();
if (is_callable($columns)) {
$columnsToSelect = array_keys($translation->getColumns());
$columns = $columns(array_combine($columnsToSelect, $columnsToSelect));
}
if (!is_array($columns)) {
$columns = array_keys($translation->getColumns());
}
if (!ArrayUtil::isDictionary($columns)) {
// convert to assoc
$columns = array_combine($columns, $columns);
}
$inheritedLanguages = static::getInheritedLanguages($languages);
// Pokud je getJoinType join a máme více jazyků, tak použijeme leftJoin a následně použijeme inNotNul v proměnné $whereOr.
// Upraveno kvůli skrývání nepřeložených objektů
$useLeftJoin = count($inheritedLanguages) > 1 && $translation->getJoinType() == 'join';
$join = $useLeftJoin ? 'leftJoin' : $translation->getJoinType();
$whereOr = [];
// join translation tables
foreach ($inheritedLanguages as $langID) {
$translationAlias = $translation->getTableAlias().'_'.$langID;
$qb->setParameter("translation_language_{$langID}", $langID);
$qb->{$join}(
$tableAlias,
$translation->getTableName().'_translations',
$translationAlias,
"{$translationAlias}.{$foreignKeyColumn} = {$tableAlias}.{$keyColumn} AND {$translationAlias}.id_language = :translation_language_{$langID}"
);
$translation->customizeJoinQueryBuilder($qb, $langID);
if ($useLeftJoin) {
$whereOr[] = Operator::isNotNull("{$translationAlias}.{$foreignKeyColumn}");
}
}
if (!empty($whereOr)) {
$qb->andWhere(Operator::orX($whereOr));
}
// add columns
foreach ($languages as $langID) {
foreach ($columns as $columnName => $alias) {
$coalesce = [];
// add translation fields to $coalesce
foreach (static::getInheritedLanguages($langID) as $inheritedLanguageId) {
$translationAlias = $translation->getTableAlias().'_'.$inheritedLanguageId;
$coalesce[] = "{$translationAlias}.{$columnName}";
}
$coalesceField = Operator::coalesce(...$coalesce);
// add select with coalesce to default field
if ($fieldsCallback($qb, $columnName, $coalesceField, $langID) !== false) {
$qb->addSelect("COALESCE({$coalesceField}, {$tableAlias}.{$columnName}) as `{$alias}`");
}
}
}
return '1';
};
}
/**
* @param array|callable|null $columns list of column names ['name', 'title', ...]
*/
public static function coalesceTranslatedFields(
string $translationClass,
$columns = null,
?string $languageID = null,
) {
if (!findModule(\Modules::TRANSLATIONS)) {
if ($columns) {
// Emulate selected columns
$class = new $translationClass();
if (is_callable($columns)) {
$columnsToSelect = array_keys($class->getColumns());
$columns = $columns(array_combine($columnsToSelect, $columnsToSelect));
}
if (!ArrayUtil::isDictionary($columns)) {
// convert to assoc
$columns = array_combine($columns, $columns);
}
return function (QueryBuilder $qb) use ($columns, $class) {
foreach ($columns as $columnName => $translatedField) {
$qb->addSelect("{$class->getTableAlias()}.{$columnName} AS `{$translatedField}`");
}
return '1';
};
}
return '1';
}
/** @var $translation ITranslation */
$translation = ServiceContainer::getService($translationClass);
if (!isset($languageID)) {
$language = ServiceContainer::getService(LanguageContext::class)->getActive();
$languageID = $language->getId();
}
return self::withTranslatedFields([$languageID], $translation, $columns, true);
}
public static function joinTranslatedFields(
string $translationClass,
callable $fieldsCallback,
array $columns,
) {
if (!findModule(\Modules::TRANSLATIONS)) {
if (!ArrayUtil::isDictionary($columns)) {
// convert to assoc
$columns = array_combine($columns, $columns);
}
return function (QueryBuilder $qb) use ($columns, $fieldsCallback, $translationClass) {
$class = new $translationClass();
foreach ($columns as $columnName => $translatedField) {
if ($fieldsCallback($qb, $columnName, null, null) !== false) {
$qb->addSelect("{$class->getTableAlias()}.{$columnName} AS `{$translatedField}`");
}
}
};
}
$translation = ServiceContainer::getService($translationClass);
$language = ServiceContainer::getService(LanguageContext::class)->getActive();
return self::joinTranslations([$language->getId()], $translation, $fieldsCallback, $columns);
}
public static function getInheritedLanguages(array|string $languages): array
{
$languageContext = Contexts::get(LanguageContext::class);
if (is_string($languages)) {
$languages = [$languages];
}
$inheritedLanguages = [];
foreach ($languages as $language) {
$inheritedLanguages = array_unique(array_merge($inheritedLanguages, $languageContext->getInheritance($language)));
}
return $inheritedLanguages;
}
}

60
class/Query/Variation.php Normal file
View File

@@ -0,0 +1,60 @@
<?php
namespace Query;
use Doctrine\DBAL\Connection;
use KupShop\I18nBundle\Translations\VariationsTranslation;
class VariationBase
{
public static function labelsInSections(array $sectionIds)
{
return function (QueryBuilder $qb) use ($sectionIds) {
$qb->join('pvcl', 'products_variations_sections', 'pvs', 'pvcl.id = pvs.id_label');
$qb->setParameter('sectionIds', $sectionIds, Connection::PARAM_INT_ARRAY);
return $qb->expr()->in('pvs.id_section', ':sectionIds');
};
}
public static function isVisible()
{
if (!findModule(\Modules::PRODUCTS_VARIATIONS)) {
return null;
}
static $counter = 0;
return function (QueryBuilder $qb) use (&$counter) {
if ($qb->isAliasPresent('pv')) {
$paramName = 'visible_variation_'.$counter++;
$expression = null;
$qb->setParameter($paramName, 'Y');
$qb->andWhere(
Translation::joinTranslatedFields(
VariationsTranslation::class,
function (QueryBuilder $qb, $columnName, $translatedField) use (&$expression, $paramName) {
$expression = Operator::coalesce($translatedField, "pv.{$columnName}")." = :{$paramName}";
return false;
},
['figure']
)
);
return Operator::orX(
$expression,
Operator::isNull('pv.figure')
);
}
return '1';
};
}
}
if (empty($subclass)) {
class Variation extends VariationBase
{
}
}

39
class/Query/XMLFeed.php Normal file
View File

@@ -0,0 +1,39 @@
<?php
namespace Query;
class XMLFeed
{
public static function categorizedProducts()
{
return function (QueryBuilder $qb) {
return $qb->expr()->isNotNull('ps.id_section');
};
}
public static function productsInFeed()
{
return function (QueryBuilder $qb) {
return $qb->expr()->eq('p.show_in_feed', '1');
};
}
public static function showInStore($in_store_only)
{
return function (QueryBuilder $qb) use ($in_store_only) {
if ($in_store_only && !findModule(\Modules::PRODUCTS_SUPPLIERS)) {
$qb->expr()->gt('COALESCE(pv.in_store, p.in_store)', 0);
}
};
}
public static function limit($count, $offset = null)
{
return function (QueryBuilder $qb) use ($count, $offset) {
if (isset($offset)) {
$qb->setFirstResult($offset);
}
$qb->setMaxResults($count);
};
}
}