first commit
This commit is contained in:
656
class/Query/Filter.php
Normal file
656
class/Query/Filter.php
Normal 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
333
class/Query/Operator.php
Normal 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
191
class/Query/Order.php
Normal 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
28
class/Query/Parameter.php
Normal 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
41
class/Query/Price.php
Normal 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
458
class/Query/Product.php
Normal 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']);
|
||||
}
|
||||
}
|
||||
766
class/Query/QueryBuilder.php
Normal file
766
class/Query/QueryBuilder.php
Normal 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
245
class/Query/Translation.php
Normal 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
60
class/Query/Variation.php
Normal 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
39
class/Query/XMLFeed.php
Normal 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);
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user