878 lines
33 KiB
PHP
878 lines
33 KiB
PHP
<?php
|
|
/**
|
|
* Copyright since 2007 PrestaShop SA and Contributors
|
|
* PrestaShop is an International Registered Trademark & Property of PrestaShop SA
|
|
*
|
|
* NOTICE OF LICENSE
|
|
*
|
|
* This source file is subject to the Academic Free License 3.0 (AFL-3.0)
|
|
* that is bundled with this package in the file LICENSE.md.
|
|
* It is also available through the world-wide-web at this URL:
|
|
* https://opensource.org/licenses/AFL-3.0
|
|
* If you did not receive a copy of the license and are unable to
|
|
* obtain it through the world-wide-web, please send an email
|
|
* to license@prestashop.com so we can send you a copy immediately.
|
|
*
|
|
* @author PrestaShop SA <contact@prestashop.com>
|
|
* @copyright Since 2007 PrestaShop SA and Contributors
|
|
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
|
|
*/
|
|
|
|
namespace PrestaShop\Module\FacetedSearch\Adapter;
|
|
|
|
use Configuration;
|
|
use Context;
|
|
use Db;
|
|
use Doctrine\Common\Collections\ArrayCollection;
|
|
use Product;
|
|
use StockAvailable;
|
|
|
|
class MySQL extends AbstractAdapter
|
|
{
|
|
/**
|
|
* @var string
|
|
*/
|
|
const TYPE = 'MySQL';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
const LEFT_JOIN = 'LEFT JOIN';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
const INNER_JOIN = 'INNER JOIN';
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getMinMaxPriceValue()
|
|
{
|
|
$mysqlAdapter = $this->getFilteredSearchAdapter();
|
|
$mysqlAdapter->copyFilters($this);
|
|
$mysqlAdapter->setSelectFields(['price_min', 'MIN(price_min) as min, MAX(price_max) as max']);
|
|
$mysqlAdapter->setOrderField('');
|
|
|
|
$result = $mysqlAdapter->execute();
|
|
|
|
return [floor((float) $result[0]['min']), ceil((float) $result[0]['max'])];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getFilteredSearchAdapter($resetFilter = null, $skipInitialPopulation = false)
|
|
{
|
|
$mysqlAdapter = new self();
|
|
if ($this->getInitialPopulation() !== null && !$skipInitialPopulation) {
|
|
$mysqlAdapter->initialPopulation = clone $this->getInitialPopulation();
|
|
if ($resetFilter) {
|
|
// Try to reset filter & operations filter
|
|
$mysqlAdapter->initialPopulation->resetFilter($resetFilter);
|
|
$mysqlAdapter->initialPopulation->resetOperationsFilter($resetFilter);
|
|
}
|
|
}
|
|
|
|
return $mysqlAdapter;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function execute()
|
|
{
|
|
return $this->getDatabase()->executeS($this->getQuery());
|
|
}
|
|
|
|
/**
|
|
* Construct the final sql query
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getQuery()
|
|
{
|
|
// Prepare mapping for joined tables
|
|
$filterToTableMapping = $this->getFieldMapping();
|
|
|
|
// Process and generate all fields for the SQL query below
|
|
$orderField = $this->computeOrderByField($filterToTableMapping);
|
|
$selectFields = $this->computeSelectFields($filterToTableMapping);
|
|
$whereConditions = $this->computeWhereConditions($filterToTableMapping);
|
|
$joinConditions = $this->computeJoinConditions($filterToTableMapping);
|
|
$groupFields = $this->computeGroupByFields($filterToTableMapping);
|
|
|
|
// Now, let's build the query...
|
|
// If this query IS the initial population (the base table), we are selecting from product table
|
|
if ($this->getInitialPopulation() === null) {
|
|
$referenceTable = _DB_PREFIX_ . 'product';
|
|
// If not, we will call this function again but for the initial population
|
|
} else {
|
|
$referenceTable = '(' . $this->getInitialPopulation()->getQuery() . ')';
|
|
}
|
|
|
|
// Construct the base query
|
|
$query = 'SELECT ' . implode(', ', $selectFields) . ' FROM ' . $referenceTable . ' p';
|
|
|
|
// Add join conditions if any
|
|
foreach ($joinConditions as $joinAliasInfos) {
|
|
foreach ($joinAliasInfos as $tableAlias => $joinInfos) {
|
|
$query .= ' ' . $joinInfos['joinType'] . ' ' . _DB_PREFIX_ . $joinInfos['tableName'] . ' ' .
|
|
$tableAlias . ' ON ' . $joinInfos['joinCondition'];
|
|
}
|
|
}
|
|
|
|
// Add where conditions if any
|
|
if (!empty($whereConditions)) {
|
|
$query .= ' WHERE ' . implode(' AND ', $whereConditions);
|
|
}
|
|
|
|
// Add groupping
|
|
if (!empty($groupFields)) {
|
|
$query .= ' GROUP BY ' . implode(', ', $groupFields);
|
|
}
|
|
|
|
// Add ordering
|
|
if (!empty($orderField)) {
|
|
$query .= ' ORDER BY ' . $orderField;
|
|
|
|
/*
|
|
* If the result is not ordered by id_product, we add it as a fallback order,
|
|
* to avoid SQL returning it in random order.
|
|
*/
|
|
if (strpos($orderField, 'p.id_product') === false) {
|
|
$query .= ', p.id_product DESC';
|
|
}
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Define the mapping between fields and tables
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getFieldMapping()
|
|
{
|
|
$stockCondition = StockAvailable::addSqlShopRestriction(
|
|
null,
|
|
null,
|
|
'sa'
|
|
);
|
|
|
|
$filterToTableMapping = [
|
|
'id_product_attribute' => [
|
|
'tableName' => 'product_attribute',
|
|
'tableAlias' => 'pa',
|
|
'joinCondition' => '(p.id_product = pa.id_product)',
|
|
'joinType' => self::LEFT_JOIN,
|
|
],
|
|
'id_attribute' => [
|
|
'tableName' => 'product_attribute_combination',
|
|
'tableAlias' => 'pac',
|
|
'joinCondition' => '(pa.id_product_attribute = pac.id_product_attribute)',
|
|
'joinType' => self::LEFT_JOIN,
|
|
'dependencyField' => 'id_product_attribute',
|
|
],
|
|
'id_attribute_group' => [
|
|
'tableName' => 'attribute',
|
|
'tableAlias' => 'a',
|
|
'joinCondition' => '(a.id_attribute = pac.id_attribute)',
|
|
'joinType' => self::INNER_JOIN,
|
|
'dependencyField' => 'id_attribute',
|
|
],
|
|
'id_feature' => [
|
|
'tableName' => 'feature_product',
|
|
'tableAlias' => 'fp',
|
|
'joinCondition' => '(p.id_product = fp.id_product)',
|
|
'joinType' => self::INNER_JOIN,
|
|
],
|
|
'id_shop' => [
|
|
'tableName' => 'product_shop',
|
|
'tableAlias' => 'ps',
|
|
'joinCondition' => '(p.id_product = ps.id_product AND ps.id_shop = ' .
|
|
$this->getContext()->shop->id . ' AND ps.active = TRUE)',
|
|
'joinType' => self::INNER_JOIN,
|
|
],
|
|
'visibility' => [
|
|
'tableName' => 'product_shop',
|
|
'tableAlias' => 'ps',
|
|
'joinCondition' => '(p.id_product = ps.id_product AND ps.id_shop = ' .
|
|
$this->getContext()->shop->id . ' AND ps.active = TRUE)',
|
|
'joinType' => self::INNER_JOIN,
|
|
],
|
|
'id_feature_value' => [
|
|
'tableName' => 'feature_product',
|
|
'tableAlias' => 'fp',
|
|
'joinCondition' => '(p.id_product = fp.id_product)',
|
|
'joinType' => self::LEFT_JOIN,
|
|
],
|
|
'id_category' => [
|
|
'tableName' => 'category_product',
|
|
'tableAlias' => 'cp',
|
|
'joinCondition' => '(p.id_product = cp.id_product)',
|
|
'joinType' => self::INNER_JOIN,
|
|
],
|
|
'position' => [
|
|
'tableName' => 'category_product',
|
|
'tableAlias' => 'cp',
|
|
'joinCondition' => '(p.id_product = cp.id_product)',
|
|
'joinType' => self::INNER_JOIN,
|
|
],
|
|
'manufacturer_name' => [
|
|
'tableName' => 'manufacturer',
|
|
'tableAlias' => 'm',
|
|
'fieldName' => 'name',
|
|
'joinCondition' => '(p.id_manufacturer = m.id_manufacturer)',
|
|
'joinType' => self::LEFT_JOIN,
|
|
],
|
|
'name' => [
|
|
'tableName' => 'product_lang',
|
|
'tableAlias' => 'pl',
|
|
'joinCondition' => '(p.id_product = pl.id_product AND pl.id_shop = ' .
|
|
$this->getContext()->shop->id . ' AND pl.id_lang = ' . $this->getContext()->language->id . ')',
|
|
'joinType' => self::INNER_JOIN,
|
|
],
|
|
'nleft' => [
|
|
'tableName' => 'category',
|
|
'tableAlias' => 'c',
|
|
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
|
|
'joinType' => self::INNER_JOIN,
|
|
'dependencyField' => 'id_category',
|
|
],
|
|
'nright' => [
|
|
'tableName' => 'category',
|
|
'tableAlias' => 'c',
|
|
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
|
|
'joinType' => self::INNER_JOIN,
|
|
'dependencyField' => 'id_category',
|
|
],
|
|
'level_depth' => [
|
|
'tableName' => 'category',
|
|
'tableAlias' => 'c',
|
|
'joinCondition' => '(cp.id_category = c.id_category AND c.active=1)',
|
|
'joinType' => self::INNER_JOIN,
|
|
'dependencyField' => 'id_category',
|
|
],
|
|
'out_of_stock' => [
|
|
'tableName' => 'stock_available',
|
|
'tableAlias' => 'sa',
|
|
'joinCondition' => '(p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute' .
|
|
$stockCondition . ')',
|
|
'joinType' => self::LEFT_JOIN,
|
|
'dependencyField' => 'id_attribute',
|
|
],
|
|
'quantity' => [
|
|
'tableName' => 'stock_available',
|
|
'tableAlias' => 'sa',
|
|
'joinCondition' => '(p.id_product = sa.id_product AND IFNULL(pac.id_product_attribute, 0) = sa.id_product_attribute' .
|
|
$stockCondition . ')',
|
|
'joinType' => self::LEFT_JOIN,
|
|
'dependencyField' => 'id_attribute',
|
|
'aggregateFunction' => 'SUM',
|
|
'aggregateFieldName' => 'quantity',
|
|
],
|
|
'price_min' => [
|
|
'tableName' => 'layered_price_index',
|
|
'tableAlias' => 'psi',
|
|
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
|
|
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
|
|
'joinType' => self::INNER_JOIN,
|
|
],
|
|
'price_max' => [
|
|
'tableName' => 'layered_price_index',
|
|
'tableAlias' => 'psi',
|
|
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
|
|
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
|
|
'joinType' => self::INNER_JOIN,
|
|
],
|
|
'range_start' => [
|
|
'tableName' => 'layered_price_index',
|
|
'tableAlias' => 'psi',
|
|
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
|
|
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
|
|
'joinType' => self::INNER_JOIN,
|
|
],
|
|
'range_end' => [
|
|
'tableName' => 'layered_price_index',
|
|
'tableAlias' => 'psi',
|
|
'joinCondition' => '(psi.id_product = p.id_product AND psi.id_shop = ' . $this->getContext()->shop->id . ' AND psi.id_currency = ' .
|
|
$this->getContext()->currency->id . ' AND psi.id_country = ' . $this->getContext()->country->id . ')',
|
|
'joinType' => self::INNER_JOIN,
|
|
],
|
|
'id_group' => [
|
|
'tableName' => 'category_group',
|
|
'tableAlias' => 'cg',
|
|
'joinCondition' => '(cg.id_category = c.id_category)',
|
|
'joinType' => self::LEFT_JOIN,
|
|
'dependencyField' => 'nleft',
|
|
],
|
|
'sales' => [
|
|
'tableName' => 'product_sale',
|
|
'tableAlias' => 'psales',
|
|
'fieldName' => 'quantity',
|
|
'fieldAlias' => 'sales',
|
|
'joinCondition' => '(psales.id_product = p.id_product)',
|
|
'joinType' => self::LEFT_JOIN,
|
|
],
|
|
'reduction' => [
|
|
'tableName' => 'specific_price',
|
|
'tableAlias' => 'sp',
|
|
'joinCondition' => '(
|
|
sp.id_product = p.id_product AND
|
|
sp.id_shop IN (0, ' . $this->getContext()->shop->id . ') AND
|
|
sp.id_currency IN (0, ' . $this->getContext()->currency->id . ') AND
|
|
sp.id_country IN (0, ' . $this->getContext()->country->id . ') AND
|
|
sp.id_group IN (0, ' . $this->getContext()->customer->id_default_group . ') AND
|
|
sp.from_quantity = 1 AND
|
|
sp.reduction > 0 AND
|
|
sp.id_customer = 0 AND
|
|
sp.id_cart = 0 AND
|
|
(sp.from = \'0000-00-00 00:00:00\' OR \'' . date('Y-m-d H:i:s') . '\' >= sp.from) AND
|
|
(sp.to = \'0000-00-00 00:00:00\' OR \'' . date('Y-m-d H:i:s') . '\' <= sp.to)
|
|
)',
|
|
'joinType' => self::LEFT_JOIN,
|
|
],
|
|
];
|
|
|
|
return $filterToTableMapping;
|
|
}
|
|
|
|
/**
|
|
* Get the joined and escaped value from an multi-dimensional array
|
|
*
|
|
* @param string $separator
|
|
* @param array $values
|
|
*
|
|
* @return string Escaped string value
|
|
*/
|
|
protected function getJoinedEscapedValue($separator, array $values)
|
|
{
|
|
foreach ($values as $key => $value) {
|
|
if (is_array($value)) {
|
|
$values[$key] = $this->getJoinedEscapedValue($separator, $value);
|
|
} elseif (is_numeric($value)) {
|
|
$values[$key] = pSQL($value);
|
|
} else {
|
|
$values[$key] = "'" . pSQL($value) . "'";
|
|
}
|
|
}
|
|
|
|
return implode($separator, $values);
|
|
}
|
|
|
|
/**
|
|
* Compute the orderby fields, adding the proper alias that will be added to the final query
|
|
*
|
|
* @param array $filterToTableMapping
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function computeOrderByField(array $filterToTableMapping)
|
|
{
|
|
// First, we get the order field from the current instance. That can be strings like 'price', 'name', 'position', etc.
|
|
$orderField = $this->getOrderField();
|
|
|
|
// If it's empty, we just return it as is, nothing to do. This is usually a case when getting products
|
|
// for available filters, they reset the order field so we save performance
|
|
if (empty($orderField)) {
|
|
return $orderField;
|
|
}
|
|
|
|
// If we have an initial population, add the field into initial population selects, so we can use it in the outer query for sorting
|
|
if ($this->getInitialPopulation() !== null && !empty($orderField)) {
|
|
$this->getInitialPopulation()->addSelectField($orderField);
|
|
}
|
|
|
|
// Alter order by field if it's a price column
|
|
if ($orderField === 'price') {
|
|
$orderField = $this->getOrderDirection() === 'asc' ? 'price_min' : 'price_max';
|
|
}
|
|
|
|
// Do not try to process the orderField if it already has an alias, or if it's a group function
|
|
// We just append the order direction and return it
|
|
if (strpos($orderField, '.') !== false || strpos($orderField, '(') !== false) {
|
|
return $orderField . ' ' . strtoupper($this->getOrderDirection());
|
|
}
|
|
|
|
// In all other cases, add table mapping or p. prefix depending on field type
|
|
$orderField = $this->computeFieldName($orderField, $filterToTableMapping, true);
|
|
|
|
/*
|
|
* Do not try to process the orderField if it's a search page. We will use manually constructed list
|
|
* to order products by their position in the search results we got from the core, with inverted order
|
|
*/
|
|
if ($orderField == 'p.position' && !empty($this->getInitialPopulation()->getFilters()['id_product']['='][0])) {
|
|
return 'FIELD(p.id_product,' . implode(',', $this->getInitialPopulation()->getFilters()['id_product']['='][0]) . ') ' .
|
|
($this->getOrderDirection() === 'asc' ? 'DESC' : 'ASC');
|
|
}
|
|
|
|
// Alter order by field and add some products to the end of the list, if required
|
|
$orderField = $this->computeShowLast($orderField, $filterToTableMapping);
|
|
|
|
// Add sort order
|
|
$orderField .= ' ' . strtoupper($this->getOrderDirection());
|
|
|
|
// And return it
|
|
return $orderField;
|
|
}
|
|
|
|
/**
|
|
* Sort product list: InStock, OOPS with qty 0, OutOfStock
|
|
*
|
|
* @param string $orderField
|
|
* @param array $filterToTableMapping
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function computeShowLast($orderField, $filterToTableMapping)
|
|
{
|
|
// allow only if feature is enabled & it is main product list query
|
|
if ($this->getInitialPopulation() === null
|
|
|| empty($orderField)
|
|
|| !Configuration::get('PS_LAYERED_FILTER_SHOW_OUT_OF_STOCK_LAST')
|
|
) {
|
|
return $orderField;
|
|
}
|
|
|
|
$this->addSelectField('out_of_stock');
|
|
|
|
// order by out-of-stock last
|
|
$computedQuantityField = $this->computeFieldName('quantity', $filterToTableMapping);
|
|
$byOutOfStockLast = 'IFNULL(' . $computedQuantityField . ', 0) <= 0';
|
|
|
|
/**
|
|
* Default behaviour when out of stock
|
|
* 0 - when deny orders
|
|
* 1 - when allow orders
|
|
*
|
|
* @var int
|
|
*/
|
|
$isAvailableWhenOutOfStock = (int) Product::isAvailableWhenOutOfStock(2);
|
|
|
|
// computing values for order by 'allow to order last'
|
|
$computedField = $this->computeFieldName('out_of_stock', $filterToTableMapping);
|
|
$computedValue = $isAvailableWhenOutOfStock ? 0 : 1;
|
|
$computedDirection = $isAvailableWhenOutOfStock ? 'ASC' : 'DESC';
|
|
|
|
// query: products with zero or less quantity and not available to order go to the end
|
|
$byOOPS = str_replace(
|
|
[':byOutOfStockLast', ':field', ':value', ':direction'],
|
|
[$byOutOfStockLast, $computedField, $computedValue, $computedDirection],
|
|
':byOutOfStockLast AND FIELD(:field, :value) :direction'
|
|
);
|
|
|
|
$orderField = $byOutOfStockLast . ', '
|
|
. $byOOPS . ', '
|
|
. $orderField;
|
|
|
|
return $orderField;
|
|
}
|
|
|
|
/**
|
|
* Add alias to table field name
|
|
*
|
|
* @param string $fieldName
|
|
* @param array $filterToTableMapping
|
|
*
|
|
* @return string Table Field name with an alias
|
|
*/
|
|
protected function computeFieldName($fieldName, $filterToTableMapping, $sortByField = false)
|
|
{
|
|
if (array_key_exists($fieldName, $filterToTableMapping)
|
|
&& (
|
|
// If the requested order field is in the result, no need to change tableAlias
|
|
// unless a fieldName key exists
|
|
isset($filterToTableMapping[$fieldName]['fieldName'])
|
|
|| $this->getInitialPopulation() === null
|
|
|| !$this->getInitialPopulation()->getSelectFields()->contains($fieldName)
|
|
)
|
|
) {
|
|
$joinMapping = $filterToTableMapping[$fieldName];
|
|
$fieldName = $joinMapping['tableAlias'] . '.' . (isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $fieldName);
|
|
if ($sortByField === false) {
|
|
$fieldName .= isset($joinMapping['fieldAlias']) ? ' as ' . $joinMapping['fieldAlias'] : '';
|
|
}
|
|
|
|
if (isset($joinMapping['aggregateFunction'], $joinMapping['aggregateFieldName'])) {
|
|
$fieldName = $joinMapping['aggregateFunction'] . '(' . $fieldName . ') as ' . $joinMapping['aggregateFieldName'];
|
|
}
|
|
} else {
|
|
if (strpos($fieldName, '(') === false) {
|
|
$fieldName = 'p.' . $fieldName;
|
|
}
|
|
}
|
|
|
|
return $fieldName;
|
|
}
|
|
|
|
/**
|
|
* Compute the select fields, adding the proper alias that will be added to the final query
|
|
*
|
|
* @param array $filterToTableMapping
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function computeSelectFields(array $filterToTableMapping)
|
|
{
|
|
// Add already added select fields to current query
|
|
$selectFields = [];
|
|
foreach ($this->getSelectFields() as $key => $selectField) {
|
|
$selectFields[] = $this->computeFieldName($selectField, $filterToTableMapping);
|
|
}
|
|
|
|
return $selectFields;
|
|
}
|
|
|
|
/**
|
|
* Computer the where conditions that will be added to the final query
|
|
*
|
|
* @param array $filterToTableMapping
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function computeWhereConditions(array $filterToTableMapping)
|
|
{
|
|
$whereConditions = [];
|
|
$operationIdx = 0;
|
|
foreach ($this->getOperationsFilters() as $filterName => $filterOperations) {
|
|
$operationsConditions = [];
|
|
foreach ($filterOperations as $operations) {
|
|
$conditions = [];
|
|
foreach ($operations as $idx => $operation) {
|
|
$selectAlias = 'p';
|
|
$values = $operation[1];
|
|
if (array_key_exists($operation[0], $filterToTableMapping)) {
|
|
$joinMapping = $filterToTableMapping[$operation[0]];
|
|
// If index is not the first, append to the table alias for
|
|
// multi join
|
|
$selectAlias = $joinMapping['tableAlias'] .
|
|
($operationIdx === 0 ? '' : '_' . $operationIdx) .
|
|
($idx === 0 ? '' : '_' . $idx);
|
|
$operation[0] = isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $operation[0];
|
|
}
|
|
|
|
if (count($values) === 1) {
|
|
$operator = !empty($operation[2]) ? $operation[2] : '=';
|
|
$conditions[] = $selectAlias . '.' . $operation[0] . $operator . current($values);
|
|
} else {
|
|
$conditions[] = $selectAlias . '.' . $operation[0] . ' IN (' . $this->getJoinedEscapedValue(', ', $values) . ')';
|
|
}
|
|
}
|
|
|
|
$operationsConditions[] = '(' . implode(' AND ', $conditions) . ')';
|
|
}
|
|
|
|
++$operationIdx;
|
|
if (!empty($operationsConditions)) {
|
|
$whereConditions[] = '(' . implode(' OR ', $operationsConditions) . ')';
|
|
}
|
|
}
|
|
|
|
foreach ($this->getFilters() as $filterName => $filterContent) {
|
|
$selectAlias = 'p';
|
|
if (array_key_exists($filterName, $filterToTableMapping)) {
|
|
$joinMapping = $filterToTableMapping[$filterName];
|
|
$selectAlias = $joinMapping['tableAlias'];
|
|
$filterName = isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $filterName;
|
|
}
|
|
|
|
foreach ($filterContent as $operator => $values) {
|
|
if (count($values) == 1) {
|
|
$values = current($values);
|
|
|
|
if ($operator === '=') {
|
|
if (count($values) == 1) {
|
|
$whereConditions[] =
|
|
$selectAlias . '.' . $filterName . $operator . "'" . current($values) . "'";
|
|
} else {
|
|
$whereConditions[] =
|
|
$selectAlias . '.' . $filterName . ' IN (' . $this->getJoinedEscapedValue(', ', $values) . ')';
|
|
}
|
|
} else {
|
|
$orConditions = [];
|
|
foreach ($values as $value) {
|
|
$orConditions[] = $selectAlias . '.' . $filterName . $operator . $value;
|
|
}
|
|
$whereConditions[] = implode(' OR ', $orConditions);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// if we have several "groups" of the same filter, we need to use the intersect of the matching products
|
|
// e.g. : mix of id_feature like Composition & Styles
|
|
$idFilteredProducts = null;
|
|
foreach ($this->getFilters() as $filterName => $filterContent) {
|
|
foreach ($filterContent as $operator => $filterValues) {
|
|
if (count($filterValues) <= 1) {
|
|
continue;
|
|
}
|
|
|
|
$idTmpFilteredProducts = [];
|
|
$mysqlAdapter = $this->getFilteredSearchAdapter();
|
|
$mysqlAdapter->addSelectField('id_product');
|
|
$mysqlAdapter->setOrderField('');
|
|
$mysqlAdapter->addFilter($filterName, $filterValues, $operator);
|
|
$idProducts = $mysqlAdapter->execute();
|
|
foreach ($idProducts as $idProduct) {
|
|
$idTmpFilteredProducts[] = $idProduct['id_product'];
|
|
}
|
|
|
|
if ($idFilteredProducts === null) {
|
|
$idFilteredProducts = $idTmpFilteredProducts;
|
|
} else {
|
|
$idFilteredProducts += array_intersect($idFilteredProducts, $idTmpFilteredProducts);
|
|
}
|
|
|
|
if (empty($idFilteredProducts)) {
|
|
// set it to 0 to make sure no result will be returned
|
|
$idFilteredProducts[] = 0;
|
|
break;
|
|
}
|
|
|
|
$whereConditions[] = 'p.id_product IN (' . implode(', ', $idFilteredProducts) . ')';
|
|
}
|
|
}
|
|
|
|
return $whereConditions;
|
|
}
|
|
|
|
/**
|
|
* Compute the joinConditions needed depending on the fields required in select, where, groupby & orderby fields
|
|
*
|
|
* @param array $filterToTableMapping
|
|
*
|
|
* @return ArrayCollection
|
|
*/
|
|
protected function computeJoinConditions(array $filterToTableMapping)
|
|
{
|
|
$joinList = new ArrayCollection();
|
|
|
|
$this->addJoinList($joinList, $this->getSelectFields(), $filterToTableMapping);
|
|
$this->addJoinList($joinList, $this->getFilters()->getKeys(), $filterToTableMapping);
|
|
|
|
$operationIdx = 0;
|
|
foreach ($this->getOperationsFilters() as $filterOperations) {
|
|
foreach ($filterOperations as $operations) {
|
|
foreach ($operations as $idx => $operation) {
|
|
if (array_key_exists($operation[0], $filterToTableMapping)) {
|
|
$joinMapping = $filterToTableMapping[$operation[0]];
|
|
if ($idx !== 0 || $operationIdx !== 0) {
|
|
// Index is not the first, append index to tableAlias on joinCondition
|
|
$joinMapping['joinCondition'] = preg_replace(
|
|
'~([\(\s=]' . $joinMapping['tableAlias'] . ')\.~',
|
|
'${1}' .
|
|
($operationIdx === 0 ? '' : '_' . $operationIdx) .
|
|
($idx === 0 ? '' : '_' . $idx) .
|
|
'.',
|
|
$joinMapping['joinCondition']
|
|
);
|
|
$joinMapping['tableAlias'] .= ($operationIdx === 0 ? '' : '_' . $operationIdx) .
|
|
($idx === 0 ? '' : '_' . $idx);
|
|
}
|
|
|
|
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
|
|
}
|
|
}
|
|
}
|
|
++$operationIdx;
|
|
}
|
|
|
|
$this->addJoinList($joinList, $this->getGroupFields()->getKeys(), $filterToTableMapping);
|
|
|
|
if (array_key_exists($this->getOrderField(), $filterToTableMapping)) {
|
|
$joinMapping = $filterToTableMapping[$this->getOrderField()];
|
|
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
|
|
}
|
|
|
|
return $joinList;
|
|
}
|
|
|
|
/**
|
|
* Helper to add tables infos to the join list.
|
|
*
|
|
* @param ArrayCollection $joinList
|
|
* @param array|ArrayCollection $list
|
|
* @param array $filterToTableMapping
|
|
*/
|
|
private function addJoinList(ArrayCollection $joinList, $list, array $filterToTableMapping)
|
|
{
|
|
foreach ($list as $field) {
|
|
if (array_key_exists($field, $filterToTableMapping)) {
|
|
$joinMapping = $filterToTableMapping[$field];
|
|
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add the required table infos to the join list, taking care of the dependent tables
|
|
*
|
|
* @param ArrayCollection $joinList
|
|
* @param array $joinMapping
|
|
* @param array $filterToTableMapping
|
|
*/
|
|
private function addJoinConditions(ArrayCollection $joinList, array $joinMapping, array $filterToTableMapping)
|
|
{
|
|
if (array_key_exists('dependencyField', $joinMapping)) {
|
|
$dependencyJoinMapping = $filterToTableMapping[$joinMapping['dependencyField']];
|
|
$this->addJoinConditions($joinList, $dependencyJoinMapping, $filterToTableMapping);
|
|
}
|
|
$joinInfos[$joinMapping['tableAlias']] = [
|
|
'tableName' => $joinMapping['tableName'],
|
|
'joinCondition' => $joinMapping['joinCondition'],
|
|
'joinType' => $joinMapping['joinType'],
|
|
];
|
|
|
|
$joinList->set($joinMapping['tableAlias'] . '_' . $joinMapping['tableName'], $joinInfos);
|
|
}
|
|
|
|
/**
|
|
* Compute the groupby condition, adding the proper alias that will be added to the final query
|
|
*
|
|
* @param array $filterToTableMapping
|
|
*
|
|
* @return array
|
|
*/
|
|
private function computeGroupByFields(array $filterToTableMapping)
|
|
{
|
|
$groupFields = [];
|
|
if ($this->getGroupFields()->isEmpty()) {
|
|
return $groupFields;
|
|
}
|
|
|
|
foreach ($this->getGroupFields() as $key => $values) {
|
|
if (strpos($values, '.') !== false
|
|
|| strpos($values, '(') !== false) {
|
|
$groupFields[$key] = $values;
|
|
continue;
|
|
}
|
|
|
|
if (array_key_exists($values, $filterToTableMapping)) {
|
|
$joinMapping = $filterToTableMapping[$values];
|
|
$groupFields[$key] = $joinMapping['tableAlias'] . '.' . $values;
|
|
} else {
|
|
$groupFields[$key] = 'p.' . $values;
|
|
}
|
|
}
|
|
|
|
return $groupFields;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getMinMaxValue($fieldName)
|
|
{
|
|
$mysqlAdapter = $this->getFilteredSearchAdapter();
|
|
$mysqlAdapter->copyFilters($this);
|
|
$mysqlAdapter->setSelectFields(['MIN(' . $fieldName . ') as min, MAX(' . $fieldName . ') as max']);
|
|
$mysqlAdapter->setOrderField('');
|
|
|
|
$result = $mysqlAdapter->execute();
|
|
|
|
return [(float) $result[0]['min'], (float) $result[0]['max']];
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function count()
|
|
{
|
|
$mysqlAdapter = $this->getFilteredSearchAdapter();
|
|
$mysqlAdapter->copyFilters($this);
|
|
|
|
$result = $mysqlAdapter->valueCount();
|
|
|
|
return isset($result[0]['c']) ? (int) $result[0]['c'] : 0;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function valueCount($fieldName = null)
|
|
{
|
|
$this->resetGroupBy();
|
|
if ($fieldName !== null) {
|
|
$this->addGroupBy($fieldName);
|
|
$this->addSelectField($fieldName);
|
|
}
|
|
|
|
$this->addSelectField('COUNT(DISTINCT p.id_product) c');
|
|
$this->setOrderField('');
|
|
|
|
$this->copyOperationsFilters();
|
|
|
|
return $this->execute();
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function useFiltersAsInitialPopulation()
|
|
{
|
|
// Initial population has no ORDER BY
|
|
$this->setOrderField('');
|
|
|
|
// We add basic select fields we will need to matter what
|
|
$this->setSelectFields(
|
|
[
|
|
'id_product',
|
|
'id_manufacturer',
|
|
'quantity',
|
|
'condition',
|
|
'weight',
|
|
'price',
|
|
'sales',
|
|
'on_sale',
|
|
'date_add',
|
|
]
|
|
);
|
|
|
|
// Clone it, add it to initial population
|
|
$this->initialPopulation = clone $this;
|
|
|
|
// Reset all filters so we start clean and add only the base select, we don't need anything else
|
|
$this->resetAll();
|
|
$this->addSelectField('id_product');
|
|
}
|
|
|
|
/**
|
|
* @return Context
|
|
*/
|
|
protected function getContext()
|
|
{
|
|
return Context::getContext();
|
|
}
|
|
|
|
/**
|
|
* @return Db
|
|
*/
|
|
protected function getDatabase()
|
|
{
|
|
return Db::getInstance();
|
|
}
|
|
|
|
/**
|
|
* Copy stock management operation filters
|
|
* to make sure quantity is also used
|
|
*/
|
|
protected function copyOperationsFilters()
|
|
{
|
|
$initialPopulation = $this->getInitialPopulation();
|
|
if (null === $initialPopulation) {
|
|
return;
|
|
}
|
|
|
|
$operationsFilters = clone $initialPopulation->getOperationsFilters();
|
|
foreach ($operationsFilters as $operationName => $operations) {
|
|
$this->addOperationsFilter(
|
|
$operationName,
|
|
$operations
|
|
);
|
|
}
|
|
}
|
|
}
|