first commit
This commit is contained in:
328
modules/ps_facetedsearch/src/Adapter/AbstractAdapter.php
Normal file
328
modules/ps_facetedsearch/src/Adapter/AbstractAdapter.php
Normal file
@@ -0,0 +1,328 @@
|
||||
<?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 Doctrine\Common\Collections\ArrayCollection;
|
||||
|
||||
abstract class AbstractAdapter implements InterfaceAdapter
|
||||
{
|
||||
/**
|
||||
* @var ArrayCollection
|
||||
*/
|
||||
protected $filters;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection
|
||||
*/
|
||||
protected $operationsFilters;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection
|
||||
*/
|
||||
protected $selectFields;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection
|
||||
*/
|
||||
protected $groupFields;
|
||||
|
||||
protected $orderField = 'id_product';
|
||||
|
||||
protected $orderDirection = 'DESC';
|
||||
|
||||
protected $limit = 20;
|
||||
|
||||
protected $offset = 0;
|
||||
|
||||
/** @var InterfaceAdapter */
|
||||
protected $initialPopulation = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->groupFields = new ArrayCollection();
|
||||
$this->selectFields = new ArrayCollection();
|
||||
$this->filters = new ArrayCollection();
|
||||
$this->operationsFilters = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->filters = clone $this->filters;
|
||||
$this->operationsFilters = clone $this->operationsFilters;
|
||||
$this->groupFields = clone $this->groupFields;
|
||||
$this->selectFields = clone $this->selectFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getInitialPopulation()
|
||||
{
|
||||
return $this->initialPopulation;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resetFilter($filterName)
|
||||
{
|
||||
if ($this->filters->offsetExists($filterName)) {
|
||||
$this->filters->offsetUnset($filterName);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resetOperationsFilter($filterName)
|
||||
{
|
||||
if ($this->operationsFilters->offsetExists($filterName)) {
|
||||
$this->operationsFilters->offsetUnset($filterName);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resetOperationsFilters()
|
||||
{
|
||||
$this->operationsFilters = new ArrayCollection();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resetAll()
|
||||
{
|
||||
$this->selectFields = new ArrayCollection();
|
||||
$this->groupFields = new ArrayCollection();
|
||||
$this->filters = new ArrayCollection();
|
||||
$this->operationsFilters = new ArrayCollection();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilter($filterName)
|
||||
{
|
||||
if (isset($this->filters[$filterName])) {
|
||||
return $this->filters[$filterName];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOrderDirection()
|
||||
{
|
||||
return $this->orderDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOrderField()
|
||||
{
|
||||
return $this->orderField;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getGroupFields()
|
||||
{
|
||||
return $this->groupFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSelectFields()
|
||||
{
|
||||
return $this->selectFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilters()
|
||||
{
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOperationsFilters()
|
||||
{
|
||||
return $this->operationsFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function copyFilters(InterfaceAdapter $adapter)
|
||||
{
|
||||
$this->filters = clone $adapter->getFilters();
|
||||
$this->operationsFilters = clone $adapter->getOperationsFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addFilter($filterName, $values, $operator = '=')
|
||||
{
|
||||
$filters = $this->filters->get($filterName);
|
||||
if (!isset($filters[$operator])) {
|
||||
$filters[$operator] = [];
|
||||
}
|
||||
|
||||
$filters[$operator][] = $values;
|
||||
$this->filters->set($filterName, $filters);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addOperationsFilter($filterName, array $operations = [])
|
||||
{
|
||||
$this->operationsFilters->set($filterName, $operations);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addSelectField($fieldName)
|
||||
{
|
||||
if (!$this->selectFields->contains($fieldName)) {
|
||||
$this->selectFields->add($fieldName);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSelectFields($selectFields)
|
||||
{
|
||||
$this->selectFields = new ArrayCollection($selectFields);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resetSelectField()
|
||||
{
|
||||
$this->selectFields = new ArrayCollection();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addGroupBy($groupField)
|
||||
{
|
||||
$this->groupFields->add($groupField);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setGroupFields($groupFields)
|
||||
{
|
||||
$this->groupFields = new ArrayCollection($groupFields);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function resetGroupBy()
|
||||
{
|
||||
$this->groupFields = new ArrayCollection();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setFilter($filterName, $value)
|
||||
{
|
||||
if ($value !== null) {
|
||||
$this->filters->set($filterName, $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOrderField($fieldName)
|
||||
{
|
||||
$this->orderField = $fieldName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setOrderDirection($direction)
|
||||
{
|
||||
$this->orderDirection = $direction === 'desc' ? 'desc' : 'asc';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setLimit($limit, $offset = 0)
|
||||
{
|
||||
$this->limit = $limit ? (int) $limit : null;
|
||||
$this->offset = (int) $offset;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
294
modules/ps_facetedsearch/src/Adapter/InterfaceAdapter.php
Normal file
294
modules/ps_facetedsearch/src/Adapter/InterfaceAdapter.php
Normal file
@@ -0,0 +1,294 @@
|
||||
<?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;
|
||||
|
||||
interface InterfaceAdapter
|
||||
{
|
||||
/**
|
||||
* Set order by field
|
||||
*
|
||||
* @param string $fieldName
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setOrderField($fieldName);
|
||||
|
||||
/**
|
||||
* Set the order by direction for the given field
|
||||
*
|
||||
* @param string $direction
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setOrderDirection($direction);
|
||||
|
||||
/**
|
||||
* Set the limit and offset associated with the current search
|
||||
*
|
||||
* @param int|null $limit
|
||||
* @param int $offset
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setLimit($limit, $offset = 0);
|
||||
|
||||
/**
|
||||
* Execute the search
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function execute();
|
||||
|
||||
/**
|
||||
* Get the current query
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQuery();
|
||||
|
||||
/**
|
||||
* Get the min & max value of the field filedName associated with the current search
|
||||
*
|
||||
* @param string $fieldName
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getMinMaxValue($fieldName);
|
||||
|
||||
/**
|
||||
* Get the min & max value of the price associated with the current search
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMinMaxPriceValue();
|
||||
|
||||
/**
|
||||
* Return order direction associated with the current search
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getOrderDirection();
|
||||
|
||||
/**
|
||||
* Return order field associated with the current search
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getOrderField();
|
||||
|
||||
/**
|
||||
* Return all group fields associated with the current search
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getGroupFields();
|
||||
|
||||
/**
|
||||
* Return all selected fields associated with the current search
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getSelectFields();
|
||||
|
||||
/**
|
||||
* Return all the filters associated with the current search
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFilters();
|
||||
|
||||
/**
|
||||
* Return all the operations filters associated with the current search
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getOperationsFilters();
|
||||
|
||||
/**
|
||||
* Return the number of results associated for the current search
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function count();
|
||||
|
||||
/**
|
||||
* Move the current search into the "initialPopulation"
|
||||
* This initialPopulation will be used to generate the first derived table 'FROM (SELECT ...)' in the final query
|
||||
* e.g. : SELECT ... FROM (initialPopulation) p JOIN ....
|
||||
*/
|
||||
public function useFiltersAsInitialPopulation();
|
||||
|
||||
/**
|
||||
* Create a new SearchAdapter, keeping the initialPopulation of the current Search
|
||||
*
|
||||
* @param string $resetFilter reset this filter inside the initialPopulation
|
||||
* @param bool $skipInitialPopulation if enable, do not copy the initialPopulation filter
|
||||
*
|
||||
* @return InterfaceAdapter
|
||||
*/
|
||||
public function getFilteredSearchAdapter($resetFilter = null, $skipInitialPopulation = false);
|
||||
|
||||
/**
|
||||
* Add a new filter with filterName, operator & values to the current search
|
||||
* If several values are provided with the = operator, it's converted automatically to a IN () in the final query
|
||||
*
|
||||
* @param string $filterName
|
||||
* @param array $values
|
||||
* @param string $operator
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function addFilter($filterName, $values, $operator = '=');
|
||||
|
||||
/**
|
||||
* Add a stack of operations with filterName. Operations must contains filterName, values and to the current search
|
||||
*
|
||||
* @param string $filterName
|
||||
* @param array $operations
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function addOperationsFilter($filterName, array $operations);
|
||||
|
||||
/**
|
||||
* Add fieldName in the current search result
|
||||
*
|
||||
* @param string $fieldName
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function addSelectField($fieldName);
|
||||
|
||||
/**
|
||||
* Returns the number of distinct products, group by fieldName values
|
||||
*
|
||||
* @param string $fieldName
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function valueCount($fieldName = null);
|
||||
|
||||
/**
|
||||
* Reset the operations filters
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function resetOperationsFilters();
|
||||
|
||||
/**
|
||||
* Reset the operations filter for the given filterName
|
||||
*
|
||||
* @param string $filterName
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function resetOperationsFilter($filterName);
|
||||
|
||||
/**
|
||||
* Reset the filter for the given filterName
|
||||
*
|
||||
* @param string $filterName
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function resetFilter($filterName);
|
||||
|
||||
/**
|
||||
* Return the filter associated with filterName
|
||||
*
|
||||
* @param string $filterName
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFilter($filterName);
|
||||
|
||||
/**
|
||||
* Set the filterName to the given array value
|
||||
*
|
||||
* @param string $filterName
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setFilter($filterName, $value);
|
||||
|
||||
/**
|
||||
* Return the current initialPopulation
|
||||
*
|
||||
* @return self|null
|
||||
*/
|
||||
public function getInitialPopulation();
|
||||
|
||||
/**
|
||||
* Return all the filters / groupFields / selectFields
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function resetAll();
|
||||
|
||||
/**
|
||||
* Copy all the filters & operationsFilters from adapter to the current search
|
||||
*
|
||||
* @param InterfaceAdapter $adapter
|
||||
*/
|
||||
public function copyFilters(InterfaceAdapter $adapter);
|
||||
|
||||
/**
|
||||
* Set all the select fields
|
||||
*
|
||||
* @param array $selectFields
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setSelectFields($selectFields);
|
||||
|
||||
/**
|
||||
* Reset all the select fields
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function resetSelectField();
|
||||
|
||||
/**
|
||||
* Add a group by field
|
||||
*
|
||||
* @param string $groupField
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function addGroupBy($groupField);
|
||||
|
||||
/**
|
||||
* Set the group by fields
|
||||
*
|
||||
* @param array $groupFields
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function setGroupFields($groupFields);
|
||||
|
||||
/**
|
||||
* Reset the group by conditions
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function resetGroupBy();
|
||||
}
|
||||
815
modules/ps_facetedsearch/src/Adapter/MySQL.php
Normal file
815
modules/ps_facetedsearch/src/Adapter/MySQL.php
Normal file
@@ -0,0 +1,815 @@
|
||||
<?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->setLimit(null);
|
||||
$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()
|
||||
{
|
||||
$filterToTableMapping = $this->getFieldMapping();
|
||||
$orderField = $this->computeOrderByField($filterToTableMapping);
|
||||
|
||||
if ($this->getInitialPopulation() === null) {
|
||||
$referenceTable = _DB_PREFIX_ . 'product';
|
||||
} else {
|
||||
$referenceTable = '(' . $this->getInitialPopulation()->getQuery() . ')';
|
||||
}
|
||||
|
||||
$query = 'SELECT ';
|
||||
|
||||
$selectFields = $this->computeSelectFields($filterToTableMapping);
|
||||
$whereConditions = $this->computeWhereConditions($filterToTableMapping);
|
||||
$joinConditions = $this->computeJoinConditions($filterToTableMapping);
|
||||
$groupFields = $this->computeGroupByFields($filterToTableMapping);
|
||||
|
||||
$query .= implode(', ', $selectFields) . ' FROM ' . $referenceTable . ' p';
|
||||
|
||||
foreach ($joinConditions as $joinAliasInfos) {
|
||||
foreach ($joinAliasInfos as $tableAlias => $joinInfos) {
|
||||
$query .= ' ' . $joinInfos['joinType'] . ' ' . _DB_PREFIX_ . $joinInfos['tableName'] . ' ' .
|
||||
$tableAlias . ' ON ' . $joinInfos['joinCondition'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($whereConditions)) {
|
||||
$query .= ' WHERE ' . implode(' AND ', $whereConditions);
|
||||
}
|
||||
|
||||
if ($groupFields) {
|
||||
$query .= ' GROUP BY ' . implode(', ', $groupFields);
|
||||
}
|
||||
|
||||
if ($orderField) {
|
||||
$query .= ' ORDER BY ' . $orderField . ' ' . strtoupper($this->getOrderDirection());
|
||||
if ($orderField !== 'p.id_product') {
|
||||
$query .= ', p.id_product DESC';
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->limit !== null) {
|
||||
$query .= ' LIMIT ' . $this->offset . ', ' . $this->limit;
|
||||
}
|
||||
|
||||
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,
|
||||
],
|
||||
'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::INNER_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,
|
||||
],
|
||||
];
|
||||
|
||||
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)
|
||||
{
|
||||
$orderField = $this->getOrderField();
|
||||
|
||||
if ($this->getInitialPopulation() !== null && !empty($orderField)) {
|
||||
$this->getInitialPopulation()->addSelectField($orderField);
|
||||
}
|
||||
|
||||
// do not try to process the orderField if it already has an alias, or if it's a group function
|
||||
if (empty($orderField) || strpos($orderField, '.') !== false
|
||||
|| strpos($orderField, '(') !== false) {
|
||||
return $orderField;
|
||||
}
|
||||
|
||||
if ($orderField === 'price') {
|
||||
$orderField = $this->getOrderDirection() === 'asc' ? 'price_min' : 'price_max';
|
||||
}
|
||||
|
||||
$orderField = $this->computeFieldName($orderField, $filterToTableMapping, true);
|
||||
|
||||
// put some products at the end of the list
|
||||
$orderField = $this->computeShowLast($orderField, $filterToTableMapping);
|
||||
|
||||
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)
|
||||
{
|
||||
$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->setLimit(null);
|
||||
$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->setLimit(null);
|
||||
$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->setLimit(null);
|
||||
$this->setOrderField('');
|
||||
|
||||
$this->copyOperationsFilters();
|
||||
|
||||
return $this->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function useFiltersAsInitialPopulation()
|
||||
{
|
||||
$this->setLimit(null);
|
||||
$this->setOrderField('');
|
||||
$this->setSelectFields(
|
||||
[
|
||||
'id_product',
|
||||
'id_manufacturer',
|
||||
'quantity',
|
||||
'condition',
|
||||
'weight',
|
||||
'price',
|
||||
'sales',
|
||||
]
|
||||
);
|
||||
$this->initialPopulation = clone $this;
|
||||
$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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
36
modules/ps_facetedsearch/src/Constraint/UrlSegment.php
Normal file
36
modules/ps_facetedsearch/src/Constraint/UrlSegment.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
class UrlSegment extends Constraint
|
||||
{
|
||||
public $message = '%s is invalid.';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validatedBy()
|
||||
{
|
||||
return UrlSegmentValidator::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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\Constraint;
|
||||
|
||||
use PrestaShop\PrestaShop\Adapter\Tools;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
/**
|
||||
* Class UrlSegmentValidator responsible for validating an URL segment.
|
||||
*/
|
||||
class UrlSegmentValidator extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
* @var Tools
|
||||
*/
|
||||
private $tools;
|
||||
|
||||
/**
|
||||
* @param Tools $tools
|
||||
*/
|
||||
public function __construct(Tools $tools)
|
||||
{
|
||||
$this->tools = $tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint)
|
||||
{
|
||||
if (!$constraint instanceof UrlSegment) {
|
||||
throw new UnexpectedTypeException($constraint, UrlSegment::class);
|
||||
}
|
||||
|
||||
if (null === $value || '' === $value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strtolower($value) !== $this->tools->linkRewrite($value)) {
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setTranslationDomain('Admin.Notifications.Error')
|
||||
->setParameter('%s', $this->formatValue($value))
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
982
modules/ps_facetedsearch/src/Filters/Block.php
Normal file
982
modules/ps_facetedsearch/src/Filters/Block.php
Normal file
@@ -0,0 +1,982 @@
|
||||
<?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\Filters;
|
||||
|
||||
use Category;
|
||||
use Configuration;
|
||||
use Context;
|
||||
use Db;
|
||||
use Feature;
|
||||
use Group;
|
||||
use Manufacturer;
|
||||
use PrestaShop\Module\FacetedSearch\Adapter\InterfaceAdapter;
|
||||
use PrestaShop\Module\FacetedSearch\Product\Search;
|
||||
use PrestaShop\PrestaShop\Core\Localization\Locale;
|
||||
use PrestaShop\PrestaShop\Core\Localization\Specification\NumberSymbolList;
|
||||
use PrestaShopDatabaseException;
|
||||
use Tools;
|
||||
|
||||
/**
|
||||
* Display filters block on navigation
|
||||
*/
|
||||
class Block
|
||||
{
|
||||
/**
|
||||
* @var InterfaceAdapter
|
||||
*/
|
||||
private $searchAdapter;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $psStockManagement;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $psOrderOutOfStock;
|
||||
|
||||
/**
|
||||
* @var Context
|
||||
*/
|
||||
private $context;
|
||||
|
||||
/**
|
||||
* @var Db
|
||||
*/
|
||||
private $database;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $attributesGroup;
|
||||
|
||||
/**
|
||||
* @var DataAccessor
|
||||
*/
|
||||
private $dataAccessor;
|
||||
|
||||
public function __construct(InterfaceAdapter $searchAdapter, Context $context, Db $database, DataAccessor $dataAccessor)
|
||||
{
|
||||
$this->searchAdapter = $searchAdapter;
|
||||
$this->context = $context;
|
||||
$this->database = $database;
|
||||
$this->dataAccessor = $dataAccessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $nbProducts
|
||||
* @param array $selectedFilters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFilterBlock(
|
||||
$nbProducts,
|
||||
$selectedFilters
|
||||
) {
|
||||
$idLang = (int) $this->context->language->id;
|
||||
$idShop = (int) $this->context->shop->id;
|
||||
$idParent = (int) Tools::getValue(
|
||||
'id_category',
|
||||
Tools::getValue('id_category_layered', Configuration::get('PS_HOME_CATEGORY'))
|
||||
);
|
||||
|
||||
/* Get the filters for the current category */
|
||||
$filters = $this->database->executeS(
|
||||
'SELECT type, id_value, filter_show_limit, filter_type ' .
|
||||
'FROM ' . _DB_PREFIX_ . 'layered_category ' .
|
||||
'WHERE id_category = ' . $idParent . ' ' .
|
||||
'AND id_shop = ' . $idShop . ' ' .
|
||||
'GROUP BY `type`, id_value ORDER BY position ASC'
|
||||
);
|
||||
|
||||
$filterBlocks = [];
|
||||
// iterate through each filter, and the get corresponding filter block
|
||||
foreach ($filters as $filter) {
|
||||
switch ($filter['type']) {
|
||||
case 'price':
|
||||
$filterBlocks[] = $this->getPriceRangeBlock($filter, $selectedFilters, $nbProducts);
|
||||
break;
|
||||
case 'weight':
|
||||
$filterBlocks[] = $this->getWeightRangeBlock($filter, $selectedFilters, $nbProducts);
|
||||
break;
|
||||
case 'condition':
|
||||
$filterBlocks[] = $this->getConditionsBlock($filter, $selectedFilters);
|
||||
break;
|
||||
case 'quantity':
|
||||
$filterBlocks[] = $this->getQuantitiesBlock($filter, $selectedFilters);
|
||||
break;
|
||||
case 'manufacturer':
|
||||
$filterBlocks[] = $this->getManufacturersBlock($filter, $selectedFilters, $idLang);
|
||||
break;
|
||||
case 'id_attribute_group':
|
||||
$filterBlocks =
|
||||
array_merge($filterBlocks, $this->getAttributesBlock($filter, $selectedFilters, $idLang));
|
||||
break;
|
||||
case 'id_feature':
|
||||
$filterBlocks =
|
||||
array_merge($filterBlocks, $this->getFeaturesBlock($filter, $selectedFilters, $idLang));
|
||||
break;
|
||||
case 'category':
|
||||
$parent = new Category($idParent, $idLang);
|
||||
$filterBlocks[] = $this->getCategoriesBlock($filter, $selectedFilters, $idLang, $parent);
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'filters' => $filterBlocks,
|
||||
];
|
||||
}
|
||||
|
||||
protected function showPriceFilter()
|
||||
{
|
||||
return Group::getCurrent()->show_prices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the filter block from the cache table
|
||||
*
|
||||
* @param string $filterHash
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
public function getFromCache($filterHash)
|
||||
{
|
||||
if (!Configuration::get('PS_LAYERED_CACHE_ENABLED')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$row = $this->database->getRow(
|
||||
'SELECT data FROM ' . _DB_PREFIX_ . 'layered_filter_block WHERE hash="' . pSQL($filterHash) . '"'
|
||||
);
|
||||
|
||||
if (!empty($row)) {
|
||||
return unserialize(current($row));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the filter block into the cache table
|
||||
*
|
||||
* @param string $filterHash
|
||||
* @param array $data
|
||||
*/
|
||||
public function insertIntoCache($filterHash, $data)
|
||||
{
|
||||
if (!Configuration::get('PS_LAYERED_CACHE_ENABLED')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->database->execute(
|
||||
'REPLACE INTO ' . _DB_PREFIX_ . 'layered_filter_block (hash, data) ' .
|
||||
'VALUES ("' . $filterHash . '", "' . pSQL(serialize($data)) . '")'
|
||||
);
|
||||
} catch (PrestaShopDatabaseException $e) {
|
||||
// Don't worry if the cache have invalid or duplicate hash
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $filter
|
||||
* @param array $selectedFilters
|
||||
* @param int $nbProducts
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getPriceRangeBlock($filter, $selectedFilters, $nbProducts)
|
||||
{
|
||||
if (!$this->showPriceFilter()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$priceSpecifications = $this->preparePriceSpecifications();
|
||||
$priceBlock = [
|
||||
'type_lite' => 'price',
|
||||
'type' => 'price',
|
||||
'id_key' => 0,
|
||||
'name' => $this->context->getTranslator()->trans('Price', [], 'Modules.Facetedsearch.Shop'),
|
||||
'max' => '0',
|
||||
'min' => null,
|
||||
'unit' => $this->context->currency->sign,
|
||||
'specifications' => $priceSpecifications,
|
||||
'filter_show_limit' => (int) $filter['filter_show_limit'],
|
||||
'filter_type' => Converter::WIDGET_TYPE_SLIDER,
|
||||
'nbr' => $nbProducts,
|
||||
];
|
||||
|
||||
list($priceMinFilter, $priceMaxFilter, $weightFilter) = $this->ignorePriceAndWeightFilters(
|
||||
$this->searchAdapter->getInitialPopulation()
|
||||
);
|
||||
|
||||
list($priceBlock['min'], $priceBlock['max']) = $this->searchAdapter->getInitialPopulation()->getMinMaxPriceValue();
|
||||
$priceBlock['value'] = !empty($selectedFilters['price']) ? $selectedFilters['price'] : null;
|
||||
|
||||
$this->restorePriceAndWeightFilters(
|
||||
$this->searchAdapter->getInitialPopulation(),
|
||||
$priceMinFilter,
|
||||
$priceMaxFilter,
|
||||
$weightFilter
|
||||
);
|
||||
|
||||
return $priceBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Price / weight filter block should not apply their own filters
|
||||
* otherwise they will always disappear if we filter on price / weight
|
||||
* because only one choice will remain
|
||||
*
|
||||
* @param InterfaceAdapter $filteredSearchAdapter
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function ignorePriceAndWeightFilters(InterfaceAdapter $filteredSearchAdapter)
|
||||
{
|
||||
// disable the current price and weight filters to compute ranges
|
||||
$priceMinFilter = $filteredSearchAdapter->getFilter('price_min');
|
||||
$priceMaxFilter = $filteredSearchAdapter->getFilter('price_max');
|
||||
$weightFilter = $filteredSearchAdapter->getFilter('weight');
|
||||
$filteredSearchAdapter->resetFilter('price_min');
|
||||
$filteredSearchAdapter->resetFilter('price_max');
|
||||
$filteredSearchAdapter->resetFilter('weight');
|
||||
|
||||
return [
|
||||
$priceMinFilter,
|
||||
$priceMaxFilter,
|
||||
$weightFilter,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore price and weight filters
|
||||
*
|
||||
* @param InterfaceAdapter $filteredSearchAdapter
|
||||
* @param int $priceMinFilter
|
||||
* @param int $priceMaxFilter
|
||||
* @param int $weightFilter
|
||||
*/
|
||||
private function restorePriceAndWeightFilters(
|
||||
$filteredSearchAdapter,
|
||||
$priceMinFilter,
|
||||
$priceMaxFilter,
|
||||
$weightFilter
|
||||
) {
|
||||
// put back the price and weight filters
|
||||
$filteredSearchAdapter->setFilter('price_min', $priceMinFilter);
|
||||
$filteredSearchAdapter->setFilter('price_max', $priceMaxFilter);
|
||||
$filteredSearchAdapter->setFilter('weight', $weightFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the weight filter block
|
||||
*
|
||||
* @param array $filter
|
||||
* @param array $selectedFilters
|
||||
* @param int $nbProducts
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getWeightRangeBlock($filter, $selectedFilters, $nbProducts)
|
||||
{
|
||||
$weightBlock = [
|
||||
'type_lite' => 'weight',
|
||||
'type' => 'weight',
|
||||
'id_key' => 0,
|
||||
'name' => $this->context->getTranslator()->trans('Weight', [], 'Modules.Facetedsearch.Shop'),
|
||||
'max' => '0',
|
||||
'min' => null,
|
||||
'unit' => Configuration::get('PS_WEIGHT_UNIT'),
|
||||
'specifications' => null,
|
||||
'filter_show_limit' => (int) $filter['filter_show_limit'],
|
||||
'filter_type' => Converter::WIDGET_TYPE_SLIDER,
|
||||
'value' => null,
|
||||
'nbr' => $nbProducts,
|
||||
];
|
||||
|
||||
list($priceMinFilter, $priceMaxFilter, $weightFilter) = $this->ignorePriceAndWeightFilters(
|
||||
$this->searchAdapter->getInitialPopulation()
|
||||
);
|
||||
|
||||
list($weightBlock['min'], $weightBlock['max']) = $this->searchAdapter->getInitialPopulation()->getMinMaxValue('p.weight');
|
||||
if (empty($weightBlock['min']) && empty($weightBlock['max'])) {
|
||||
// We don't need to continue, no filter available
|
||||
return [];
|
||||
}
|
||||
|
||||
$weightBlock['value'] = !empty($selectedFilters['weight']) ? $selectedFilters['weight'] : null;
|
||||
|
||||
$this->restorePriceAndWeightFilters(
|
||||
$this->searchAdapter->getInitialPopulation(),
|
||||
$priceMinFilter,
|
||||
$priceMaxFilter,
|
||||
$weightFilter
|
||||
);
|
||||
|
||||
return $weightBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the condition filter block
|
||||
*
|
||||
* @param array $filter
|
||||
* @param array $selectedFilters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getConditionsBlock($filter, $selectedFilters)
|
||||
{
|
||||
$conditionArray = [
|
||||
'new' => [
|
||||
'name' => $this->context->getTranslator()->trans(
|
||||
'New',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
),
|
||||
'nbr' => 0,
|
||||
],
|
||||
'used' => [
|
||||
'name' => $this->context->getTranslator()->trans(
|
||||
'Used',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
),
|
||||
'nbr' => 0,
|
||||
],
|
||||
'refurbished' => [
|
||||
'name' => $this->context->getTranslator()->trans(
|
||||
'Refurbished',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
),
|
||||
'nbr' => 0,
|
||||
],
|
||||
];
|
||||
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('condition');
|
||||
$results = $filteredSearchAdapter->valueCount('condition');
|
||||
foreach ($results as $key => $values) {
|
||||
$condition = $values['condition'];
|
||||
$count = $values['c'];
|
||||
|
||||
$conditionArray[$condition]['nbr'] = $count;
|
||||
if (isset($selectedFilters['condition'])
|
||||
&& in_array($condition, $selectedFilters['condition'])
|
||||
) {
|
||||
$conditionArray[$condition]['checked'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$conditionBlock = [
|
||||
'type_lite' => 'condition',
|
||||
'type' => 'condition',
|
||||
'id_key' => 0,
|
||||
'name' => $this->context->getTranslator()->trans('Condition', [], 'Modules.Facetedsearch.Shop'),
|
||||
'values' => $conditionArray,
|
||||
'filter_show_limit' => (int) $filter['filter_show_limit'],
|
||||
'filter_type' => $filter['filter_type'],
|
||||
];
|
||||
|
||||
return $conditionBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the quantities filter block
|
||||
*
|
||||
* @param array $filter
|
||||
* @param array $selectedFilters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getQuantitiesBlock($filter, $selectedFilters)
|
||||
{
|
||||
if ($this->psStockManagement === null) {
|
||||
$this->psStockManagement = (bool) Configuration::get('PS_STOCK_MANAGEMENT');
|
||||
}
|
||||
|
||||
if ($this->psOrderOutOfStock === null) {
|
||||
$this->psOrderOutOfStock = (bool) Configuration::get('PS_ORDER_OUT_OF_STOCK');
|
||||
}
|
||||
|
||||
// We only initialize the options if stock management is activated
|
||||
$availabilityOptions = [];
|
||||
if ($this->psStockManagement) {
|
||||
$availabilityOptions = [
|
||||
0 => [
|
||||
'name' => $this->context->getTranslator()->trans(
|
||||
'Not available',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
),
|
||||
'nbr' => 0,
|
||||
],
|
||||
1 => [
|
||||
'name' => $this->context->getTranslator()->trans(
|
||||
'Available',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
),
|
||||
'nbr' => 0,
|
||||
],
|
||||
2 => [
|
||||
'name' => $this->context->getTranslator()->trans(
|
||||
'In stock',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
),
|
||||
'nbr' => 0,
|
||||
],
|
||||
];
|
||||
|
||||
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter(Search::STOCK_MANAGEMENT_FILTER);
|
||||
|
||||
// Products without quantity in stock, with out-of-stock ordering disabled
|
||||
$filteredSearchAdapter->addOperationsFilter(
|
||||
Search::STOCK_MANAGEMENT_FILTER,
|
||||
[
|
||||
[
|
||||
['quantity', [0], '<='],
|
||||
['out_of_stock', !$this->psOrderOutOfStock ? [0, 2] : [0], '='],
|
||||
],
|
||||
]
|
||||
);
|
||||
$availabilityOptions[0]['nbr'] = $filteredSearchAdapter->count();
|
||||
|
||||
// Products in stock, or with out-of-stock ordering enabled
|
||||
$filteredSearchAdapter->addOperationsFilter(
|
||||
Search::STOCK_MANAGEMENT_FILTER,
|
||||
[
|
||||
[
|
||||
['out_of_stock', $this->psOrderOutOfStock ? [1, 2] : [1], '='],
|
||||
],
|
||||
[
|
||||
['quantity', [0], '>'],
|
||||
],
|
||||
]
|
||||
);
|
||||
$availabilityOptions[1]['nbr'] = $filteredSearchAdapter->count();
|
||||
|
||||
// Products in stock
|
||||
$filteredSearchAdapter->addOperationsFilter(
|
||||
Search::STOCK_MANAGEMENT_FILTER,
|
||||
[
|
||||
[
|
||||
['quantity', [0], '>'],
|
||||
],
|
||||
]
|
||||
);
|
||||
$availabilityOptions[2]['nbr'] = $filteredSearchAdapter->count();
|
||||
|
||||
// If some filter was selected, we want to show only this single filter, it does not make sense to show others
|
||||
if (isset($selectedFilters['quantity'])) {
|
||||
// We loop through selected filters and assign it to our options and remove the rest
|
||||
foreach ($availabilityOptions as $key => $values) {
|
||||
if (in_array($key, $selectedFilters['quantity'], true)) {
|
||||
$availabilityOptions[$key]['checked'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$quantityBlock = [
|
||||
'type_lite' => 'quantity',
|
||||
'type' => 'quantity',
|
||||
'id_key' => 0,
|
||||
'name' => $this->context->getTranslator()->trans('Availability', [], 'Modules.Facetedsearch.Shop'),
|
||||
'values' => $availabilityOptions,
|
||||
'filter_show_limit' => (int) $filter['filter_show_limit'],
|
||||
'filter_type' => $filter['filter_type'],
|
||||
];
|
||||
|
||||
return $quantityBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the manufacturers filter block
|
||||
*
|
||||
* @param array $filter
|
||||
* @param array $selectedFilters
|
||||
* @param int $idLang
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getManufacturersBlock($filter, $selectedFilters, $idLang)
|
||||
{
|
||||
$manufacturersArray = $manufacturers = [];
|
||||
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('id_manufacturer');
|
||||
|
||||
$tempManufacturers = Manufacturer::getManufacturers(false, $idLang);
|
||||
if (empty($tempManufacturers)) {
|
||||
return $manufacturersArray;
|
||||
}
|
||||
|
||||
foreach ($tempManufacturers as $key => $manufacturer) {
|
||||
$manufacturers[$manufacturer['id_manufacturer']] = $manufacturer;
|
||||
}
|
||||
|
||||
$results = $filteredSearchAdapter->valueCount('id_manufacturer');
|
||||
foreach ($results as $key => $values) {
|
||||
if (!isset($values['id_manufacturer'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id_manufacturer = $values['id_manufacturer'];
|
||||
if (empty($manufacturers[$id_manufacturer]['name'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = $values['c'];
|
||||
$manufacturersArray[$id_manufacturer] = [
|
||||
'name' => $manufacturers[$id_manufacturer]['name'],
|
||||
'nbr' => $count,
|
||||
];
|
||||
|
||||
if (isset($selectedFilters['manufacturer'])
|
||||
&& in_array($id_manufacturer, $selectedFilters['manufacturer'])
|
||||
) {
|
||||
$manufacturersArray[$id_manufacturer]['checked'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$manufacturerBlock = [
|
||||
'type_lite' => 'manufacturer',
|
||||
'type' => 'manufacturer',
|
||||
'id_key' => 0,
|
||||
'name' => $this->context->getTranslator()->trans('Brand', [], 'Modules.Facetedsearch.Shop'),
|
||||
'values' => $manufacturersArray,
|
||||
'filter_show_limit' => (int) $filter['filter_show_limit'],
|
||||
'filter_type' => $filter['filter_type'],
|
||||
];
|
||||
|
||||
return $manufacturerBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the attributes filter block
|
||||
*
|
||||
* @param array $filter
|
||||
* @param array $selectedFilters
|
||||
* @param int $idLang
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getAttributesBlock($filter, $selectedFilters, $idLang)
|
||||
{
|
||||
$attributesBlock = [];
|
||||
$filteredSearchAdapter = null;
|
||||
$idAttributeGroup = $filter['id_value'];
|
||||
|
||||
if (!empty($selectedFilters['id_attribute_group'])) {
|
||||
foreach ($selectedFilters['id_attribute_group'] as $key => $selectedFilter) {
|
||||
if ($key == $idAttributeGroup) {
|
||||
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('with_attributes_' . $idAttributeGroup);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$filteredSearchAdapter) {
|
||||
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter();
|
||||
}
|
||||
|
||||
$attributesGroup = $this->dataAccessor->getAttributesGroups($idLang);
|
||||
if ($attributesGroup === []) {
|
||||
return $attributesBlock;
|
||||
}
|
||||
|
||||
$attributes = $this->dataAccessor->getAttributes($idLang, $idAttributeGroup);
|
||||
|
||||
$filteredSearchAdapter->addOperationsFilter(
|
||||
'id_attribute_group_' . $idAttributeGroup,
|
||||
[[['id_attribute_group', [(int) $idAttributeGroup]]]]
|
||||
);
|
||||
$results = $filteredSearchAdapter->valueCount('id_attribute');
|
||||
foreach ($results as $key => $values) {
|
||||
$idAttribute = $values['id_attribute'];
|
||||
if (!isset($attributes[$idAttribute])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = $values['c'];
|
||||
$attribute = $attributes[$idAttribute];
|
||||
$idAttributeGroup = $attribute['id_attribute_group'];
|
||||
if (!isset($attributesBlock[$idAttributeGroup])) {
|
||||
$attributeGroup = $attributesGroup[$idAttributeGroup];
|
||||
|
||||
$attributesBlock[$idAttributeGroup] = [
|
||||
'type_lite' => 'id_attribute_group',
|
||||
'type' => 'id_attribute_group',
|
||||
'id_key' => $idAttributeGroup,
|
||||
'name' => $attributeGroup['attribute_group_name'],
|
||||
'is_color_group' => (bool) $attributeGroup['is_color_group'],
|
||||
'values' => [],
|
||||
'url_name' => $attributeGroup['url_name'],
|
||||
'meta_title' => $attributeGroup['meta_title'],
|
||||
'filter_show_limit' => (int) $filter['filter_show_limit'],
|
||||
'filter_type' => $filter['filter_type'],
|
||||
];
|
||||
}
|
||||
|
||||
$attributesBlock[$idAttributeGroup]['values'][$idAttribute] = [
|
||||
'name' => $attribute['name'],
|
||||
'nbr' => $count,
|
||||
'url_name' => $attribute['url_name'],
|
||||
'meta_title' => $attribute['meta_title'],
|
||||
];
|
||||
|
||||
if ($attributesBlock[$idAttributeGroup]['is_color_group'] !== false) {
|
||||
$attributesBlock[$idAttributeGroup]['values'][$idAttribute]['color'] = $attribute['color'];
|
||||
}
|
||||
|
||||
if (array_key_exists('id_attribute_group', $selectedFilters)) {
|
||||
foreach ($selectedFilters['id_attribute_group'] as $selectedAttribute) {
|
||||
if (in_array($idAttribute, $selectedAttribute)) {
|
||||
$attributesBlock[$idAttributeGroup]['values'][$idAttribute]['checked'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($attributesBlock as $idAttributeGroup => $value) {
|
||||
$attributesBlock[$idAttributeGroup]['values'] = $this->sortByKey($attributes, $value['values']);
|
||||
}
|
||||
|
||||
$attributesBlock = $this->sortByKey($attributesGroup, $attributesBlock);
|
||||
|
||||
return $attributesBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort an array using the same key order than the sortedReferenceArray
|
||||
*
|
||||
* @param array $sortedReferenceArray
|
||||
* @param array $array
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function sortByKey(array $sortedReferenceArray, $array)
|
||||
{
|
||||
$sortedArray = [];
|
||||
|
||||
// iterate in the original order
|
||||
foreach ($sortedReferenceArray as $key => $value) {
|
||||
if (array_key_exists($key, $array)) {
|
||||
$sortedArray[$key] = $array[$key];
|
||||
}
|
||||
}
|
||||
|
||||
return $sortedArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the features filter block
|
||||
*
|
||||
* @param array $filter
|
||||
* @param array $selectedFilters
|
||||
* @param int $idLang
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getFeaturesBlock($filter, $selectedFilters, $idLang)
|
||||
{
|
||||
$features = $featureBlock = [];
|
||||
$idFeature = $filter['id_value'];
|
||||
$filteredSearchAdapter = null;
|
||||
|
||||
if (!empty($selectedFilters['id_feature'])) {
|
||||
foreach ($selectedFilters['id_feature'] as $key => $selectedFilter) {
|
||||
if ($key == $idFeature) {
|
||||
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('with_features_' . $idFeature);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$filteredSearchAdapter) {
|
||||
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter();
|
||||
}
|
||||
|
||||
$tempFeatures = $this->dataAccessor->getFeatures($idLang);
|
||||
if (empty($tempFeatures)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($tempFeatures as $key => $feature) {
|
||||
$features[$feature['id_feature']] = $feature;
|
||||
}
|
||||
|
||||
$filteredSearchAdapter->addOperationsFilter(
|
||||
'id_feature_' . $idFeature,
|
||||
[[['id_feature', [(int) $idFeature]]]]
|
||||
);
|
||||
|
||||
$filteredSearchAdapter->addSelectField('id_feature');
|
||||
$results = $filteredSearchAdapter->valueCount('id_feature_value');
|
||||
foreach ($results as $key => $values) {
|
||||
$idFeatureValue = $values['id_feature_value'];
|
||||
$idFeature = $values['id_feature'];
|
||||
$count = $values['c'];
|
||||
|
||||
$feature = $features[$idFeature];
|
||||
|
||||
if (!isset($featureBlock[$idFeature])) {
|
||||
$tempFeatureValues = $this->dataAccessor->getFeatureValues($idFeature, $idLang);
|
||||
foreach ($tempFeatureValues as $featureValueKey => $featureValue) {
|
||||
$features[$idFeature]['featureValues'][$featureValue['id_feature_value']] = $featureValue;
|
||||
}
|
||||
|
||||
$featureBlock[$idFeature] = [
|
||||
'type_lite' => 'id_feature',
|
||||
'type' => 'id_feature',
|
||||
'id_key' => $idFeature,
|
||||
'values' => [],
|
||||
'name' => $feature['name'],
|
||||
'url_name' => $feature['url_name'],
|
||||
'meta_title' => $feature['meta_title'],
|
||||
'filter_show_limit' => (int) $filter['filter_show_limit'],
|
||||
'filter_type' => $filter['filter_type'],
|
||||
];
|
||||
}
|
||||
|
||||
$featureValues = $features[$idFeature]['featureValues'];
|
||||
if (!isset($featureValues[$idFeatureValue]['value'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$featureBlock[$idFeature]['values'][$idFeatureValue] = [
|
||||
'nbr' => $count,
|
||||
'name' => $featureValues[$idFeatureValue]['value'],
|
||||
'url_name' => $featureValues[$idFeatureValue]['url_name'],
|
||||
'meta_title' => $featureValues[$idFeatureValue]['meta_title'],
|
||||
];
|
||||
|
||||
if (array_key_exists('id_feature', $selectedFilters)) {
|
||||
foreach ($selectedFilters['id_feature'] as $selectedFeature) {
|
||||
if (in_array($idFeatureValue, $selectedFeature)) {
|
||||
$featureBlock[$feature['id_feature']]['values'][$idFeatureValue]['checked'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$featureBlock = $this->sortFeatureBlock($featureBlock);
|
||||
|
||||
return $featureBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Natural sort multi-dimensional feature array
|
||||
*
|
||||
* @param array $featureBlock
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function sortFeatureBlock($featureBlock)
|
||||
{
|
||||
//Natural sort
|
||||
foreach ($featureBlock as $key => $value) {
|
||||
$temp = [];
|
||||
foreach ($featureBlock[$key]['values'] as $idFeatureValue => $featureValueInfos) {
|
||||
$temp[$idFeatureValue] = $featureValueInfos['name'];
|
||||
}
|
||||
|
||||
natcasesort($temp);
|
||||
$temp2 = [];
|
||||
|
||||
foreach ($temp as $keytemp => $valuetemp) {
|
||||
$temp2[$keytemp] = $featureBlock[$key]['values'][$keytemp];
|
||||
}
|
||||
|
||||
$featureBlock[$key]['values'] = $temp2;
|
||||
}
|
||||
|
||||
return $featureBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the categories filter condition based on the parent and config variables
|
||||
*
|
||||
* @param InterfaceAdapter $filteredSearchAdapter
|
||||
* @param Category $parent
|
||||
*/
|
||||
private function addCategoriesBlockFilters(InterfaceAdapter $filteredSearchAdapter, $parent)
|
||||
{
|
||||
if (Group::isFeatureActive()) {
|
||||
$userGroups = ($this->context->customer->isLogged() ? $this->context->customer->getGroups() : [
|
||||
Configuration::get(
|
||||
'PS_UNIDENTIFIED_GROUP'
|
||||
),
|
||||
]);
|
||||
|
||||
$filteredSearchAdapter->addFilter('id_group', $userGroups);
|
||||
}
|
||||
|
||||
$depth = (int) Configuration::get('PS_LAYERED_FILTER_CATEGORY_DEPTH', null, null, null, 1);
|
||||
|
||||
if ($depth) {
|
||||
$levelDepth = $parent->level_depth;
|
||||
$filteredSearchAdapter->addFilter('level_depth', [$depth + $levelDepth], '<=');
|
||||
}
|
||||
|
||||
$filteredSearchAdapter->addFilter('nleft', [$parent->nleft], '>');
|
||||
$filteredSearchAdapter->addFilter('nright', [$parent->nright], '<');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the categories filter block
|
||||
*
|
||||
* @param array $filter
|
||||
* @param array $selectedFilters
|
||||
* @param int $idLang
|
||||
* @param Category $parent
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getCategoriesBlock($filter, $selectedFilters, $idLang, $parent)
|
||||
{
|
||||
$filteredSearchAdapter = $this->searchAdapter->getFilteredSearchAdapter('id_category');
|
||||
$this->addCategoriesBlockFilters($filteredSearchAdapter, $parent);
|
||||
|
||||
$categoryArray = [];
|
||||
$categories = Category::getAllCategoriesName(
|
||||
null,
|
||||
$idLang,
|
||||
true,
|
||||
null,
|
||||
true,
|
||||
'',
|
||||
'ORDER BY c.nleft, c.position'
|
||||
);
|
||||
foreach ($categories as $key => $value) {
|
||||
$categories[$value['id_category']] = $value;
|
||||
}
|
||||
|
||||
$results = $filteredSearchAdapter->valueCount('id_category');
|
||||
|
||||
foreach ($results as $key => $values) {
|
||||
$idCategory = $values['id_category'];
|
||||
if (!isset($categories[$idCategory])) {
|
||||
// Category can sometimes not be found in case of multistore
|
||||
// plus waiting for indexation
|
||||
continue;
|
||||
}
|
||||
|
||||
$count = $values['c'];
|
||||
$categoryArray[$idCategory] = [
|
||||
'name' => $categories[$idCategory]['name'],
|
||||
'nbr' => $count,
|
||||
];
|
||||
|
||||
if (isset($selectedFilters['category']) && in_array($idCategory, $selectedFilters['category'])) {
|
||||
$categoryArray[$idCategory]['checked'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$categoryBlock = [
|
||||
'type_lite' => 'category',
|
||||
'type' => 'category',
|
||||
'id_key' => 0,
|
||||
'name' => $this->context->getTranslator()->trans('Categories', [], 'Modules.Facetedsearch.Shop'),
|
||||
'values' => $categoryArray,
|
||||
'filter_show_limit' => (int) $filter['filter_show_limit'],
|
||||
'filter_type' => $filter['filter_type'],
|
||||
];
|
||||
|
||||
return $categoryBlock;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare price specifications to display cldr prices.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function preparePriceSpecifications()
|
||||
{
|
||||
/* @var Currency */
|
||||
$currency = $this->context->currency;
|
||||
// New method since PS 1.7.6
|
||||
if (isset($this->context->currentLocale) && method_exists($this->context->currentLocale, 'getPriceSpecification')) {
|
||||
/* @var PriceSpecification */
|
||||
$priceSpecification = $this->context->currentLocale->getPriceSpecification($currency->iso_code);
|
||||
/* @var NumberSymbolList */
|
||||
$symbolList = $priceSpecification->getSymbolsByNumberingSystem(Locale::NUMBERING_SYSTEM_LATIN);
|
||||
|
||||
$symbol = [
|
||||
$symbolList->getDecimal(),
|
||||
$symbolList->getGroup(),
|
||||
$symbolList->getList(),
|
||||
$symbolList->getPercentSign(),
|
||||
$symbolList->getMinusSign(),
|
||||
$symbolList->getPlusSign(),
|
||||
$symbolList->getExponential(),
|
||||
$symbolList->getSuperscriptingExponent(),
|
||||
$symbolList->getPerMille(),
|
||||
$symbolList->getInfinity(),
|
||||
$symbolList->getNaN(),
|
||||
];
|
||||
|
||||
return array_merge(
|
||||
['symbol' => $symbol],
|
||||
$priceSpecification->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
// Default symbol configuration
|
||||
$symbol = [
|
||||
'.',
|
||||
',',
|
||||
';',
|
||||
'%',
|
||||
'-',
|
||||
'+',
|
||||
'E',
|
||||
'×',
|
||||
'‰',
|
||||
'∞',
|
||||
'NaN',
|
||||
];
|
||||
// The property `$precision` exists only from PS 1.7.6. On previous versions, all prices have 2 decimals
|
||||
$precision = isset($currency->precision) ? $currency->precision : 2;
|
||||
$formats = explode(';', $currency->format);
|
||||
if (count($formats) > 1) {
|
||||
$positivePattern = $formats[0];
|
||||
$negativePattern = $formats[1];
|
||||
} else {
|
||||
$positivePattern = $currency->format;
|
||||
$negativePattern = $currency->format;
|
||||
}
|
||||
|
||||
return [
|
||||
'positivePattern' => $positivePattern,
|
||||
'negativePattern' => $negativePattern,
|
||||
'symbol' => $symbol,
|
||||
'maxFractionDigits' => $precision,
|
||||
'minFractionDigits' => $precision,
|
||||
'groupingUsed' => true,
|
||||
'primaryGroupSize' => 3,
|
||||
'secondaryGroupSize' => 3,
|
||||
'currencyCode' => $currency->iso_code,
|
||||
'currencySymbol' => $currency->sign,
|
||||
];
|
||||
}
|
||||
}
|
||||
517
modules/ps_facetedsearch/src/Filters/Converter.php
Normal file
517
modules/ps_facetedsearch/src/Filters/Converter.php
Normal file
@@ -0,0 +1,517 @@
|
||||
<?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\Filters;
|
||||
|
||||
use Category;
|
||||
use Configuration;
|
||||
use Context;
|
||||
use Db;
|
||||
use Manufacturer;
|
||||
use PrestaShop\Module\FacetedSearch\Filters;
|
||||
use PrestaShop\Module\FacetedSearch\URLSerializer;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
|
||||
use Tools;
|
||||
|
||||
class Converter
|
||||
{
|
||||
const WIDGET_TYPE_CHECKBOX = 0;
|
||||
const WIDGET_TYPE_RADIO = 1;
|
||||
const WIDGET_TYPE_DROPDOWN = 2;
|
||||
const WIDGET_TYPE_SLIDER = 3;
|
||||
|
||||
const TYPE_ATTRIBUTE_GROUP = 'id_attribute_group';
|
||||
const TYPE_AVAILABILITY = 'availability';
|
||||
const TYPE_CATEGORY = 'category';
|
||||
const TYPE_CONDITION = 'condition';
|
||||
const TYPE_FEATURE = 'id_feature';
|
||||
const TYPE_QUANTITY = 'quantity';
|
||||
const TYPE_MANUFACTURER = 'manufacturer';
|
||||
const TYPE_PRICE = 'price';
|
||||
const TYPE_WEIGHT = 'weight';
|
||||
|
||||
const PROPERTY_URL_NAME = 'url_name';
|
||||
const PROPERTY_COLOR = 'color';
|
||||
const PROPERTY_TEXTURE = 'texture';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
const RANGE_FILTERS = [self::TYPE_PRICE, self::TYPE_WEIGHT];
|
||||
|
||||
/**
|
||||
* @var Context
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* @var Db
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
/**
|
||||
* @var URLSerializer
|
||||
*/
|
||||
protected $urlSerializer;
|
||||
|
||||
/**
|
||||
* @var Filters\DataAccessor
|
||||
*/
|
||||
private $dataAccessor;
|
||||
|
||||
public function __construct(
|
||||
Context $context,
|
||||
Db $database,
|
||||
URLSerializer $urlSerializer,
|
||||
Filters\DataAccessor $dataAccessor
|
||||
) {
|
||||
$this->context = $context;
|
||||
$this->database = $database;
|
||||
$this->urlSerializer = $urlSerializer;
|
||||
$this->dataAccessor = $dataAccessor;
|
||||
}
|
||||
|
||||
public function getFacetsFromFilterBlocks(array $filterBlocks)
|
||||
{
|
||||
$facets = [];
|
||||
|
||||
foreach ($filterBlocks as $filterBlock) {
|
||||
if (empty($filterBlock)) {
|
||||
// Empty filter, let's continue
|
||||
continue;
|
||||
}
|
||||
|
||||
$facet = new Facet();
|
||||
$facet
|
||||
->setLabel($filterBlock['name'])
|
||||
->setProperty('filter_show_limit', $filterBlock['filter_show_limit'])
|
||||
->setMultipleSelectionAllowed(true);
|
||||
|
||||
switch ($filterBlock['type']) {
|
||||
case self::TYPE_CATEGORY:
|
||||
case self::TYPE_CONDITION:
|
||||
case self::TYPE_MANUFACTURER:
|
||||
case self::TYPE_QUANTITY:
|
||||
case self::TYPE_ATTRIBUTE_GROUP:
|
||||
case self::TYPE_FEATURE:
|
||||
$type = $filterBlock['type'];
|
||||
if ($filterBlock['type'] === self::TYPE_QUANTITY) {
|
||||
$type = 'availability';
|
||||
} elseif ($filterBlock['type'] == self::TYPE_ATTRIBUTE_GROUP) {
|
||||
$type = 'attribute_group';
|
||||
$facet->setProperty(self::TYPE_ATTRIBUTE_GROUP, $filterBlock['id_key']);
|
||||
if (isset($filterBlock['url_name'])) {
|
||||
$facet->setProperty(self::PROPERTY_URL_NAME, $filterBlock['url_name']);
|
||||
}
|
||||
} elseif ($filterBlock['type'] == self::TYPE_FEATURE) {
|
||||
$type = 'feature';
|
||||
$facet->setProperty(self::TYPE_FEATURE, $filterBlock['id_key']);
|
||||
if (isset($filterBlock['url_name'])) {
|
||||
$facet->setProperty(self::PROPERTY_URL_NAME, $filterBlock['url_name']);
|
||||
}
|
||||
}
|
||||
|
||||
$facet->setType($type);
|
||||
$filters = [];
|
||||
foreach ($filterBlock['values'] as $id => $filterArray) {
|
||||
$filter = new Filter();
|
||||
$filter
|
||||
->setType($type)
|
||||
->setLabel($filterArray['name'])
|
||||
->setMagnitude($filterArray['nbr'])
|
||||
->setValue($id);
|
||||
|
||||
if (isset($filterArray['url_name'])) {
|
||||
$filter->setProperty(self::PROPERTY_URL_NAME, $filterArray['url_name']);
|
||||
}
|
||||
|
||||
if (array_key_exists('checked', $filterArray)) {
|
||||
$filter->setActive($filterArray['checked']);
|
||||
}
|
||||
|
||||
if (isset($filterArray['color'])) {
|
||||
if ($filterArray['color'] != '') {
|
||||
$filter->setProperty(self::PROPERTY_COLOR, $filterArray['color']);
|
||||
} elseif (file_exists(_PS_COL_IMG_DIR_ . $id . '.jpg')) {
|
||||
$filter->setProperty(self::PROPERTY_TEXTURE, _THEME_COL_DIR_ . $id . '.jpg');
|
||||
}
|
||||
}
|
||||
|
||||
$filters[] = $filter;
|
||||
}
|
||||
|
||||
if ((int) $filterBlock['filter_show_limit'] !== 0) {
|
||||
usort($filters, [$this, 'sortFiltersByMagnitude']);
|
||||
}
|
||||
|
||||
$this->hideZeroValuesAndShowLimit($filters, (int) $filterBlock['filter_show_limit']);
|
||||
|
||||
if ((int) $filterBlock['filter_show_limit'] !== 0 || $filterBlock['type'] !== self::TYPE_ATTRIBUTE_GROUP) {
|
||||
usort($filters, [$this, 'sortFiltersByLabel']);
|
||||
}
|
||||
|
||||
// No method available to add all filters
|
||||
foreach ($filters as $filter) {
|
||||
$facet->addFilter($filter);
|
||||
}
|
||||
break;
|
||||
case self::TYPE_WEIGHT:
|
||||
case self::TYPE_PRICE:
|
||||
$facet
|
||||
->setType($filterBlock['type'])
|
||||
->setProperty('min', $filterBlock['min'])
|
||||
->setProperty('max', $filterBlock['max'])
|
||||
->setProperty('unit', $filterBlock['unit'])
|
||||
->setProperty('specifications', $filterBlock['specifications'])
|
||||
->setMultipleSelectionAllowed(false)
|
||||
->setProperty('range', true);
|
||||
|
||||
$filter = new Filter();
|
||||
$filter
|
||||
->setActive($filterBlock['value'] !== null)
|
||||
->setType($filterBlock['type'])
|
||||
->setMagnitude($filterBlock['nbr'])
|
||||
->setProperty('symbol', $filterBlock['unit'])
|
||||
->setValue($filterBlock['value']);
|
||||
|
||||
$facet->addFilter($filter);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
switch ((int) $filterBlock['filter_type']) {
|
||||
case self::WIDGET_TYPE_CHECKBOX:
|
||||
$facet->setMultipleSelectionAllowed(true);
|
||||
$facet->setWidgetType('checkbox');
|
||||
break;
|
||||
case self::WIDGET_TYPE_RADIO:
|
||||
$facet->setMultipleSelectionAllowed(false);
|
||||
$facet->setWidgetType('radio');
|
||||
break;
|
||||
case self::WIDGET_TYPE_DROPDOWN:
|
||||
$facet->setMultipleSelectionAllowed(false);
|
||||
$facet->setWidgetType('dropdown');
|
||||
break;
|
||||
case self::WIDGET_TYPE_SLIDER:
|
||||
$facet->setMultipleSelectionAllowed(false);
|
||||
$facet->setWidgetType('slider');
|
||||
break;
|
||||
}
|
||||
|
||||
$facets[] = $facet;
|
||||
}
|
||||
|
||||
return $facets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProductSearchQuery $query
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function createFacetedSearchFiltersFromQuery(ProductSearchQuery $query)
|
||||
{
|
||||
$idShop = (int) $this->context->shop->id;
|
||||
$idLang = (int) $this->context->language->id;
|
||||
|
||||
$idParent = $query->getIdCategory();
|
||||
if (empty($idParent)) {
|
||||
$idParent = (int) Tools::getValue('id_category_layered', Configuration::get('PS_HOME_CATEGORY'));
|
||||
}
|
||||
|
||||
$searchFilters = [];
|
||||
|
||||
/* Get the filters for the current category */
|
||||
$filters = $this->database->executeS(
|
||||
'SELECT type, id_value, filter_show_limit, filter_type FROM ' . _DB_PREFIX_ . 'layered_category
|
||||
WHERE id_category = ' . (int) $idParent . '
|
||||
AND id_shop = ' . (int) $idShop . '
|
||||
GROUP BY `type`, id_value ORDER BY position ASC'
|
||||
);
|
||||
|
||||
$facetAndFiltersLabels = $this->urlSerializer->unserialize($query->getEncodedFacets());
|
||||
foreach ($filters as $filter) {
|
||||
$filterLabel = $this->convertFilterTypeToLabel($filter['type']);
|
||||
|
||||
switch ($filter['type']) {
|
||||
case self::TYPE_MANUFACTURER:
|
||||
if (!isset($facetAndFiltersLabels[$filterLabel])) {
|
||||
// No need to filter if no information
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$manufacturers = Manufacturer::getManufacturers(false, $idLang);
|
||||
$searchFilters[$filter['type']] = [];
|
||||
foreach ($manufacturers as $manufacturer) {
|
||||
if (in_array($manufacturer['name'], $facetAndFiltersLabels[$filterLabel])) {
|
||||
$searchFilters[$filter['type']][$manufacturer['name']] = $manufacturer['id_manufacturer'];
|
||||
}
|
||||
}
|
||||
break;
|
||||
case self::TYPE_QUANTITY:
|
||||
if (!isset($facetAndFiltersLabels[$filterLabel])) {
|
||||
// No need to filter if no information
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$quantityArray = [
|
||||
$this->context->getTranslator()->trans(
|
||||
'Not available',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
) => 0,
|
||||
$this->context->getTranslator()->trans(
|
||||
'Available',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
) => 1,
|
||||
$this->context->getTranslator()->trans(
|
||||
'In stock',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
) => 2,
|
||||
];
|
||||
|
||||
$searchFilters[$filter['type']] = [];
|
||||
foreach ($quantityArray as $quantityName => $quantityId) {
|
||||
if (isset($facetAndFiltersLabels[$filterLabel]) && in_array($quantityName, $facetAndFiltersLabels[$filterLabel])) {
|
||||
$searchFilters[$filter['type']][] = $quantityId;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case self::TYPE_CONDITION:
|
||||
if (!isset($facetAndFiltersLabels[$filterLabel])) {
|
||||
// No need to filter if no information
|
||||
continue 2;
|
||||
}
|
||||
|
||||
$conditionArray = [
|
||||
$this->context->getTranslator()->trans(
|
||||
'New',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
) => 'new',
|
||||
$this->context->getTranslator()->trans(
|
||||
'Used',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
) => 'used',
|
||||
$this->context->getTranslator()->trans(
|
||||
'Refurbished',
|
||||
[],
|
||||
'Modules.Facetedsearch.Shop'
|
||||
) => 'refurbished',
|
||||
];
|
||||
|
||||
$searchFilters[$filter['type']] = [];
|
||||
foreach ($conditionArray as $conditionName => $conditionId) {
|
||||
if (isset($facetAndFiltersLabels[$filterLabel]) && in_array($conditionName, $facetAndFiltersLabels[$filterLabel])) {
|
||||
$searchFilters[$filter['type']][] = $conditionId;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case self::TYPE_FEATURE:
|
||||
$features = $this->dataAccessor->getFeatures($idLang);
|
||||
foreach ($features as $feature) {
|
||||
if ($filter['id_value'] != $feature['id_feature']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($facetAndFiltersLabels[$feature['url_name']])) {
|
||||
$featureValueLabels = $facetAndFiltersLabels[$feature['url_name']];
|
||||
} elseif (isset($facetAndFiltersLabels[$feature['name']])) {
|
||||
$featureValueLabels = $facetAndFiltersLabels[$feature['name']];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$featureValues = $this->dataAccessor->getFeatureValues($feature['id_feature'], $idLang);
|
||||
foreach ($featureValues as $featureValue) {
|
||||
if (in_array($featureValue['url_name'], $featureValueLabels)
|
||||
|| in_array($featureValue['value'], $featureValueLabels)
|
||||
) {
|
||||
$searchFilters['id_feature'][$feature['id_feature']][] = $featureValue['id_feature_value'];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case self::TYPE_ATTRIBUTE_GROUP:
|
||||
$attributesGroup = $this->dataAccessor->getAttributesGroups($idLang);
|
||||
foreach ($attributesGroup as $attributeGroup) {
|
||||
if ($filter['id_value'] != $attributeGroup['id_attribute_group']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($facetAndFiltersLabels[$attributeGroup['url_name']])) {
|
||||
$attributeLabels = $facetAndFiltersLabels[$attributeGroup['url_name']];
|
||||
} elseif (isset($facetAndFiltersLabels[$attributeGroup['attribute_group_name']])) {
|
||||
$attributeLabels = $facetAndFiltersLabels[$attributeGroup['attribute_group_name']];
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributes = $this->dataAccessor->getAttributes($idLang, $attributeGroup['id_attribute_group']);
|
||||
foreach ($attributes as $attribute) {
|
||||
if (in_array($attribute['url_name'], $attributeLabels)
|
||||
|| in_array($attribute['name'], $attributeLabels)
|
||||
) {
|
||||
$searchFilters['id_attribute_group'][$attributeGroup['id_attribute_group']][] = $attribute['id_attribute'];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case self::TYPE_PRICE:
|
||||
case self::TYPE_WEIGHT:
|
||||
if (isset($facetAndFiltersLabels[$filterLabel])) {
|
||||
$filters = $facetAndFiltersLabels[$filterLabel];
|
||||
if (isset($filters[1]) && isset($filters[2])) {
|
||||
$from = $filters[1];
|
||||
$to = $filters[2];
|
||||
$searchFilters[$filter['type']][0] = $from;
|
||||
$searchFilters[$filter['type']][1] = $to;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case self::TYPE_CATEGORY:
|
||||
if (isset($facetAndFiltersLabels[$filterLabel])) {
|
||||
foreach ($facetAndFiltersLabels[$filterLabel] as $queryFilter) {
|
||||
$categories = Category::searchByNameAndParentCategoryId($idLang, $queryFilter, (int) $query->getIdCategory());
|
||||
if ($categories) {
|
||||
$searchFilters[$filter['type']][] = $categories['id_category'];
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (isset($facetAndFiltersLabels[$filterLabel])) {
|
||||
foreach ($facetAndFiltersLabels[$filterLabel] as $queryFilter) {
|
||||
$searchFilters[$filter['type']][] = $queryFilter;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove all empty selected filters
|
||||
foreach ($searchFilters as $key => $value) {
|
||||
switch ($key) {
|
||||
case self::TYPE_PRICE:
|
||||
case self::TYPE_WEIGHT:
|
||||
if ($value[0] === '' && $value[1] === '') {
|
||||
unset($searchFilters[$key]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if ($value == '' || $value == []) {
|
||||
unset($searchFilters[$key]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $searchFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert filter type to label
|
||||
*
|
||||
* @param string $filterType
|
||||
*/
|
||||
private function convertFilterTypeToLabel($filterType)
|
||||
{
|
||||
switch ($filterType) {
|
||||
case self::TYPE_PRICE:
|
||||
return $this->context->getTranslator()->trans('Price', [], 'Modules.Facetedsearch.Shop');
|
||||
case self::TYPE_WEIGHT:
|
||||
return $this->context->getTranslator()->trans('Weight', [], 'Modules.Facetedsearch.Shop');
|
||||
case self::TYPE_CONDITION:
|
||||
return $this->context->getTranslator()->trans('Condition', [], 'Modules.Facetedsearch.Shop');
|
||||
case self::TYPE_QUANTITY:
|
||||
return $this->context->getTranslator()->trans('Availability', [], 'Modules.Facetedsearch.Shop');
|
||||
case self::TYPE_MANUFACTURER:
|
||||
return $this->context->getTranslator()->trans('Brand', [], 'Modules.Facetedsearch.Shop');
|
||||
case self::TYPE_CATEGORY:
|
||||
return $this->context->getTranslator()->trans('Categories', [], 'Modules.Facetedsearch.Shop');
|
||||
case self::TYPE_FEATURE:
|
||||
case self::TYPE_ATTRIBUTE_GROUP:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide entries with 0 results
|
||||
* Hide depending of show limit parameter
|
||||
*
|
||||
* @param array $filters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function hideZeroValuesAndShowLimit(array $filters, $showLimit)
|
||||
{
|
||||
$count = 0;
|
||||
foreach ($filters as $filter) {
|
||||
if ($filter->getMagnitude() === 0
|
||||
|| ($showLimit > 0 && $count >= $showLimit)
|
||||
) {
|
||||
$filter->setDisplayed(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
++$count;
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort filters by magnitude
|
||||
*
|
||||
* @param Filter $a
|
||||
* @param Filter $b
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function sortFiltersByMagnitude(Filter $a, Filter $b)
|
||||
{
|
||||
$aMagnitude = $a->getMagnitude();
|
||||
$bMagnitude = $b->getMagnitude();
|
||||
if ($aMagnitude == $bMagnitude) {
|
||||
// Same magnitude, sort by label
|
||||
return $this->sortFiltersByLabel($a, $b);
|
||||
}
|
||||
|
||||
return $aMagnitude > $bMagnitude ? -1 : +1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort filters by label
|
||||
*
|
||||
* @param Filter $a
|
||||
* @param Filter $b
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
private function sortFiltersByLabel(Filter $a, Filter $b)
|
||||
{
|
||||
return strnatcasecmp($a->getLabel(), $b->getLabel());
|
||||
}
|
||||
}
|
||||
184
modules/ps_facetedsearch/src/Filters/DataAccessor.php
Normal file
184
modules/ps_facetedsearch/src/Filters/DataAccessor.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?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\Filters;
|
||||
|
||||
use Combination;
|
||||
use Db;
|
||||
use Shop;
|
||||
|
||||
/**
|
||||
* Data accessor for features and attributes
|
||||
*/
|
||||
class DataAccessor
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $attributesGroup;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $attributes;
|
||||
|
||||
/**
|
||||
* @var Db
|
||||
*/
|
||||
private $database;
|
||||
|
||||
public function __construct(Db $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $idLang
|
||||
*
|
||||
* @return array|false|\PDOStatement|resource|null
|
||||
*/
|
||||
public function getAttributes($idLang, $idAttributeGroup)
|
||||
{
|
||||
if (!Combination::isFeatureActive()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!isset($this->attributes[$idLang][$idAttributeGroup])) {
|
||||
$this->attributes[$idLang] = [$idAttributeGroup => []];
|
||||
$tempAttributes = $this->database->executeS(
|
||||
'SELECT DISTINCT a.`id_attribute`, ' .
|
||||
'a.`color`, ' .
|
||||
'al.`name`, ' .
|
||||
'agl.`id_attribute_group`, ' .
|
||||
'IF(lialv.`url_name` IS NULL OR lialv.`url_name` = "", NULL, lialv.`url_name`) AS url_name, ' .
|
||||
'IF(lialv.`meta_title` IS NULL OR lialv.`meta_title` = "", NULL, lialv.`meta_title`) AS meta_title ' .
|
||||
'FROM `' . _DB_PREFIX_ . 'attribute_group` ag ' .
|
||||
'INNER JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ' .
|
||||
'ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $idLang . ') ' .
|
||||
'INNER JOIN `' . _DB_PREFIX_ . 'attribute` a ' .
|
||||
'ON a.`id_attribute_group` = ag.`id_attribute_group` ' .
|
||||
'INNER JOIN `' . _DB_PREFIX_ . 'attribute_lang` al ' .
|
||||
'ON (a.`id_attribute` = al.`id_attribute` AND al.`id_lang` = ' . (int) $idLang . ')' .
|
||||
Shop::addSqlAssociation('attribute_group', 'ag') . ' ' .
|
||||
Shop::addSqlAssociation('attribute', 'a') . ' ' .
|
||||
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value` lialv ' .
|
||||
'ON (a.`id_attribute` = lialv.`id_attribute` AND lialv.`id_lang` = ' . (int) $idLang . ') ' .
|
||||
'WHERE ag.id_attribute_group = ' . (int) $idAttributeGroup . ' ' .
|
||||
'ORDER BY agl.`name` ASC, a.`position` ASC'
|
||||
);
|
||||
|
||||
foreach ($tempAttributes as $key => $attribute) {
|
||||
$this->attributes[$idLang][$idAttributeGroup][$attribute['id_attribute']] = $attribute;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->attributes[$idLang][$idAttributeGroup];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all attributes groups for a given language
|
||||
*
|
||||
* @param int $idLang Language id
|
||||
*
|
||||
* @return array Attributes groups
|
||||
*/
|
||||
public function getAttributesGroups($idLang)
|
||||
{
|
||||
if (!Combination::isFeatureActive()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!isset($this->attributesGroup[$idLang])) {
|
||||
$this->attributesGroup[$idLang] = [];
|
||||
$tempAttributesGroup = $this->database->executeS(
|
||||
'SELECT ag.id_attribute_group, ' .
|
||||
'agl.public_name as attribute_group_name, ' .
|
||||
'is_color_group, ' .
|
||||
'IF(liaglv.`url_name` IS NULL OR liaglv.`url_name` = "", NULL, liaglv.`url_name`) AS url_name, ' .
|
||||
'IF(liaglv.`meta_title` IS NULL OR liaglv.`meta_title` = "", NULL, liaglv.`meta_title`) AS meta_title, ' .
|
||||
'IFNULL(liag.indexable, TRUE) AS indexable ' .
|
||||
'FROM `' . _DB_PREFIX_ . 'attribute_group` ag ' .
|
||||
Shop::addSqlAssociation('attribute_group', 'ag') . ' ' .
|
||||
'LEFT JOIN `' . _DB_PREFIX_ . 'attribute_group_lang` agl ' .
|
||||
'ON (ag.`id_attribute_group` = agl.`id_attribute_group` AND agl.`id_lang` = ' . (int) $idLang . ') ' .
|
||||
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_attribute_group` liag ' .
|
||||
'ON (ag.`id_attribute_group` = liag.`id_attribute_group`) ' .
|
||||
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value` AS liaglv ' .
|
||||
'ON (ag.`id_attribute_group` = liaglv.`id_attribute_group` AND agl.`id_lang` = ' . (int) $idLang . ') ' .
|
||||
'GROUP BY ag.id_attribute_group ORDER BY ag.`position` ASC'
|
||||
);
|
||||
|
||||
foreach ($tempAttributesGroup as $key => $attributeGroup) {
|
||||
$this->attributesGroup[$idLang][$attributeGroup['id_attribute_group']] = $attributeGroup;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->attributesGroup[$idLang];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get features with their associated layered information
|
||||
*
|
||||
* @param int $idLang
|
||||
*
|
||||
* @return array|false|\PDOStatement|resource|null
|
||||
*/
|
||||
public function getFeatures($idLang)
|
||||
{
|
||||
return $this->database->executeS(
|
||||
'SELECT DISTINCT f.id_feature, f.*, fl.*, ' .
|
||||
'IF(liflv.`url_name` IS NULL OR liflv.`url_name` = "", NULL, liflv.`url_name`) AS url_name, ' .
|
||||
'IF(liflv.`meta_title` IS NULL OR liflv.`meta_title` = "", NULL, liflv.`meta_title`) AS meta_title, ' .
|
||||
'lif.indexable ' .
|
||||
'FROM `' . _DB_PREFIX_ . 'feature` f ' .
|
||||
'' . Shop::addSqlAssociation('feature', 'f') . ' ' .
|
||||
'LEFT JOIN `' . _DB_PREFIX_ . 'feature_lang` fl ON (f.`id_feature` = fl.`id_feature` AND fl.`id_lang` = ' . (int) $idLang . ') ' .
|
||||
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_feature` lif ' .
|
||||
'ON (f.`id_feature` = lif.`id_feature`) ' .
|
||||
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value` liflv ' .
|
||||
'ON (f.`id_feature` = liflv.`id_feature` AND liflv.`id_lang` = ' . (int) $idLang . ') ' .
|
||||
'ORDER BY f.`position` ASC'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get feature values with their associated layered information
|
||||
*
|
||||
* @param int $idFeature
|
||||
* @param int $idLang
|
||||
*
|
||||
* @return array|false|\PDOStatement|resource|null
|
||||
*/
|
||||
public function getFeatureValues($idFeature, $idLang)
|
||||
{
|
||||
return $this->database->executeS(
|
||||
'SELECT v.*, vl.*, ' .
|
||||
'IF(lifvlv.`url_name` IS NULL OR lifvlv.`url_name` = "", NULL, lifvlv.`url_name`) AS url_name, ' .
|
||||
'IF(lifvlv.`meta_title` IS NULL OR lifvlv.`meta_title` = "", NULL, lifvlv.`meta_title`) AS meta_title ' .
|
||||
'FROM `' . _DB_PREFIX_ . 'feature_value` v ' .
|
||||
'LEFT JOIN `' . _DB_PREFIX_ . 'feature_value_lang` vl ' .
|
||||
'ON (v.`id_feature_value` = vl.`id_feature_value` AND vl.`id_lang` = ' . (int) $idLang . ') ' .
|
||||
'LEFT JOIN `' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value` lifvlv ' .
|
||||
'ON (v.`id_feature_value` = lifvlv.`id_feature_value` AND lifvlv.`id_lang` = ' . (int) $idLang . ') ' .
|
||||
'WHERE v.`id_feature` = ' . (int) $idFeature . ' ' .
|
||||
'ORDER BY vl.`value` ASC'
|
||||
);
|
||||
}
|
||||
}
|
||||
168
modules/ps_facetedsearch/src/Filters/Products.php
Normal file
168
modules/ps_facetedsearch/src/Filters/Products.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?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\Filters;
|
||||
|
||||
use Configuration;
|
||||
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
|
||||
use PrestaShop\Module\FacetedSearch\Product\Search;
|
||||
use Product;
|
||||
use Validate;
|
||||
|
||||
class Products
|
||||
{
|
||||
/**
|
||||
* Use price tax filter
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $psLayeredFilterPriceUsetax;
|
||||
|
||||
/**
|
||||
* Use price rounding
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $psLayeredFilterPriceRounding;
|
||||
|
||||
/**
|
||||
* @var AbstractAdapter
|
||||
*/
|
||||
private $searchAdapter;
|
||||
|
||||
public function __construct(Search $productSearch)
|
||||
{
|
||||
$this->searchAdapter = $productSearch->getSearchAdapter();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the products associated with the current filters
|
||||
*
|
||||
* @param int $productsPerPage
|
||||
* @param int $page
|
||||
* @param string $orderBy
|
||||
* @param string $orderWay
|
||||
* @param array $selectedFilters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getProductByFilters(
|
||||
$productsPerPage,
|
||||
$page,
|
||||
$orderBy,
|
||||
$orderWay,
|
||||
$selectedFilters = []
|
||||
) {
|
||||
$orderWay = Validate::isOrderWay($orderWay) ? $orderWay : 'ASC';
|
||||
$orderBy = Validate::isOrderBy($orderBy) ? $orderBy : 'position';
|
||||
|
||||
$this->searchAdapter->setLimit((int) $productsPerPage, ((int) $page - 1) * $productsPerPage);
|
||||
$this->searchAdapter->setOrderField($orderBy);
|
||||
$this->searchAdapter->setOrderDirection($orderWay);
|
||||
|
||||
$this->searchAdapter->addGroupBy('id_product');
|
||||
if (isset($selectedFilters['price']) || $orderBy === 'price') {
|
||||
$this->searchAdapter->addSelectField('id_product');
|
||||
$this->searchAdapter->addSelectField('price');
|
||||
$this->searchAdapter->addSelectField('price_min');
|
||||
$this->searchAdapter->addSelectField('price_max');
|
||||
}
|
||||
|
||||
$matchingProductList = $this->searchAdapter->execute();
|
||||
|
||||
$this->pricePostFiltering($matchingProductList, $selectedFilters);
|
||||
|
||||
$nbrProducts = $this->searchAdapter->count();
|
||||
|
||||
if (empty($nbrProducts)) {
|
||||
$matchingProductList = [];
|
||||
}
|
||||
|
||||
return [
|
||||
'products' => $matchingProductList,
|
||||
'count' => $nbrProducts,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Post filter product depending on the price and a few extra config variables
|
||||
*
|
||||
* @param array $matchingProductList
|
||||
* @param array $selectedFilters
|
||||
*/
|
||||
private function pricePostFiltering(&$matchingProductList, $selectedFilters)
|
||||
{
|
||||
if (!isset($selectedFilters['price'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$priceFilter['min'] = (float) ($selectedFilters['price'][0]);
|
||||
$priceFilter['max'] = (float) ($selectedFilters['price'][1]);
|
||||
|
||||
if ($this->psLayeredFilterPriceUsetax === null) {
|
||||
$this->psLayeredFilterPriceUsetax = (bool) Configuration::get('PS_LAYERED_FILTER_PRICE_USETAX');
|
||||
}
|
||||
|
||||
if ($this->psLayeredFilterPriceRounding === null) {
|
||||
$this->psLayeredFilterPriceRounding = (bool) Configuration::get('PS_LAYERED_FILTER_PRICE_ROUNDING');
|
||||
}
|
||||
|
||||
if ($this->psLayeredFilterPriceUsetax || $this->psLayeredFilterPriceRounding) {
|
||||
$this->filterPrice(
|
||||
$matchingProductList,
|
||||
$this->psLayeredFilterPriceUsetax,
|
||||
$this->psLayeredFilterPriceRounding,
|
||||
$priceFilter
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove products from the product list in case of price postFiltering
|
||||
*
|
||||
* @param array $matchingProductList
|
||||
* @param bool $psLayeredFilterPriceUsetax
|
||||
* @param bool $psLayeredFilterPriceRounding
|
||||
* @param array $priceFilter
|
||||
*/
|
||||
private function filterPrice(
|
||||
&$matchingProductList,
|
||||
$psLayeredFilterPriceUsetax,
|
||||
$psLayeredFilterPriceRounding,
|
||||
$priceFilter
|
||||
) {
|
||||
/* for this case, price could be out of range, so we need to compute the real price */
|
||||
foreach ($matchingProductList as $key => $product) {
|
||||
if (($product['price_min'] < (int) $priceFilter['min'] && $product['price_max'] > (int) $priceFilter['min'])
|
||||
|| ($product['price_max'] > (int) $priceFilter['max'] && $product['price_min'] < (int) $priceFilter['max'])
|
||||
) {
|
||||
$price = Product::getPriceStatic($product['id_product'], $psLayeredFilterPriceUsetax);
|
||||
if ($psLayeredFilterPriceRounding) {
|
||||
$price = (int) $price;
|
||||
}
|
||||
|
||||
if ($price < $priceFilter['min'] || $price > $priceFilter['max']) {
|
||||
// out of range price, exclude the product
|
||||
unset($matchingProductList[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?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\Form\Feature;
|
||||
|
||||
use Db;
|
||||
use PrestaShopDatabaseException;
|
||||
|
||||
/**
|
||||
* Provides form data
|
||||
*/
|
||||
class FormDataProvider
|
||||
{
|
||||
/**
|
||||
* @var Db
|
||||
*/
|
||||
private $database;
|
||||
|
||||
public function __construct(Db $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills form data
|
||||
*
|
||||
* @param array $params
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws PrestaShopDatabaseException
|
||||
*/
|
||||
public function getData(array $params)
|
||||
{
|
||||
$defaultUrl = [];
|
||||
$defaultMetaTitle = [];
|
||||
$isIndexable = false;
|
||||
|
||||
// if params contains id, gets data for edit form
|
||||
if (!empty($params['id'])) {
|
||||
$featureId = (int) $params['id'];
|
||||
|
||||
// returns false if request failed.
|
||||
$queryIndexable = $this->database->getValue(
|
||||
'SELECT `indexable` ' .
|
||||
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature ' .
|
||||
'WHERE `id_feature` = ' . $featureId
|
||||
);
|
||||
|
||||
$isIndexable = (bool) $queryIndexable;
|
||||
$result = $this->database->executeS(
|
||||
'SELECT `url_name`, `meta_title`, `id_lang` ' .
|
||||
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
|
||||
'WHERE `id_feature` = ' . $featureId
|
||||
);
|
||||
|
||||
if (!empty($result) && is_array($result)) {
|
||||
foreach ($result as $data) {
|
||||
$defaultUrl[$data['id_lang']] = $data['url_name'];
|
||||
$defaultMetaTitle[$data['id_lang']] = $data['meta_title'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'url' => $defaultUrl,
|
||||
'meta_title' => $defaultMetaTitle,
|
||||
'is_indexable' => $isIndexable,
|
||||
];
|
||||
}
|
||||
}
|
||||
117
modules/ps_facetedsearch/src/Form/Feature/FormModifier.php
Normal file
117
modules/ps_facetedsearch/src/Form/Feature/FormModifier.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?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\Form\Feature;
|
||||
|
||||
use Context;
|
||||
use PrestaShop\Module\FacetedSearch\Constraint\UrlSegment;
|
||||
use PrestaShopBundle\Form\Admin\Type\SwitchType;
|
||||
use PrestaShopBundle\Form\Admin\Type\TranslatableType;
|
||||
use PrestaShopBundle\Translation\TranslatorComponent;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* Adds module specific fields to BO form
|
||||
*/
|
||||
class FormModifier
|
||||
{
|
||||
/**
|
||||
* @var Context
|
||||
*/
|
||||
private $context;
|
||||
|
||||
public function __construct(Context $context)
|
||||
{
|
||||
$this->context = $context;
|
||||
}
|
||||
|
||||
public function modify(
|
||||
FormBuilderInterface $formBuilder,
|
||||
array $data
|
||||
) {
|
||||
/**
|
||||
* @var TranslatorComponent
|
||||
*/
|
||||
$translator = $this->context->getTranslator();
|
||||
$invalidCharsHint = $translator->trans(
|
||||
'Invalid characters: <>;=#{}_',
|
||||
[],
|
||||
'Modules.Facetedsearch.Admin'
|
||||
);
|
||||
|
||||
$urlTip = $translator->trans(
|
||||
'When the Faceted Search module is enabled, you can get more detailed URLs by choosing ' .
|
||||
'the word that best represents this feature. By default, PrestaShop uses the ' .
|
||||
'feature\'s name, but you can change that setting using this field.',
|
||||
[],
|
||||
'Modules.Facetedsearch.Admin'
|
||||
);
|
||||
$metaTitleTip = $translator->trans(
|
||||
'When the Faceted Search module is enabled, you can get more detailed page titles by ' .
|
||||
'choosing the word that best represents this feature. By default, PrestaShop uses the ' .
|
||||
'feature\'s name, but you can change that setting using this field.',
|
||||
[],
|
||||
'Modules.Facetedsearch.Admin'
|
||||
);
|
||||
|
||||
$formBuilder
|
||||
->add(
|
||||
'url_name',
|
||||
TranslatableType::class,
|
||||
[
|
||||
'required' => false,
|
||||
'label' => $translator->trans('URL', [], 'Modules.Facetedsearch.Admin'),
|
||||
'help' => $urlTip . ' ' . $invalidCharsHint,
|
||||
'options' => [
|
||||
'constraints' => [
|
||||
new UrlSegment([
|
||||
'message' => $translator->trans('%s is invalid.', [], 'Admin.Notifications.Error'),
|
||||
]),
|
||||
],
|
||||
],
|
||||
'data' => $data['url'],
|
||||
]
|
||||
)
|
||||
->add(
|
||||
'meta_title',
|
||||
TranslatableType::class,
|
||||
[
|
||||
'required' => false,
|
||||
'label' => $translator->trans('Meta title', [], 'Modules.Facetedsearch.Admin'),
|
||||
'help' => $metaTitleTip,
|
||||
'data' => $data['meta_title'],
|
||||
]
|
||||
)
|
||||
->add(
|
||||
'layered_indexable',
|
||||
SwitchType::class,
|
||||
[
|
||||
'required' => false,
|
||||
'label' => $translator->trans('Indexable', [], 'Modules.Facetedsearch.Admin'),
|
||||
'help' => $translator->trans(
|
||||
'Use this attribute in URL generated by the Faceted Search module.',
|
||||
[],
|
||||
'Modules.Facetedsearch.Admin'
|
||||
),
|
||||
'data' => $data['is_indexable'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
60
modules/ps_facetedsearch/src/Hook/AbstractHook.php
Normal file
60
modules/ps_facetedsearch/src/Hook/AbstractHook.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?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\Hook;
|
||||
|
||||
use Context;
|
||||
use Db;
|
||||
use Ps_Facetedsearch;
|
||||
|
||||
abstract class AbstractHook
|
||||
{
|
||||
const AVAILABLE_HOOKS = [];
|
||||
|
||||
/**
|
||||
* @var Context
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* @var Ps_Facetedsearch
|
||||
*/
|
||||
protected $module;
|
||||
|
||||
/**
|
||||
* @var Db
|
||||
*/
|
||||
protected $database;
|
||||
|
||||
public function __construct(Ps_Facetedsearch $module)
|
||||
{
|
||||
$this->module = $module;
|
||||
$this->context = $module->getContext();
|
||||
$this->database = $module->getDatabase();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getAvailableHooks()
|
||||
{
|
||||
return static::AVAILABLE_HOOKS;
|
||||
}
|
||||
}
|
||||
125
modules/ps_facetedsearch/src/Hook/Attribute.php
Normal file
125
modules/ps_facetedsearch/src/Hook/Attribute.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?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\Hook;
|
||||
|
||||
use Language;
|
||||
use Tools;
|
||||
|
||||
class Attribute extends AbstractHook
|
||||
{
|
||||
const AVAILABLE_HOOKS = [
|
||||
'actionAttributeGroupDelete',
|
||||
'actionAttributeSave',
|
||||
'displayAttributeForm',
|
||||
'actionAttributePostProcess',
|
||||
];
|
||||
|
||||
/**
|
||||
* After save attribute
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionAttributeSave(array $params)
|
||||
{
|
||||
if (empty($params['id_attribute'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
|
||||
WHERE `id_attribute` = ' . (int) $params['id_attribute']
|
||||
);
|
||||
|
||||
foreach (Language::getLanguages(false) as $language) {
|
||||
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
|
||||
$metaTitle = Tools::getValue('meta_title_' . (int) $language['id_lang']);
|
||||
if (empty($seoUrl) && empty($metaTitle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->database->execute(
|
||||
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
|
||||
(`id_attribute`, `id_lang`, `url_name`, `meta_title`)
|
||||
VALUES (
|
||||
' . (int) $params['id_attribute'] . ', ' . (int) $language['id_lang'] . ',
|
||||
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
|
||||
\'' . pSQL($metaTitle, true) . '\')'
|
||||
);
|
||||
}
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* After delete attribute
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionAttributeGroupDelete(array $params)
|
||||
{
|
||||
if (empty($params['id_attribute'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
|
||||
WHERE `id_attribute` = ' . (int) $params['id_attribute']
|
||||
);
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Post process attribute
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionAttributePostProcess(array $params)
|
||||
{
|
||||
$this->module->checkLinksRewrite($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute form
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function displayAttributeForm(array $params)
|
||||
{
|
||||
$values = [];
|
||||
|
||||
if ($result = $this->database->executeS(
|
||||
'SELECT `url_name`, `meta_title`, `id_lang`
|
||||
FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_lang_value
|
||||
WHERE `id_attribute` = ' . (int) $params['id_attribute']
|
||||
)) {
|
||||
foreach ($result as $data) {
|
||||
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
|
||||
}
|
||||
}
|
||||
|
||||
$this->context->smarty->assign([
|
||||
'languages' => Language::getLanguages(false),
|
||||
'default_form_language' => (int) $this->context->controller->default_form_language,
|
||||
'values' => $values,
|
||||
]);
|
||||
|
||||
return $this->module->render('attribute_form.tpl');
|
||||
}
|
||||
}
|
||||
145
modules/ps_facetedsearch/src/Hook/AttributeGroup.php
Normal file
145
modules/ps_facetedsearch/src/Hook/AttributeGroup.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?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\Hook;
|
||||
|
||||
use Language;
|
||||
use Tools;
|
||||
|
||||
class AttributeGroup extends AbstractHook
|
||||
{
|
||||
const AVAILABLE_HOOKS = [
|
||||
'actionAttributeGroupDelete',
|
||||
'actionAttributeGroupSave',
|
||||
'displayAttributeGroupForm',
|
||||
'displayAttributeGroupPostProcess',
|
||||
];
|
||||
|
||||
/**
|
||||
* After save Attributes group
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionAttributeGroupSave(array $params)
|
||||
{
|
||||
if (empty($params['id_attribute_group']) || Tools::getValue('layered_indexable') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
|
||||
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
|
||||
);
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
|
||||
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
|
||||
);
|
||||
|
||||
$this->database->execute(
|
||||
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_group (`id_attribute_group`, `indexable`)
|
||||
VALUES (' . (int) $params['id_attribute_group'] . ', ' . (int) Tools::getValue('layered_indexable') . ')'
|
||||
);
|
||||
|
||||
foreach (Language::getLanguages(false) as $language) {
|
||||
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
|
||||
$metaTitle = Tools::getValue('meta_title_' . (int) $language['id_lang']);
|
||||
if (empty($seoUrl) && empty($metaTitle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->database->execute(
|
||||
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
|
||||
(`id_attribute_group`, `id_lang`, `url_name`, `meta_title`)
|
||||
VALUES (
|
||||
' . (int) $params['id_attribute_group'] . ', ' . (int) $language['id_lang'] . ',
|
||||
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
|
||||
\'' . pSQL($metaTitle, true) . '\')'
|
||||
);
|
||||
}
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* After delete attribute group
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionAttributeGroupDelete(array $params)
|
||||
{
|
||||
if (empty($params['id_attribute_group'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
|
||||
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
|
||||
);
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
|
||||
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
|
||||
);
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Post process attribute group
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function displayAttributeGroupPostProcess(array $params)
|
||||
{
|
||||
$this->module->checkLinksRewrite($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute group form
|
||||
*
|
||||
* @param array $params
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function displayAttributeGroupForm(array $params)
|
||||
{
|
||||
$values = [];
|
||||
$isIndexable = $this->database->getValue(
|
||||
'SELECT `indexable`
|
||||
FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group
|
||||
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
|
||||
);
|
||||
|
||||
if ($result = $this->database->executeS(
|
||||
'SELECT `url_name`, `meta_title`, `id_lang` FROM ' . _DB_PREFIX_ . 'layered_indexable_attribute_group_lang_value
|
||||
WHERE `id_attribute_group` = ' . (int) $params['id_attribute_group']
|
||||
)) {
|
||||
foreach ($result as $data) {
|
||||
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
|
||||
}
|
||||
}
|
||||
|
||||
$this->context->smarty->assign([
|
||||
'languages' => Language::getLanguages(false),
|
||||
'default_form_language' => (int) $this->context->controller->default_form_language,
|
||||
'values' => $values,
|
||||
'is_indexable' => (bool) $isIndexable,
|
||||
]);
|
||||
|
||||
return $this->module->render('attribute_group_form.tpl');
|
||||
}
|
||||
}
|
||||
97
modules/ps_facetedsearch/src/Hook/Category.php
Normal file
97
modules/ps_facetedsearch/src/Hook/Category.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?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\Hook;
|
||||
|
||||
use Tools;
|
||||
|
||||
class Category extends AbstractHook
|
||||
{
|
||||
const AVAILABLE_HOOKS = [
|
||||
'actionCategoryAdd',
|
||||
'actionCategoryDelete',
|
||||
'actionCategoryUpdate',
|
||||
];
|
||||
|
||||
/**
|
||||
* Category addition
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionCategoryAdd(array $params)
|
||||
{
|
||||
$this->module->rebuildLayeredCache([], [(int) $params['category']->id]);
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Category update
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionCategoryUpdate(array $params)
|
||||
{
|
||||
/*
|
||||
* The category status might (active, inactive) have changed,
|
||||
* we have to update the layered cache table structure
|
||||
*/
|
||||
if (isset($params['category']) && !$params['category']->active) {
|
||||
$this->cleanAndRebuildCategoryFilters($params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Category deletion
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionCategoryDelete(array $params)
|
||||
{
|
||||
$this->cleanAndRebuildCategoryFilters($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean and rebuild category filters
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
private function cleanAndRebuildCategoryFilters(array $params)
|
||||
{
|
||||
$layeredFilterList = $this->database->executeS(
|
||||
'SELECT * FROM ' . _DB_PREFIX_ . 'layered_filter'
|
||||
);
|
||||
|
||||
foreach ($layeredFilterList as $layeredFilter) {
|
||||
$data = Tools::unSerialize($layeredFilter['filters']);
|
||||
|
||||
if (in_array((int) $params['category']->id, $data['categories'])) {
|
||||
unset($data['categories'][array_search((int) $params['category']->id, $data['categories'])]);
|
||||
$this->database->execute(
|
||||
'UPDATE `' . _DB_PREFIX_ . 'layered_filter`
|
||||
SET `filters` = \'' . pSQL(serialize($data)) . '\'
|
||||
WHERE `id_layered_filter` = ' . (int) $layeredFilter['id_layered_filter']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
$this->module->buildLayeredCategories();
|
||||
}
|
||||
}
|
||||
38
modules/ps_facetedsearch/src/Hook/Configuration.php
Normal file
38
modules/ps_facetedsearch/src/Hook/Configuration.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\Hook;
|
||||
|
||||
class Configuration extends AbstractHook
|
||||
{
|
||||
const AVAILABLE_HOOKS = [
|
||||
'actionProductPreferencesPageStockSave',
|
||||
];
|
||||
|
||||
/**
|
||||
* After save of product stock preferences form
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionProductPreferencesPageStockSave(array $params)
|
||||
{
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
}
|
||||
40
modules/ps_facetedsearch/src/Hook/Design.php
Normal file
40
modules/ps_facetedsearch/src/Hook/Design.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?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\Hook;
|
||||
|
||||
class Design extends AbstractHook
|
||||
{
|
||||
const AVAILABLE_HOOKS = [
|
||||
'displayLeftColumn',
|
||||
];
|
||||
|
||||
/**
|
||||
* Force this hook to be called here instance of using WidgetInterface
|
||||
* because Hook::isHookCallableOn before the instanceof function.
|
||||
* Which means is_callable always returns true with a __call usage.
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function displayLeftColumn(array $params)
|
||||
{
|
||||
return $this->module->fetch('module:ps_facetedsearch/ps_facetedsearch.tpl');
|
||||
}
|
||||
}
|
||||
268
modules/ps_facetedsearch/src/Hook/Feature.php
Normal file
268
modules/ps_facetedsearch/src/Hook/Feature.php
Normal file
@@ -0,0 +1,268 @@
|
||||
<?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\Hook;
|
||||
|
||||
use Configuration;
|
||||
use Language;
|
||||
use PrestaShop\Module\FacetedSearch\Form\Feature\FormDataProvider;
|
||||
use PrestaShop\Module\FacetedSearch\Form\Feature\FormModifier;
|
||||
use PrestaShopDatabaseException;
|
||||
use Ps_Facetedsearch;
|
||||
use Tools;
|
||||
|
||||
class Feature extends AbstractHook
|
||||
{
|
||||
/**
|
||||
* @var FormModifier
|
||||
*/
|
||||
private $formModifier;
|
||||
|
||||
/**
|
||||
* @var FormDataProvider
|
||||
*/
|
||||
private $dataProvider;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isMigratedPage = false;
|
||||
|
||||
public function __construct(Ps_Facetedsearch $module)
|
||||
{
|
||||
parent::__construct($module);
|
||||
|
||||
$this->formModifier = new FormModifier($module->getContext());
|
||||
$this->dataProvider = new FormDataProvider($module->getDatabase());
|
||||
}
|
||||
|
||||
const AVAILABLE_HOOKS = [
|
||||
'actionFeatureSave',
|
||||
'actionFeatureDelete',
|
||||
'displayFeatureForm',
|
||||
'displayFeaturePostProcess',
|
||||
'actionFeatureFormBuilderModifier',
|
||||
'actionAfterCreateFeatureFormHandler',
|
||||
'actionAfterUpdateFeatureFormHandler',
|
||||
];
|
||||
|
||||
/**
|
||||
* Hook for modifying feature form formBuilder
|
||||
*
|
||||
* @param array $params
|
||||
*
|
||||
* @throws PrestaShopDatabaseException
|
||||
*/
|
||||
public function actionFeatureFormBuilderModifier(array $params)
|
||||
{
|
||||
$this->isMigratedPage = true;
|
||||
$this->formModifier->modify($params['form_builder'], $this->dataProvider->getData($params));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook after create feature.
|
||||
*
|
||||
* @since PrestaShop 1.7.8.0
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionAfterCreateFeatureFormHandler(array $params)
|
||||
{
|
||||
$this->save($params['id'], $params['form_data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook after update feature.
|
||||
*
|
||||
* @since PrestaShop 1.7.8.0
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionAfterUpdateFeatureFormHandler(array $params)
|
||||
{
|
||||
$this->save($params['id'], $params['form_data']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook after delete a feature
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionFeatureDelete(array $params)
|
||||
{
|
||||
if (empty($params['id_feature'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
|
||||
WHERE `id_feature` = ' . (int) $params['id_feature']
|
||||
);
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook post process feature
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function displayFeaturePostProcess(array $params)
|
||||
{
|
||||
$this->module->checkLinksRewrite($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook feature form
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function displayFeatureForm(array $params)
|
||||
{
|
||||
if ($this->isMigratedPage === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$values = [];
|
||||
$isIndexable = $this->database->getValue(
|
||||
'SELECT `indexable` ' .
|
||||
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature ' .
|
||||
'WHERE `id_feature` = ' . (int) $params['id_feature']
|
||||
);
|
||||
|
||||
$result = $this->database->executeS(
|
||||
'SELECT `url_name`, `meta_title`, `id_lang` ' .
|
||||
'FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
|
||||
'WHERE `id_feature` = ' . (int) $params['id_feature']
|
||||
);
|
||||
if ($result) {
|
||||
foreach ($result as $data) {
|
||||
$values[$data['id_lang']] = [
|
||||
'url_name' => $data['url_name'],
|
||||
'meta_title' => $data['meta_title'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->context->smarty->assign([
|
||||
'languages' => Language::getLanguages(false),
|
||||
'default_form_language' => (int) $this->context->controller->default_form_language,
|
||||
'values' => $values,
|
||||
'is_indexable' => (bool) $isIndexable,
|
||||
]);
|
||||
|
||||
return $this->module->render('feature_form.tpl');
|
||||
}
|
||||
|
||||
/**
|
||||
* After save feature
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionFeatureSave(array $params)
|
||||
{
|
||||
if (empty($params['id_feature']) || Tools::getValue('layered_indexable') === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$featureId = (int) $params['id_feature'];
|
||||
$formData = [
|
||||
'layered_indexable' => Tools::getValue('layered_indexable'),
|
||||
];
|
||||
|
||||
foreach (Language::getLanguages(false) as $language) {
|
||||
$langId = (int) $language['id_lang'];
|
||||
$seoUrl = Tools::getValue('url_name_' . $langId);
|
||||
$metaTitle = Tools::getValue('meta_title_' . $langId);
|
||||
|
||||
if (empty($seoUrl) && empty($metaTitle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$formData['meta_title'][$langId] = $metaTitle;
|
||||
$formData['url_name'][$langId] = $seoUrl;
|
||||
}
|
||||
|
||||
$this->save($featureId, $formData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves feature form.
|
||||
*
|
||||
* @param int $featureId
|
||||
* @param array $formData
|
||||
*
|
||||
* @since PrestaShop 1.7.8.0
|
||||
*/
|
||||
private function save($featureId, array $formData)
|
||||
{
|
||||
$this->cleanLayeredIndexableTables($featureId);
|
||||
|
||||
$this->database->execute(
|
||||
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature
|
||||
(`id_feature`, `indexable`)
|
||||
VALUES (' . (int) $featureId . ', ' . (int) $formData['layered_indexable'] . ')'
|
||||
);
|
||||
|
||||
$defaultLangId = (int) Configuration::get('PS_LANG_DEFAULT');
|
||||
$query = 'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value ' .
|
||||
'(`id_feature`, `id_lang`, `url_name`, `meta_title`) ' .
|
||||
'VALUES (%d, %d, \'%s\', \'%s\')';
|
||||
|
||||
foreach (Language::getLanguages(false) as $language) {
|
||||
$langId = (int) $language['id_lang'];
|
||||
$metaTitle = pSQL($formData['meta_title'][$langId]);
|
||||
$seoUrl = $formData['url_name'][$langId];
|
||||
$name = $formData['name'][$langId] ?: $formData['name'][$defaultLangId];
|
||||
|
||||
if (!empty($seoUrl)) {
|
||||
$seoUrl = pSQL(Tools::link_rewrite($seoUrl));
|
||||
}
|
||||
|
||||
$this->database->execute(
|
||||
sprintf(
|
||||
$query,
|
||||
$featureId,
|
||||
$langId,
|
||||
$seoUrl,
|
||||
$metaTitle
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes from layered_indexable_feature and layered_indexable_feature_lang_value by feature id
|
||||
*
|
||||
* @param int $featureId
|
||||
*/
|
||||
private function cleanLayeredIndexableTables($featureId)
|
||||
{
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
|
||||
WHERE `id_feature` = ' . $featureId
|
||||
);
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value
|
||||
WHERE `id_feature` = ' . $featureId
|
||||
);
|
||||
}
|
||||
}
|
||||
129
modules/ps_facetedsearch/src/Hook/FeatureValue.php
Normal file
129
modules/ps_facetedsearch/src/Hook/FeatureValue.php
Normal file
@@ -0,0 +1,129 @@
|
||||
<?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\Hook;
|
||||
|
||||
use Language;
|
||||
use Tools;
|
||||
|
||||
class FeatureValue extends AbstractHook
|
||||
{
|
||||
const AVAILABLE_HOOKS = [
|
||||
'actionFeatureValueSave',
|
||||
'actionFeatureValueDelete',
|
||||
'displayFeatureValueForm',
|
||||
'displayFeatureValuePostProcess',
|
||||
];
|
||||
|
||||
/**
|
||||
* After save feature value
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionFeatureValueSave(array $params)
|
||||
{
|
||||
if (empty($params['id_feature_value'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Removing all indexed language data for this attribute value id
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
|
||||
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
|
||||
);
|
||||
|
||||
foreach (Language::getLanguages(false) as $language) {
|
||||
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
|
||||
$metaTitle = Tools::getValue('meta_title_' . (int) $language['id_lang']);
|
||||
if (empty($seoUrl) && empty($metaTitle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->database->execute(
|
||||
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
|
||||
(`id_feature_value`, `id_lang`, `url_name`, `meta_title`)
|
||||
VALUES (
|
||||
' . (int) $params['id_feature_value'] . ', ' . (int) $language['id_lang'] . ',
|
||||
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
|
||||
\'' . pSQL($metaTitle, true) . '\')'
|
||||
);
|
||||
}
|
||||
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* After delete Feature value
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionFeatureValueDelete(array $params)
|
||||
{
|
||||
if (empty($params['id_feature_value'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->database->execute(
|
||||
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
|
||||
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
|
||||
);
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Post process feature value
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function displayFeatureValuePostProcess(array $params)
|
||||
{
|
||||
$this->module->checkLinksRewrite($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display feature value form
|
||||
*
|
||||
* @param array $params
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function displayFeatureValueForm(array $params)
|
||||
{
|
||||
$values = [];
|
||||
|
||||
if ($result = $this->database->executeS(
|
||||
'SELECT `url_name`, `meta_title`, `id_lang`
|
||||
FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_value_lang_value
|
||||
WHERE `id_feature_value` = ' . (int) $params['id_feature_value']
|
||||
)) {
|
||||
foreach ($result as $data) {
|
||||
$values[$data['id_lang']] = ['url_name' => $data['url_name'], 'meta_title' => $data['meta_title']];
|
||||
}
|
||||
}
|
||||
|
||||
$this->context->smarty->assign([
|
||||
'languages' => Language::getLanguages(false),
|
||||
'default_form_language' => (int) $this->context->controller->default_form_language,
|
||||
'values' => $values,
|
||||
]);
|
||||
|
||||
return $this->module->render('feature_value_form.tpl');
|
||||
}
|
||||
}
|
||||
44
modules/ps_facetedsearch/src/Hook/Product.php
Normal file
44
modules/ps_facetedsearch/src/Hook/Product.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?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\Hook;
|
||||
|
||||
class Product extends AbstractHook
|
||||
{
|
||||
const AVAILABLE_HOOKS = [
|
||||
'actionProductSave',
|
||||
];
|
||||
|
||||
/**
|
||||
* After save product
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionProductSave(array $params)
|
||||
{
|
||||
if (empty($params['id_product'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->module->indexProductPrices((int) $params['id_product']);
|
||||
$this->module->indexAttributes((int) $params['id_product']);
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
}
|
||||
79
modules/ps_facetedsearch/src/Hook/ProductSearch.php
Normal file
79
modules/ps_facetedsearch/src/Hook/ProductSearch.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Hook;
|
||||
|
||||
use PrestaShop\Module\FacetedSearch\Filters\Converter;
|
||||
use PrestaShop\Module\FacetedSearch\Filters\DataAccessor;
|
||||
use PrestaShop\Module\FacetedSearch\Product\SearchProvider;
|
||||
use PrestaShop\Module\FacetedSearch\URLSerializer;
|
||||
|
||||
class ProductSearch extends AbstractHook
|
||||
{
|
||||
const AVAILABLE_HOOKS = [
|
||||
'productSearchProvider',
|
||||
];
|
||||
|
||||
/**
|
||||
* Hook project search provider
|
||||
*
|
||||
* @param array $params
|
||||
*
|
||||
* @return SearchProvider|null
|
||||
*/
|
||||
public function productSearchProvider(array $params)
|
||||
{
|
||||
$query = $params['query'];
|
||||
// do something with query,
|
||||
// e.g. use $query->getIdCategory()
|
||||
// to choose a template for filters.
|
||||
// Query is an instance of:
|
||||
// PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery
|
||||
if ($query->getIdCategory()) {
|
||||
$this->context->controller->addJqueryUi('slider');
|
||||
$this->context->controller->registerStylesheet(
|
||||
'facetedsearch_front',
|
||||
'/modules/ps_facetedsearch/views/dist/front.css'
|
||||
);
|
||||
$this->context->controller->registerJavascript(
|
||||
'facetedsearch_front',
|
||||
'/modules/ps_facetedsearch/views/dist/front.js',
|
||||
['position' => 'bottom', 'priority' => 100]
|
||||
);
|
||||
|
||||
$urlSerializer = new URLSerializer();
|
||||
$dataAccessor = new DataAccessor($this->module->getDatabase());
|
||||
|
||||
return new SearchProvider(
|
||||
$this->module,
|
||||
new Converter(
|
||||
$this->module->getContext(),
|
||||
$this->module->getDatabase(),
|
||||
$urlSerializer,
|
||||
$dataAccessor
|
||||
),
|
||||
$urlSerializer,
|
||||
$dataAccessor
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
72
modules/ps_facetedsearch/src/Hook/SpecificPrice.php
Normal file
72
modules/ps_facetedsearch/src/Hook/SpecificPrice.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?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\Hook;
|
||||
|
||||
class SpecificPrice extends AbstractHook
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $productsBefore = null;
|
||||
|
||||
const AVAILABLE_HOOKS = [
|
||||
'actionObjectSpecificPriceRuleUpdateBefore',
|
||||
'actionAdminSpecificPriceRuleControllerSaveAfter',
|
||||
];
|
||||
|
||||
/**
|
||||
* Before saving a specific price rule
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionObjectSpecificPriceRuleUpdateBefore(array $params)
|
||||
{
|
||||
if (empty($params['object']->id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \SpecificPriceRule */
|
||||
$specificPrice = $params['object'];
|
||||
$this->productsBefore = $specificPrice->getAffectedProducts();
|
||||
}
|
||||
|
||||
/**
|
||||
* After saving a specific price rule
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function actionAdminSpecificPriceRuleControllerSaveAfter(array $params)
|
||||
{
|
||||
if (empty($params['return']->id) || empty($this->productsBefore)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var \SpecificPriceRule */
|
||||
$specificPrice = $params['return'];
|
||||
$affectedProducts = array_merge($this->productsBefore, $specificPrice->getAffectedProducts());
|
||||
foreach ($affectedProducts as $product) {
|
||||
$this->module->indexProductPrices($product['id_product']);
|
||||
$this->module->indexAttributes($product['id_product']);
|
||||
}
|
||||
|
||||
$this->module->invalidateLayeredFilterBlockCache();
|
||||
}
|
||||
}
|
||||
113
modules/ps_facetedsearch/src/HookDispatcher.php
Normal file
113
modules/ps_facetedsearch/src/HookDispatcher.php
Normal file
@@ -0,0 +1,113 @@
|
||||
<?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;
|
||||
|
||||
use Ps_Facetedsearch;
|
||||
|
||||
/**
|
||||
* Class works with Hook\AbstractHook instances in order to reduce ps_facetedsearch.php size.
|
||||
*
|
||||
* The dispatch method is called from the __call method in the module class.
|
||||
*/
|
||||
class HookDispatcher
|
||||
{
|
||||
const CLASSES = [
|
||||
Hook\Attribute::class,
|
||||
Hook\AttributeGroup::class,
|
||||
Hook\Category::class,
|
||||
Hook\Configuration::class,
|
||||
Hook\Design::class,
|
||||
Hook\Feature::class,
|
||||
Hook\FeatureValue::class,
|
||||
Hook\Product::class,
|
||||
Hook\ProductSearch::class,
|
||||
Hook\SpecificPrice::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* List of available hooks
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private $availableHooks = [];
|
||||
|
||||
/**
|
||||
* Hook classes
|
||||
*
|
||||
* @var Hook\AbstractHook[]
|
||||
*/
|
||||
private $hooks = [];
|
||||
|
||||
/**
|
||||
* Module
|
||||
*
|
||||
* @var Ps_Facetedsearch
|
||||
*/
|
||||
private $module;
|
||||
|
||||
/**
|
||||
* Init hooks
|
||||
*
|
||||
* @param Ps_Facetedsearch $module
|
||||
*/
|
||||
public function __construct(Ps_Facetedsearch $module)
|
||||
{
|
||||
$this->module = $module;
|
||||
|
||||
foreach (self::CLASSES as $hookClass) {
|
||||
$hook = new $hookClass($this->module);
|
||||
$this->availableHooks = array_merge($this->availableHooks, $hook->getAvailableHooks());
|
||||
$this->hooks[] = $hook;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available hooks
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAvailableHooks()
|
||||
{
|
||||
return $this->availableHooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find hook and dispatch it
|
||||
*
|
||||
* @param string $hookName
|
||||
* @param array $params
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function dispatch($hookName, array $params = [])
|
||||
{
|
||||
$hookName = preg_replace('~^hook~', '', $hookName);
|
||||
|
||||
foreach ($this->hooks as $hook) {
|
||||
if (method_exists($hook, $hookName)) {
|
||||
return call_user_func([$hook, $hookName], $params);
|
||||
}
|
||||
}
|
||||
|
||||
// No hook found, render it as a widget
|
||||
return $this->module->renderWidget($hookName, $params);
|
||||
}
|
||||
}
|
||||
332
modules/ps_facetedsearch/src/Product/Search.php
Normal file
332
modules/ps_facetedsearch/src/Product/Search.php
Normal file
@@ -0,0 +1,332 @@
|
||||
<?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\Product;
|
||||
|
||||
use Category;
|
||||
use Configuration;
|
||||
use Context;
|
||||
use FrontController;
|
||||
use Group;
|
||||
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
|
||||
use PrestaShop\Module\FacetedSearch\Adapter\MySQL as MySQLAdapter;
|
||||
use Tools;
|
||||
|
||||
class Search
|
||||
{
|
||||
const STOCK_MANAGEMENT_FILTER = 'with_stock_management';
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $psStockManagement;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $psOrderOutOfStock;
|
||||
|
||||
/**
|
||||
* @var AbstractAdapter
|
||||
*/
|
||||
protected $searchAdapter;
|
||||
|
||||
/**
|
||||
* @var Context
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* Search constructor.
|
||||
*
|
||||
* @param Context $context
|
||||
* @param string $adapterType
|
||||
*/
|
||||
public function __construct(Context $context, $adapterType = MySQLAdapter::TYPE)
|
||||
{
|
||||
$this->context = $context;
|
||||
|
||||
switch ($adapterType) {
|
||||
case MySQLAdapter::TYPE:
|
||||
default:
|
||||
$this->searchAdapter = new MySQLAdapter();
|
||||
}
|
||||
|
||||
if ($this->psStockManagement === null) {
|
||||
$this->psStockManagement = (bool) Configuration::get('PS_STOCK_MANAGEMENT');
|
||||
}
|
||||
|
||||
if ($this->psOrderOutOfStock === null) {
|
||||
$this->psOrderOutOfStock = (bool) Configuration::get('PS_ORDER_OUT_OF_STOCK');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AbstractAdapter
|
||||
*/
|
||||
public function getSearchAdapter()
|
||||
{
|
||||
return $this->searchAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the initial population of the search filter
|
||||
*
|
||||
* @param array $selectedFilters
|
||||
*/
|
||||
public function initSearch($selectedFilters)
|
||||
{
|
||||
$homeCategory = Configuration::get('PS_HOME_CATEGORY');
|
||||
/* If the current category isn't defined or if it's homepage, we have nothing to display */
|
||||
$idParent = (int) Tools::getValue(
|
||||
'id_category',
|
||||
Tools::getValue('id_category_layered', $homeCategory)
|
||||
);
|
||||
|
||||
$parent = new Category((int) $idParent);
|
||||
|
||||
$psLayeredFullTree = Configuration::get('PS_LAYERED_FULL_TREE');
|
||||
if (!$psLayeredFullTree) {
|
||||
$this->addFilter('id_category', [$parent->id]);
|
||||
}
|
||||
|
||||
$psLayeredFilterByDefaultCategory = Configuration::get('PS_LAYERED_FILTER_BY_DEFAULT_CATEGORY');
|
||||
if ($psLayeredFilterByDefaultCategory) {
|
||||
$this->addFilter('id_category_default', [$parent->id]);
|
||||
}
|
||||
|
||||
// Visibility of a product must be in catalog or both (search & catalog)
|
||||
$this->addFilter('visibility', ['both', 'catalog']);
|
||||
|
||||
// User must belong to one of the groups that can access the product
|
||||
if (Group::isFeatureActive()) {
|
||||
$groups = FrontController::getCurrentCustomerGroups();
|
||||
|
||||
$this->addFilter('id_group', empty($groups) ? [Group::getCurrent()->id] : $groups);
|
||||
}
|
||||
|
||||
$this->addSearchFilters(
|
||||
$selectedFilters,
|
||||
$psLayeredFullTree ? $parent : null,
|
||||
(int) $this->context->shop->id
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $selectedFilters
|
||||
* @param Category $parent
|
||||
* @param int $idShop
|
||||
*/
|
||||
private function addSearchFilters($selectedFilters, $parent, $idShop)
|
||||
{
|
||||
$hasCategory = false;
|
||||
foreach ($selectedFilters as $key => $filterValues) {
|
||||
if (!count($filterValues)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($key) {
|
||||
case 'id_feature':
|
||||
$operationsFilter = [];
|
||||
foreach ($filterValues as $featureId => $filterValue) {
|
||||
$this->getSearchAdapter()->addOperationsFilter(
|
||||
'with_features_' . $featureId,
|
||||
[[['id_feature_value', $filterValue]]]
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'id_attribute_group':
|
||||
$operationsFilter = [];
|
||||
foreach ($filterValues as $attributeId => $filterValue) {
|
||||
$this->getSearchAdapter()->addOperationsFilter(
|
||||
'with_attributes_' . $attributeId,
|
||||
[[['id_attribute', $filterValue]]]
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'category':
|
||||
$this->addFilter('id_category', $filterValues);
|
||||
$this->getSearchAdapter()->resetFilter('id_category_default');
|
||||
$hasCategory = true;
|
||||
break;
|
||||
|
||||
case 'quantity':
|
||||
/*
|
||||
* $filterValues options can have following values:
|
||||
* 0 - Not available - 0 or less quantity and disabled backorders
|
||||
* 1 - Available - Positive quantity or enabled backorders
|
||||
* 2 - In stock - Positive quantity
|
||||
*/
|
||||
|
||||
// If all three values are checked, we show everything
|
||||
if (count($filterValues) == 3) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If stock management is deactivated, we show everything
|
||||
if (!$this->psStockManagement) {
|
||||
break;
|
||||
}
|
||||
|
||||
$operationsFilter = [];
|
||||
|
||||
// Simple cases with 1 option selected
|
||||
if (count($filterValues) == 1) {
|
||||
// Not available
|
||||
if ($filterValues[0] == 0) {
|
||||
$operationsFilter[] = [
|
||||
['quantity', [0], '<='],
|
||||
['out_of_stock', $this->psOrderOutOfStock ? [0] : [0, 2], '='],
|
||||
];
|
||||
// Available
|
||||
} elseif ($filterValues[0] == 1) {
|
||||
$operationsFilter[] = [
|
||||
['out_of_stock', $this->psOrderOutOfStock ? [1, 2] : [1], '='],
|
||||
];
|
||||
$operationsFilter[] = [
|
||||
['quantity', [0], '>'],
|
||||
];
|
||||
// In stock
|
||||
} elseif ($filterValues[0] == 2) {
|
||||
$operationsFilter[] = [
|
||||
['quantity', [0], '>'],
|
||||
];
|
||||
}
|
||||
// Cases with 2 options selected
|
||||
} elseif (count($filterValues) == 2) {
|
||||
// Not available and available, we show everything
|
||||
if (in_array(0, $filterValues) && in_array(1, $filterValues)) {
|
||||
break;
|
||||
// Not available or in stock
|
||||
} elseif (in_array(0, $filterValues) && in_array(2, $filterValues)) {
|
||||
$operationsFilter[] = [
|
||||
['quantity', [0], '<='],
|
||||
['out_of_stock', $this->psOrderOutOfStock ? [0] : [0, 2], '='],
|
||||
];
|
||||
$operationsFilter[] = [
|
||||
['quantity', [0], '>'],
|
||||
];
|
||||
// Available or in stock
|
||||
} elseif (in_array(1, $filterValues) && in_array(2, $filterValues)) {
|
||||
$operationsFilter[] = [
|
||||
['out_of_stock', $this->psOrderOutOfStock ? [1, 2] : [1], '='],
|
||||
];
|
||||
$operationsFilter[] = [
|
||||
['quantity', [0], '>'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$this->getSearchAdapter()->addOperationsFilter(
|
||||
self::STOCK_MANAGEMENT_FILTER,
|
||||
$operationsFilter
|
||||
);
|
||||
break;
|
||||
|
||||
case 'manufacturer':
|
||||
$this->addFilter('id_manufacturer', $filterValues);
|
||||
break;
|
||||
|
||||
case 'condition':
|
||||
if (count($selectedFilters['condition']) == 3) {
|
||||
break;
|
||||
}
|
||||
$this->addFilter('condition', $filterValues);
|
||||
break;
|
||||
|
||||
case 'weight':
|
||||
if (!empty($selectedFilters['weight'][0]) || !empty($selectedFilters['weight'][1])) {
|
||||
$this->getSearchAdapter()->addFilter(
|
||||
'weight',
|
||||
[(float) $selectedFilters['weight'][0]],
|
||||
'>='
|
||||
);
|
||||
$this->getSearchAdapter()->addFilter(
|
||||
'weight',
|
||||
[(float) $selectedFilters['weight'][1]],
|
||||
'<='
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'price':
|
||||
if (isset($selectedFilters['price'])
|
||||
&& (
|
||||
$selectedFilters['price'][0] !== '' || $selectedFilters['price'][1] !== ''
|
||||
)
|
||||
) {
|
||||
$this->addPriceFilter(
|
||||
(float) $selectedFilters['price'][0],
|
||||
(float) $selectedFilters['price'][1]
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$hasCategory && $parent !== null) {
|
||||
$this->getSearchAdapter()->addFilter('nleft', [$parent->nleft], '>=');
|
||||
$this->getSearchAdapter()->addFilter('nright', [$parent->nright], '<=');
|
||||
}
|
||||
|
||||
$this->getSearchAdapter()->addFilter('id_shop', [$idShop]);
|
||||
$this->getSearchAdapter()->addGroupBy('id_product');
|
||||
|
||||
$this->getSearchAdapter()->useFiltersAsInitialPopulation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a filter with the filterValues extracted from the selectedFilters
|
||||
*
|
||||
* @param string $filterName
|
||||
* @param array $filterValues
|
||||
*/
|
||||
public function addFilter($filterName, array $filterValues)
|
||||
{
|
||||
$values = [];
|
||||
foreach ($filterValues as $filterValue) {
|
||||
if (is_array($filterValue)) {
|
||||
foreach ($filterValue as $subFilterValue) {
|
||||
$values[] = (int) $subFilterValue;
|
||||
}
|
||||
} else {
|
||||
$values[] = $filterValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($values)) {
|
||||
$this->getSearchAdapter()->addFilter($filterName, $values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a price filter
|
||||
*
|
||||
* @param float $minPrice
|
||||
* @param float $maxPrice
|
||||
*/
|
||||
private function addPriceFilter($minPrice, $maxPrice)
|
||||
{
|
||||
$this->getSearchAdapter()->addFilter('price_min', [$maxPrice], '<=');
|
||||
$this->getSearchAdapter()->addFilter('price_max', [$minPrice], '>=');
|
||||
}
|
||||
}
|
||||
38
modules/ps_facetedsearch/src/Product/SearchFactory.php
Normal file
38
modules/ps_facetedsearch/src/Product/SearchFactory.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\Product;
|
||||
|
||||
use Context;
|
||||
|
||||
class SearchFactory
|
||||
{
|
||||
/**
|
||||
* Returns an instance of Search for this context
|
||||
*
|
||||
* @param Context $context
|
||||
*
|
||||
* @return Search
|
||||
*/
|
||||
public function build(Context $context)
|
||||
{
|
||||
return new Search($context);
|
||||
}
|
||||
}
|
||||
522
modules/ps_facetedsearch/src/Product/SearchProvider.php
Normal file
522
modules/ps_facetedsearch/src/Product/SearchProvider.php
Normal file
@@ -0,0 +1,522 @@
|
||||
<?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\Product;
|
||||
|
||||
use Configuration;
|
||||
use PrestaShop\Module\FacetedSearch\Filters;
|
||||
use PrestaShop\Module\FacetedSearch\URLSerializer;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\FacetsRendererInterface;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\SortOrder;
|
||||
use Ps_Facetedsearch;
|
||||
use Tools;
|
||||
|
||||
class SearchProvider implements FacetsRendererInterface, ProductSearchProviderInterface
|
||||
{
|
||||
/**
|
||||
* @var Ps_Facetedsearch
|
||||
*/
|
||||
private $module;
|
||||
|
||||
/**
|
||||
* @var Filters\Converter
|
||||
*/
|
||||
private $filtersConverter;
|
||||
|
||||
/**
|
||||
* @var Filters\DataAccessor
|
||||
*/
|
||||
private $dataAccessor;
|
||||
|
||||
/**
|
||||
* @var URLSerializer
|
||||
*/
|
||||
private $urlSerializer;
|
||||
|
||||
/**
|
||||
* @var SearchFactory
|
||||
*/
|
||||
private $searchFactory;
|
||||
|
||||
public function __construct(
|
||||
Ps_Facetedsearch $module,
|
||||
Filters\Converter $converter,
|
||||
URLSerializer $serializer,
|
||||
Filters\DataAccessor $dataAccessor,
|
||||
SearchFactory $searchFactory = null
|
||||
) {
|
||||
$this->module = $module;
|
||||
$this->filtersConverter = $converter;
|
||||
$this->urlSerializer = $serializer;
|
||||
$this->dataAccessor = $dataAccessor;
|
||||
$this->searchFactory = $searchFactory === null ? new SearchFactory() : $searchFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private function getAvailableSortOrders()
|
||||
{
|
||||
$sortSalesDesc = new SortOrder('product', 'sales', 'desc');
|
||||
$sortPosAsc = new SortOrder('product', 'position', 'asc');
|
||||
$sortNameAsc = new SortOrder('product', 'name', 'asc');
|
||||
$sortNameDesc = new SortOrder('product', 'name', 'desc');
|
||||
$sortPriceAsc = new SortOrder('product', 'price', 'asc');
|
||||
$sortPriceDesc = new SortOrder('product', 'price', 'desc');
|
||||
$translator = $this->module->getTranslator();
|
||||
|
||||
return [
|
||||
$sortSalesDesc->setLabel(
|
||||
$translator->trans('Best sellers', [], 'Modules.Facetedsearch.Shop')
|
||||
),
|
||||
$sortPosAsc->setLabel(
|
||||
$translator->trans('Relevance', [], 'Modules.Facetedsearch.Shop')
|
||||
),
|
||||
$sortNameAsc->setLabel(
|
||||
$translator->trans('Name, A to Z', [], 'Shop.Theme.Catalog')
|
||||
),
|
||||
$sortNameDesc->setLabel(
|
||||
$translator->trans('Name, Z to A', [], 'Shop.Theme.Catalog')
|
||||
),
|
||||
$sortPriceAsc->setLabel(
|
||||
$translator->trans('Price, low to high', [], 'Shop.Theme.Catalog')
|
||||
),
|
||||
$sortPriceDesc->setLabel(
|
||||
$translator->trans('Price, high to low', [], 'Shop.Theme.Catalog')
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProductSearchContext $context
|
||||
* @param ProductSearchQuery $query
|
||||
*
|
||||
* @return ProductSearchResult
|
||||
*/
|
||||
public function runQuery(
|
||||
ProductSearchContext $context,
|
||||
ProductSearchQuery $query
|
||||
) {
|
||||
$result = new ProductSearchResult();
|
||||
// extract the filter array from the Search query
|
||||
$facetedSearchFilters = $this->filtersConverter->createFacetedSearchFiltersFromQuery($query);
|
||||
|
||||
$context = $this->module->getContext();
|
||||
$facetedSearch = $this->searchFactory->build($context);
|
||||
// init the search with the initial population associated with the current filters
|
||||
$facetedSearch->initSearch($facetedSearchFilters);
|
||||
|
||||
$orderBy = $query->getSortOrder()->toLegacyOrderBy(false);
|
||||
$orderWay = $query->getSortOrder()->toLegacyOrderWay();
|
||||
|
||||
$filterProductSearch = new Filters\Products($facetedSearch);
|
||||
|
||||
// get the product associated with the current filter
|
||||
$productsAndCount = $filterProductSearch->getProductByFilters(
|
||||
$query->getResultsPerPage(),
|
||||
$query->getPage(),
|
||||
$orderBy,
|
||||
$orderWay,
|
||||
$facetedSearchFilters
|
||||
);
|
||||
|
||||
$result
|
||||
->setProducts($productsAndCount['products'])
|
||||
->setTotalProductsCount($productsAndCount['count'])
|
||||
->setAvailableSortOrders($this->getAvailableSortOrders());
|
||||
|
||||
// now get the filter blocks associated with the current search
|
||||
$filterBlockSearch = new Filters\Block(
|
||||
$facetedSearch->getSearchAdapter(),
|
||||
$context,
|
||||
$this->module->getDatabase(),
|
||||
$this->dataAccessor
|
||||
);
|
||||
|
||||
$idShop = (int) $context->shop->id;
|
||||
$idLang = (int) $context->language->id;
|
||||
$idCurrency = (int) $context->currency->id;
|
||||
$idCountry = (int) $context->country->id;
|
||||
$idCategory = (int) $query->getIdCategory();
|
||||
|
||||
$filterHash = md5(
|
||||
sprintf(
|
||||
'%d-%d-%d-%d-%d-%s',
|
||||
$idShop,
|
||||
$idCurrency,
|
||||
$idLang,
|
||||
$idCategory,
|
||||
$idCountry,
|
||||
serialize($facetedSearchFilters)
|
||||
)
|
||||
);
|
||||
|
||||
$filterBlock = $filterBlockSearch->getFromCache($filterHash);
|
||||
if (empty($filterBlock)) {
|
||||
$filterBlock = $filterBlockSearch->getFilterBlock($productsAndCount['count'], $facetedSearchFilters);
|
||||
$filterBlockSearch->insertIntoCache($filterHash, $filterBlock);
|
||||
}
|
||||
|
||||
$facets = $this->filtersConverter->getFacetsFromFilterBlocks(
|
||||
$filterBlock['filters']
|
||||
);
|
||||
|
||||
$this->labelRangeFilters($facets);
|
||||
$this->addEncodedFacetsToFilters($facets);
|
||||
$this->hideUselessFacets($facets, (int) $result->getTotalProductsCount());
|
||||
|
||||
$facetCollection = new FacetCollection();
|
||||
$nextMenu = $facetCollection->setFacets($facets);
|
||||
$result->setFacetCollection($nextMenu);
|
||||
|
||||
$facetFilters = $this->urlSerializer->getActiveFacetFiltersFromFacets($facets);
|
||||
$result->setEncodedFacets($this->urlSerializer->serialize($facetFilters));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an product search result.
|
||||
*
|
||||
* @param ProductSearchContext $context
|
||||
* @param ProductSearchResult $result
|
||||
*
|
||||
* @return string the HTML of the facets
|
||||
*/
|
||||
public function renderFacets(ProductSearchContext $context, ProductSearchResult $result)
|
||||
{
|
||||
list($activeFilters, $displayedFacets, $facetsVar) = $this->prepareActiveFiltersForRender($context, $result);
|
||||
|
||||
// No need to render without facets
|
||||
if (empty($facetsVar)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$this->module->getContext()->smarty->assign(
|
||||
[
|
||||
'show_quantities' => Configuration::get('PS_LAYERED_SHOW_QTIES'),
|
||||
'facets' => $facetsVar,
|
||||
'js_enabled' => $this->module->isAjax(),
|
||||
'displayedFacets' => $displayedFacets,
|
||||
'activeFilters' => $activeFilters,
|
||||
'sort_order' => $result->getCurrentSortOrder()->toString(),
|
||||
'clear_all_link' => $this->updateQueryString(
|
||||
[
|
||||
'q' => null,
|
||||
'page' => null,
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
return $this->module->fetch(
|
||||
'module:ps_facetedsearch/views/templates/front/catalog/facets.tpl'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an product search result of active filters.
|
||||
*
|
||||
* @param ProductSearchContext $context
|
||||
* @param ProductSearchResult $result
|
||||
*
|
||||
* @return string the HTML of the facets
|
||||
*/
|
||||
public function renderActiveFilters(ProductSearchContext $context, ProductSearchResult $result)
|
||||
{
|
||||
list($activeFilters) = $this->prepareActiveFiltersForRender($context, $result);
|
||||
|
||||
$this->module->getContext()->smarty->assign(
|
||||
[
|
||||
'activeFilters' => $activeFilters,
|
||||
'clear_all_link' => $this->updateQueryString(
|
||||
[
|
||||
'q' => null,
|
||||
'page' => null,
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
|
||||
return $this->module->fetch(
|
||||
'module:ps_facetedsearch/views/templates/front/catalog/active-filters.tpl'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare active filters for renderer.
|
||||
*
|
||||
* @param ProductSearchContext $context
|
||||
* @param ProductSearchResult $result
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function prepareActiveFiltersForRender(ProductSearchContext $context, ProductSearchResult $result)
|
||||
{
|
||||
$facetCollection = $result->getFacetCollection();
|
||||
|
||||
// not all search providers generate menus
|
||||
if (empty($facetCollection)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$facetsVar = array_map(
|
||||
[$this, 'prepareFacetForTemplate'],
|
||||
$facetCollection->getFacets()
|
||||
);
|
||||
|
||||
$displayedFacets = [];
|
||||
$activeFilters = [];
|
||||
foreach ($facetsVar as $idx => $facet) {
|
||||
// Remove undisplayed facets
|
||||
if (!empty($facet['displayed'])) {
|
||||
$displayedFacets[] = $facet;
|
||||
}
|
||||
|
||||
// Check if a filter is active
|
||||
foreach ($facet['filters'] as $filter) {
|
||||
if ($filter['active']) {
|
||||
$activeFilters[] = $filter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
$activeFilters,
|
||||
$displayedFacets,
|
||||
$facetsVar,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Facet to an array with all necessary
|
||||
* information for templating.
|
||||
*
|
||||
* @param Facet $facet
|
||||
*
|
||||
* @return array ready for templating
|
||||
*/
|
||||
protected function prepareFacetForTemplate(Facet $facet)
|
||||
{
|
||||
$facetsArray = $facet->toArray();
|
||||
foreach ($facetsArray['filters'] as &$filter) {
|
||||
$filter['facetLabel'] = $facet->getLabel();
|
||||
if ($filter['nextEncodedFacets'] || $facet->getWidgetType() === 'slider') {
|
||||
$filter['nextEncodedFacetsURL'] = $this->updateQueryString([
|
||||
'q' => $filter['nextEncodedFacets'],
|
||||
'page' => null,
|
||||
]);
|
||||
} else {
|
||||
$filter['nextEncodedFacetsURL'] = $this->updateQueryString([
|
||||
'q' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
unset($filter);
|
||||
|
||||
return $facetsArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a label associated with the facets
|
||||
*
|
||||
* @param array $facets
|
||||
*/
|
||||
private function labelRangeFilters(array $facets)
|
||||
{
|
||||
$context = $this->module->getContext();
|
||||
|
||||
foreach ($facets as $facet) {
|
||||
if (!in_array($facet->getType(), Filters\Converter::RANGE_FILTERS)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($facet->getFilters() as $filter) {
|
||||
$filterValue = $filter->getValue();
|
||||
$min = empty($filterValue[0]) ? $facet->getProperty('min') : $filterValue[0];
|
||||
$max = empty($filterValue[1]) ? $facet->getProperty('max') : $filterValue[1];
|
||||
if ($facet->getType() === 'weight') {
|
||||
$unit = Configuration::get('PS_WEIGHT_UNIT');
|
||||
$filter->setLabel(
|
||||
sprintf(
|
||||
'%1$s%2$s - %3$s%4$s',
|
||||
Tools::displayNumber($min),
|
||||
$unit,
|
||||
Tools::displayNumber($max),
|
||||
$unit
|
||||
)
|
||||
);
|
||||
} elseif ($facet->getType() === 'price') {
|
||||
$filter->setLabel(
|
||||
sprintf(
|
||||
'%1$s - %2$s',
|
||||
$context->getCurrentLocale()->formatPrice($min, $context->currency->iso_code),
|
||||
$context->getCurrentLocale()->formatPrice($max, $context->currency->iso_code)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method generates a URL stub for each filter inside the given facets
|
||||
* and assigns this stub to the filters.
|
||||
* The URL stub is called 'nextEncodedFacets' because it is used
|
||||
* to generate the URL of the search once a filter is activated.
|
||||
*/
|
||||
private function addEncodedFacetsToFilters(array $facets)
|
||||
{
|
||||
// first get the currently active facetFilter in an array
|
||||
$originalFacetFilters = $this->urlSerializer->getActiveFacetFiltersFromFacets($facets);
|
||||
|
||||
foreach ($facets as $facet) {
|
||||
$activeFacetFilters = $originalFacetFilters;
|
||||
// If only one filter can be selected, we keep track of
|
||||
// the current active filter to disable it before generating the url stub
|
||||
// and not select two filters in a facet that can have only one active filter.
|
||||
if (!$facet->isMultipleSelectionAllowed() && !$facet->getProperty('range')) {
|
||||
foreach ($facet->getFilters() as $filter) {
|
||||
if ($filter->isActive()) {
|
||||
// we have a currently active filter is the facet, remove it from the facetFilter array
|
||||
$activeFacetFilters = $this->urlSerializer->removeFilterFromFacetFilters(
|
||||
$originalFacetFilters,
|
||||
$filter,
|
||||
$facet
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($facet->getFilters() as $filter) {
|
||||
// toggle the current filter
|
||||
if ($filter->isActive() || $facet->getProperty('range')) {
|
||||
$facetFilters = $this->urlSerializer->removeFilterFromFacetFilters(
|
||||
$activeFacetFilters,
|
||||
$filter,
|
||||
$facet
|
||||
);
|
||||
} else {
|
||||
$facetFilters = $this->urlSerializer->addFilterToFacetFilters(
|
||||
$activeFacetFilters,
|
||||
$filter,
|
||||
$facet
|
||||
);
|
||||
}
|
||||
|
||||
// We've toggled the filter, so the call to serialize
|
||||
// returns the "URL" for the search when user has toggled
|
||||
// the filter.
|
||||
$filter->setNextEncodedFacets(
|
||||
$this->urlSerializer->serialize($facetFilters)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the facet when there's only 1 result.
|
||||
* Keep facet status when it's a slider
|
||||
*
|
||||
* @param array $facets
|
||||
* @param int $totalProducts
|
||||
*/
|
||||
private function hideUselessFacets(array $facets, $totalProducts)
|
||||
{
|
||||
foreach ($facets as $facet) {
|
||||
if ($facet->getWidgetType() === 'slider') {
|
||||
$facet->setDisplayed(
|
||||
$facet->getProperty('min') != $facet->getProperty('max')
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$totalFacetProducts = 0;
|
||||
$usefulFiltersCount = 0;
|
||||
foreach ($facet->getFilters() as $filter) {
|
||||
if ($filter->getMagnitude() > 0 && $filter->isDisplayed()) {
|
||||
$totalFacetProducts += $filter->getMagnitude();
|
||||
++$usefulFiltersCount;
|
||||
}
|
||||
}
|
||||
|
||||
$facet->setDisplayed(
|
||||
// There are two filters displayed
|
||||
$usefulFiltersCount > 1
|
||||
||
|
||||
/*
|
||||
* There is only one fitler and the
|
||||
* magnitude is different than the
|
||||
* total products
|
||||
*/
|
||||
(
|
||||
count($facet->getFilters()) === 1
|
||||
&& $totalFacetProducts < $totalProducts
|
||||
&& $usefulFiltersCount > 0
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a URL corresponding to the current page but
|
||||
* with the query string altered.
|
||||
*
|
||||
* Params from $extraParams that have a null value are stripped,
|
||||
* and other params are added. Params not in $extraParams are unchanged.
|
||||
*/
|
||||
private function updateQueryString(array $extraParams = [])
|
||||
{
|
||||
$uriWithoutParams = explode('?', $_SERVER['REQUEST_URI'])[0];
|
||||
$url = Tools::getCurrentUrlProtocolPrefix() . $_SERVER['HTTP_HOST'] . $uriWithoutParams;
|
||||
$params = [];
|
||||
$paramsFromUri = '';
|
||||
if (strpos($_SERVER['REQUEST_URI'], '?') !== false) {
|
||||
$paramsFromUri = explode('?', $_SERVER['REQUEST_URI'])[1];
|
||||
}
|
||||
parse_str($paramsFromUri, $params);
|
||||
|
||||
foreach ($extraParams as $key => $value) {
|
||||
if (null === $value) {
|
||||
// Force clear param if null value is passed
|
||||
unset($params[$key]);
|
||||
} else {
|
||||
$params[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($params as $key => $param) {
|
||||
if (null === $param || '' === $param) {
|
||||
unset($params[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$queryString = str_replace('%2F', '/', http_build_query($params, '', '&'));
|
||||
|
||||
return $url . ($queryString ? "?$queryString" : '');
|
||||
}
|
||||
}
|
||||
245
modules/ps_facetedsearch/src/URLSerializer.php
Normal file
245
modules/ps_facetedsearch/src/URLSerializer.php
Normal file
@@ -0,0 +1,245 @@
|
||||
<?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;
|
||||
|
||||
use PrestaShop\Module\FacetedSearch\Filters\Converter;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
|
||||
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
|
||||
|
||||
class URLSerializer
|
||||
{
|
||||
/**
|
||||
* Add filter
|
||||
*
|
||||
* @param array $facetFilters
|
||||
* @param Filter $facetFilter
|
||||
* @param Facet $facet
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function addFilterToFacetFilters(array $facetFilters, Filter $facetFilter, Facet $facet)
|
||||
{
|
||||
$facetLabel = $this->getFacetLabel($facet);
|
||||
$filterLabel = $this->getFilterLabel($facetFilter);
|
||||
|
||||
if ($facet->getProperty('range')) {
|
||||
$facetValue = $facet->getProperty('values');
|
||||
$facetFilters[$facetLabel] = [
|
||||
$facetFilter->getProperty('symbol'),
|
||||
isset($facetValue[0]) ? $facetValue[0] : $facet->getProperty('min'),
|
||||
isset($facetValue[1]) ? $facetValue[1] : $facet->getProperty('max'),
|
||||
];
|
||||
} else {
|
||||
$facetFilters[$facetLabel][$filterLabel] = $filterLabel;
|
||||
}
|
||||
|
||||
return $facetFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove filter
|
||||
*
|
||||
* @param array $facetFilters
|
||||
* @param Filter $facetFilter
|
||||
* @param Facet $facet
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function removeFilterFromFacetFilters(array $facetFilters, Filter $facetFilter, $facet)
|
||||
{
|
||||
$facetLabel = $this->getFacetLabel($facet);
|
||||
|
||||
if ($facet->getProperty('range')) {
|
||||
unset($facetFilters[$facetLabel]);
|
||||
} else {
|
||||
$filterLabel = $this->getFilterLabel($facetFilter);
|
||||
unset($facetFilters[$facetLabel][$filterLabel]);
|
||||
if (empty($facetFilters[$facetLabel])) {
|
||||
unset($facetFilters[$facetLabel]);
|
||||
}
|
||||
}
|
||||
|
||||
return $facetFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active facet filters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getActiveFacetFiltersFromFacets(array $facets)
|
||||
{
|
||||
$facetFilters = [];
|
||||
foreach ($facets as $facet) {
|
||||
foreach ($facet->getFilters() as $facetFilter) {
|
||||
if (!$facetFilter->isActive()) {
|
||||
// Filter is not active
|
||||
continue;
|
||||
}
|
||||
|
||||
$facetLabel = $this->getFacetLabel($facet);
|
||||
$filterLabel = $this->getFilterLabel($facetFilter);
|
||||
if (!$facet->getProperty('range')) {
|
||||
$facetFilters[$facetLabel][$filterLabel] = $filterLabel;
|
||||
continue;
|
||||
}
|
||||
|
||||
$facetValue = $facetFilter->getValue();
|
||||
$facetFilters[$facetLabel] = [
|
||||
$facetFilter->getProperty('symbol'),
|
||||
$facetValue[0],
|
||||
$facetValue[1],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $facetFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Facet label
|
||||
*
|
||||
* @param Facet $facet
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getFacetLabel(Facet $facet)
|
||||
{
|
||||
if ($facet->getProperty(Converter::PROPERTY_URL_NAME) !== null) {
|
||||
return $facet->getProperty(Converter::PROPERTY_URL_NAME);
|
||||
}
|
||||
|
||||
return $facet->getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Facet Filter label
|
||||
*
|
||||
* @param Filter $facetFilter
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getFilterLabel(Filter $facetFilter)
|
||||
{
|
||||
if ($facetFilter->getProperty(Converter::PROPERTY_URL_NAME) !== null) {
|
||||
return $facetFilter->getProperty(Converter::PROPERTY_URL_NAME);
|
||||
}
|
||||
|
||||
return $facetFilter->getLabel();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $fragment
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function serialize(array $fragment)
|
||||
{
|
||||
$parts = [];
|
||||
foreach ($fragment as $key => $values) {
|
||||
array_unshift($values, $key);
|
||||
$parts[] = $this->serializeListOfStrings($values, '-');
|
||||
}
|
||||
|
||||
return $this->serializeListOfStrings($parts, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function unserialize($string)
|
||||
{
|
||||
$fragment = [];
|
||||
$parts = $this->unserializeListOfStrings($string, '/');
|
||||
foreach ($parts as $part) {
|
||||
$values = $this->unserializeListOfStrings($part, '-');
|
||||
$key = array_shift($values);
|
||||
$fragment[$key] = $values;
|
||||
}
|
||||
|
||||
return $fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $separator the string separator
|
||||
* @param string $escape the string escape
|
||||
* @param array $list
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function serializeListOfStrings($list, $separator, $escape = '\\')
|
||||
{
|
||||
return implode($separator, array_map(function ($item) use ($separator, $escape) {
|
||||
return strtr(
|
||||
$item,
|
||||
[
|
||||
$separator => $escape . $separator,
|
||||
]
|
||||
);
|
||||
}, $list));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $separator the string separator
|
||||
* @param string $escape the string escape
|
||||
* @param string $string the UTF8 string
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function unserializeListOfStrings($string, $separator, $escape = '\\')
|
||||
{
|
||||
$list = [];
|
||||
$currentString = '';
|
||||
$escaping = false;
|
||||
|
||||
// get UTF-8 chars, inspired from http://stackoverflow.com/questions/9438158/split-utf8-string-into-array-of-chars
|
||||
$arrayOfCharacters = [];
|
||||
preg_match_all('/./u', $string, $arrayOfCharacters);
|
||||
$characters = $arrayOfCharacters[0];
|
||||
|
||||
foreach ($characters as $index => $character) {
|
||||
if ($character === $escape
|
||||
&& isset($characters[$index + 1])
|
||||
&& $characters[$index + 1] === $separator
|
||||
) {
|
||||
$escaping = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($character === $separator && $escaping === false) {
|
||||
$list[] = $currentString;
|
||||
$currentString = '';
|
||||
continue;
|
||||
}
|
||||
|
||||
$currentString .= $character;
|
||||
$escaping = false;
|
||||
}
|
||||
|
||||
if ('' !== $currentString) {
|
||||
$list[] = $currentString;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user