Initial commit

This commit is contained in:
2019-11-20 07:44:43 +01:00
commit 5bf49c4a81
41188 changed files with 5459177 additions and 0 deletions

View File

@@ -0,0 +1,330 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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;
}
/**
* {@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 = $adapter->getFilters();
}
/**
* {@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;
}
}

View File

@@ -0,0 +1,300 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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);
/**
* 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();
}

View File

@@ -0,0 +1,698 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Adapter;
use Db;
use Context;
use StockAvailable;
use Doctrine\Common\Collections\ArrayCollection;
class MySQL extends AbstractAdapter
{
/**
* @var string
*/
const TYPE = 'MySQL';
/**
* @var string
*/
const LEFT_JOIN = 'LEFT JOIN';
/**
* @var string
*/
const INNER_JOIN = 'INNER JOIN';
/**
* @var string
*/
const STRAIGHT_JOIN = 'STRAIGHT_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() . ')';
}
if (empty($this->getSelectFields())
&& empty($this->getFilters())
&& empty($this->getGroupFields())
&& empty($this->getOrderField())
) {
// avoid adding an extra SELECT FROM (SELECT ...) if it's not needed
$query = $referenceTable;
$this->setOrderField('');
} else {
$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 ($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::STRAIGHT_JOIN,
],
'id_attribute' => [
'tableName' => 'product_attribute_combination',
'tableAlias' => 'pac',
'joinCondition' => '(pa.id_product_attribute = pac.id_product_attribute)',
'joinType' => self::STRAIGHT_JOIN,
'dependencyField' => 'id_product_attribute',
],
'id_attribute_group' => [
'tableName' => 'attribute',
'tableAlias' => 'a',
'joinCondition' => '(a.id_attribute = pac.id_attribute)',
'joinType' => self::STRAIGHT_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 0 = sa.id_product_attribute ' .
$stockCondition . ')',
'joinType' => self::LEFT_JOIN,
],
'quantity' => [
'tableName' => 'stock_available',
'tableAlias' => 'sa',
'joinCondition' => '(p.id_product = sa.id_product AND 0 = sa.id_product_attribute ' .
$stockCondition . ')',
'joinType' => self::LEFT_JOIN,
],
'price_min' => [
'tableName' => 'layered_price_index',
'tableAlias' => 'psi',
'joinCondition' => '(psi.id_product = p.id_product 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_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_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_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',
],
];
return $filterToTableMapping;
}
/**
* Compute the orderby fields, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return string
*/
private 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';
}
if (array_key_exists($orderField, $filterToTableMapping)
&& (
// If the requested order field is in the result, no need to change tableAlias
// unless a fieldName key exists
isset($filterToTableMapping[$orderField]['fieldName'])
|| $this->getInitialPopulation() === null
|| !$this->getInitialPopulation()->getSelectFields()->contains($orderField)
)
) {
$joinMapping = $filterToTableMapping[$orderField];
$orderField = $joinMapping['tableAlias'] . '.' . (isset($joinMapping['fieldName']) ? $joinMapping['fieldName'] : $orderField);
} else {
$orderField = 'p.' . $orderField;
}
return $orderField;
}
/**
* Compute the select fields, adding the proper alias that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
private function computeSelectFields(array $filterToTableMapping)
{
$selectFields = [];
foreach ($this->getSelectFields() as $key => $selectField) {
$selectAlias = 'p';
if (array_key_exists($selectField, $filterToTableMapping)) {
$joinMapping = $filterToTableMapping[$selectField];
$selectAlias = $joinMapping['tableAlias'];
if (isset($joinMapping['fieldName'])) {
$selectField = $joinMapping['fieldName'];
}
}
if (strpos($selectField, '(') !== false) {
$selectFields[] = $selectField;
} else {
$selectFields[] = $selectAlias . '.' . $selectField;
}
}
return $selectFields;
}
/**
* Computer the where conditions that will be added to the final query
*
* @param array $filterToTableMapping
*
* @return array
*/
private function computeWhereConditions(array $filterToTableMapping)
{
$whereConditions = [];
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'] . ($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 (' . implode(', ', array_map(function ($value) {
return is_numeric($value) ? pSQL($value) : "'" . pSQL($value) . "'";
}, $values)) . ')';
}
}
$operationsConditions[] = '(' . implode(' AND ', $conditions) . ')';
}
$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 (' . implode(', ', array_map(function ($value) {
return is_numeric($value) ? pSQL($value) : "'" . pSQL($value) . "'";
}, $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
*/
private function computeJoinConditions(array $filterToTableMapping)
{
$joinList = new ArrayCollection();
$this->addJoinList($joinList, $this->getSelectFields(), $filterToTableMapping);
$this->addJoinList($joinList, $this->getFilters()->getKeys(), $filterToTableMapping);
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) {
// Index is not the first, append index to tableAlias on joinCondition
$joinMapping['joinCondition'] = preg_replace(
'~([\(\s=]' . $joinMapping['tableAlias'] . ')\.~',
'${1}' . $idx . '.',
$joinMapping['joinCondition']
);
$joinMapping['tableAlias'] .= $idx;
}
$this->addJoinConditions($joinList, $joinMapping, $filterToTableMapping);
}
}
}
}
$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 (empty($this->getGroupFields())) {
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);
$mysqlAdapter->setSelectFields(['COUNT(DISTINCT p.id_product) c']);
$mysqlAdapter->setLimit(null);
$mysqlAdapter->setOrderField('');
$result = $mysqlAdapter->execute();
return isset($result[0]['c']) ? $result[0]['c'] : 0;
}
/**
* {@inheritdoc}
*/
public function valueCount($fieldName)
{
$this->resetGroupBy();
$this->addGroupBy($fieldName);
$this->addSelectField($fieldName);
$this->addSelectField('COUNT(DISTINCT p.id_product) c');
$this->setLimit(null);
$this->setOrderField('');
return $this->execute();
}
/**
* {@inheritdoc}
*/
public function useFiltersAsInitialPopulation()
{
$this->setLimit(null);
$this->setOrderField('');
$this->setSelectFields(
[
'id_product',
'id_manufacturer',
'quantity',
'condition',
'weight',
'price',
]
);
$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();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,377 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use AttributeGroup;
use Category;
use Configuration;
use Context;
use Db;
use Feature;
use FeatureValue;
use Manufacturer;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
use Tools;
class Converter
{
const WIDGET_TYPE_CHECKBOX = 0;
const WIDGET_TYPE_RADIO = 1;
const WIDGET_TYPE_DROPDOWN = 2;
const WIDGET_TYPE_SLIDER = 3;
/**
* @var Context
*/
protected $context;
/**
* @var Db
*/
protected $database;
public function __construct(Context $context, Db $database)
{
$this->context = $context;
$this->database = $database;
}
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'])
->setMultipleSelectionAllowed(true);
switch ($filterBlock['type']) {
case 'category':
case 'quantity':
case 'condition':
case 'manufacturer':
case 'id_attribute_group':
case 'id_feature':
$type = $filterBlock['type'];
if ($filterBlock['type'] == 'quantity') {
$type = 'availability';
} elseif ($filterBlock['type'] == 'id_attribute_group') {
$type = 'attribute_group';
$facet->setProperty('id_attribute_group', $filterBlock['id_key']);
} elseif ($filterBlock['type'] == 'id_feature') {
$type = 'feature';
$facet->setProperty('id_feature', $filterBlock['id_key']);
}
$facet->setType($type);
foreach ($filterBlock['values'] as $id => $filterArray) {
$filter = new Filter();
$filter
->setType($type)
->setLabel($filterArray['name'])
->setMagnitude($filterArray['nbr'])
->setValue($id);
if (array_key_exists('checked', $filterArray)) {
$filter->setActive($filterArray['checked']);
}
if (isset($filterArray['color']) && $filterArray['color'] != '') {
$filter->setProperty('color', $filterArray['color']);
}
if (isset($filterArray['url_name']) && $filterArray['url_name'] != '') {
$filter->setProperty('texture', _THEME_COL_DIR_ . $id . '.jpg');
}
$facet->addFilter($filter);
}
break;
case 'weight':
case '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'
);
$urlSerializer = new URLFragmentSerializer();
$facetAndFiltersLabels = $urlSerializer->unserialize($query->getEncodedFacets());
foreach ($filters as $filter) {
$filterLabel = $this->convertFilterTypeToLabel($filter['type']);
switch ($filter['type']) {
case '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 '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(
'In stock',
[],
'Modules.Facetedsearch.Shop'
) => 1,
];
$searchFilters[$filter['type']] = [];
foreach ($quantityArray as $quantityName => $quantityId) {
if (isset($facetAndFiltersLabels[$filterLabel]) && in_array($quantityName, $facetAndFiltersLabels[$filterLabel])) {
$searchFilters[$filter['type']][] = $quantityId;
}
}
break;
case '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 'id_feature':
$features = Feature::getFeatures($idLang);
foreach ($features as $feature) {
if ($filter['id_value'] == $feature['id_feature']
&& isset($facetAndFiltersLabels[$feature['name']])
) {
$featureValueLabels = $facetAndFiltersLabels[$feature['name']];
$featureValues = FeatureValue::getFeatureValuesWithLang($idLang, $feature['id_feature']);
foreach ($featureValues as $featureValue) {
if (in_array($featureValue['value'], $featureValueLabels)) {
$searchFilters['id_feature'][$feature['id_feature']][] =
$featureValue['id_feature_value'];
}
}
}
}
break;
case 'id_attribute_group':
$attributesGroup = AttributeGroup::getAttributesGroups($idLang);
foreach ($attributesGroup as $attributeGroup) {
if ($filter['id_value'] == $attributeGroup['id_attribute_group']
&& isset($facetAndFiltersLabels[$attributeGroup['name']])
) {
$attributeLabels = $facetAndFiltersLabels[$attributeGroup['name']];
$attributes = AttributeGroup::getAttributes($idLang, $attributeGroup['id_attribute_group']);
foreach ($attributes as $attribute) {
if (in_array($attribute['name'], $attributeLabels)) {
$searchFilters['id_attribute_group'][$attributeGroup['id_attribute_group']][] = $attribute['id_attribute'];
}
}
}
}
break;
case 'price':
case '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 'category':
if (isset($facetAndFiltersLabels[$filterLabel])) {
foreach ($facetAndFiltersLabels[$filterLabel] as $queryFilter) {
$categories = Category::searchByNameAndParentCategoryId($idLang, $queryFilter, $idParent);
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 'price':
case 'weight':
if ($value[0] === '' && $value[1] === '') {
unset($searchFilters[$key]);
}
break;
default:
if ($value == '' || $value == []) {
unset($searchFilters[$key]);
}
break;
}
}
return $searchFilters;
}
private function convertFilterTypeToLabel($filterType)
{
switch ($filterType) {
case 'price':
return $this->context->getTranslator()->trans('Price', [], 'Modules.Facetedsearch.Shop');
case 'weight':
return $this->context->getTranslator()->trans('Weight', [], 'Modules.Facetedsearch.Shop');
case 'condition':
return $this->context->getTranslator()->trans('Condition', [], 'Modules.Facetedsearch.Shop');
case 'quantity':
return $this->context->getTranslator()->trans('Availability', [], 'Modules.Facetedsearch.Shop');
case 'manufacturer':
return $this->context->getTranslator()->trans('Brand', [], 'Modules.Facetedsearch.Shop');
case 'category':
return $this->context->getTranslator()->trans('Categories', [], 'Modules.Facetedsearch.Shop');
case 'id_feature':
case 'id_attribute_group':
default:
return null;
}
}
}

View File

@@ -0,0 +1,174 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Filters;
use PrestaShop\Module\FacetedSearch\Product\Search;
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
use Product;
use Validate;
use Configuration;
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]);
}
}
}
}
}

View File

@@ -0,0 +1,66 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Db;
use Context;
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;
}
}

View File

@@ -0,0 +1,131 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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']);
if (empty($seoUrl)) {
$seoUrl = Tools::getValue('name_' . (int) $language['id_lang']);
}
$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(Tools::getValue('meta_title_' . (int) $language['id_lang']), 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');
}
}

View File

@@ -0,0 +1,156 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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']);
if (empty($seoUrl)) {
$seoUrl = Tools::getValue('name_' . (int) $language['id_lang']);
}
$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(Tools::getValue('meta_title_' . (int) $language['id_lang']), 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']
);
// Request failed, force $isIndexable
if ($isIndexable === false) {
$isIndexable = true;
}
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');
}
}

View File

@@ -0,0 +1,103 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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();
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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');
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use Tools;
use Language;
class Feature extends AbstractHook
{
const AVAILABLE_HOOKS = [
'actionFeatureSave',
'actionFeatureDelete',
'displayFeatureForm',
'displayFeaturePostProcess',
];
/**
* 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)
{
$values = [];
$isIndexable = $this->database->getValue(
'SELECT `indexable`
FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
WHERE `id_feature` = ' . (int) $params['id_feature']
);
// Request failed, force $isIndexable
if ($isIndexable === false) {
$isIndexable = true;
}
if ($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']
)) {
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;
}
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature
WHERE `id_feature` = ' . (int) $params['id_feature']
);
$this->database->execute(
'DELETE FROM ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value
WHERE `id_feature` = ' . (int) $params['id_feature']
);
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature
(`id_feature`, `indexable`)
VALUES (' . (int) $params['id_feature'] . ', ' . (int) Tools::getValue('layered_indexable') . ')'
);
foreach (Language::getLanguages(false) as $language) {
$seoUrl = Tools::getValue('url_name_' . (int) $language['id_lang']);
if (empty($seoUrl)) {
$seoUrl = Tools::getValue('name_' . (int) $language['id_lang']);
}
$this->database->execute(
'INSERT INTO ' . _DB_PREFIX_ . 'layered_indexable_feature_lang_value
(`id_feature`, `id_lang`, `url_name`, `meta_title`)
VALUES (
' . (int) $params['id_feature'] . ', ' . (int) $language['id_lang'] . ',
\'' . pSQL(Tools::link_rewrite($seoUrl)) . '\',
\'' . pSQL(Tools::getValue('meta_title_' . (int) $language['id_lang']), true) . '\')'
);
}
$this->module->invalidateLayeredFilterBlockCache();
}
}

View File

@@ -0,0 +1,135 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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']);
if (empty($seoUrl)) {
$seoUrl = Tools::getValue('name_' . (int) $language['id_lang']);
}
$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(Tools::getValue('meta_title_' . (int) $language['id_lang']), 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');
}
}

View File

@@ -0,0 +1,50 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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();
}
}

View File

@@ -0,0 +1,78 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Hook;
use PrestaShop\Module\FacetedSearch\Product\SearchProvider;
use PrestaShop\Module\FacetedSearch\URLSerializer;
use PrestaShop\Module\FacetedSearch\Filters\Converter;
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]
);
return new SearchProvider(
$this->module,
new Converter(
$this->module->getContext(),
$this->module->getDatabase()
),
new URLSerializer()
);
}
return null;
}
}

View File

@@ -0,0 +1,117 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
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\Design::class,
Hook\Feature::class,
Hook\FeatureValue::class,
Hook\Product::class,
Hook\ProductSearch::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);
}
}

View File

@@ -0,0 +1,288 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use PrestaShop\Module\FacetedSearch\Adapter\MySQL as MySQLAdapter;
use PrestaShop\Module\FacetedSearch\Adapter\AbstractAdapter;
use Configuration;
use Tools;
use Category;
use Context;
class Search
{
/**
* @var bool
*/
private $psStockManagement;
/**
* @var bool
*/
private $psOrderOutOfStock;
/**
* @var AbstractAdapter
*/
private $searchAdapter;
/**
* @var Context
*/
private $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_default', [$parent->id]);
}
// Visibility of a product must be in catalog or both (search & catalog)
$this->addFilter('visibility', ['both', 'catalog']);
$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 $filterValue) {
$operationsFilter[] = ['id_feature_value', $filterValue];
}
$this->getSearchAdapter()->addOperationsFilter(
'with_features',
[$operationsFilter]
);
break;
case 'id_attribute_group':
$operationsFilter = [];
foreach ($filterValues as $filterValue) {
$operationsFilter[] = ['id_attribute', $filterValue];
}
$this->getSearchAdapter()->addOperationsFilter(
'with_attributes',
[$operationsFilter]
);
break;
case 'category':
$this->addFilter('id_category', $filterValues);
$this->getSearchAdapter()->resetFilter('id_category_default');
$hasCategory = true;
break;
case 'quantity':
if (count($selectedFilters['quantity']) == 2) {
break;
}
if (!$this->psStockManagement) {
$this->getSearchAdapter()->addFilter('quantity', [0], (!$filterValues[0] ? '<=' : '>'));
break;
}
$operationsFilter = [];
if ($filterValues[0]) {
// Filter for available quantity, we must be able to request
// product with out_of_stock at 1 or 2
// which mean we can buy out of stock products
$operationsFilter[] = [
['quantity', [0], '>='],
['out_of_stock', [1], $this->psOrderOutOfStock ? '>=' : '='],
];
$operationsFilter[] = [
['quantity', [0], '>'],
];
} else {
$operationsFilter[] = [
['quantity', [0], '<='],
['out_of_stock', !$this->psOrderOutOfStock ? [0, 2] : [0], '='],
];
}
$this->getSearchAdapter()->addOperationsFilter(
'with_stock_management',
$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', [$minPrice], '>=');
$this->getSearchAdapter()->addFilter('price_max', [$maxPrice], '<=');
}
}

View File

@@ -0,0 +1,505 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch\Product;
use Configuration;
use Context;
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 PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
use Ps_Facetedsearch;
use Tools;
class SearchProvider implements FacetsRendererInterface, ProductSearchProviderInterface
{
/**
* @var Ps_Facetedsearch
*/
private $module;
/**
* @var Filters\Converter
*/
private $filtersConverter;
/**
* @var URLSerializer
*/
private $facetsSerializer;
public function __construct(
Ps_Facetedsearch $module,
Filters\Converter $converter,
URLSerializer $serializer
) {
$this->module = $module;
$this->filtersConverter = $converter;
$this->facetsSerializer = $serializer;
}
/**
* @return array
*/
private function getAvailableSortOrders()
{
$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 [
$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 = new Search($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()
);
$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->hideZeroValues($facets);
$this->hideUselessFacets($facets);
$facetCollection = new FacetCollection();
$nextMenu = $facetCollection->setFacets($facets);
$result->setFacetCollection($nextMenu);
$result->setEncodedFacets($this->facetsSerializer->serialize($facets));
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 '';
}
return $this->module->render(
'views/templates/front/catalog/facets.tpl',
[
'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,
]
),
]
);
}
/**
* 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);
return $this->module->render(
'views/templates/front/catalog/active-filters.tpl',
[
'activeFilters' => $activeFilters,
'clear_all_link' => $this->updateQueryString(
[
'q' => null,
'page' => null,
]
),
]
);
}
/**
* 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']) {
$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)
{
foreach ($facets as $facet) {
if ($facet->getType() === 'weight') {
$unit = Configuration::get('PS_WEIGHT_UNIT');
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];
$filter->setLabel(
sprintf(
'%1$s%2$s - %3$s%4$s',
Tools::displayNumber($min),
$unit,
Tools::displayNumber($max),
$unit
)
);
}
} elseif ($facet->getType() === 'price') {
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];
$filter->setLabel(
sprintf(
'%1$s - %2$s',
Tools::displayPrice($min),
Tools::displayPrice($max)
)
);
}
}
}
}
/**
* 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->facetsSerializer->getActiveFacetFiltersFromFacets($facets);
$urlSerializer = new URLFragmentSerializer();
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->facetsSerializer->removeFilterFromFacetFilters(
$originalFacetFilters,
$filter,
$facet
);
break;
}
}
}
foreach ($facet->getFilters() as $filter) {
// toggle the current filter
if ($filter->isActive() || $facet->getProperty('range')) {
$facetFilters = $this->facetsSerializer->removeFilterFromFacetFilters(
$activeFacetFilters,
$filter,
$facet
);
} else {
$facetFilters = $this->facetsSerializer->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(
$urlSerializer->serialize($facetFilters)
);
}
}
}
/**
* Hide entries with 0 results
*
* @param array $facets
*/
private function hideZeroValues(array $facets)
{
foreach ($facets as $facet) {
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() === 0) {
$filter->setDisplayed(false);
}
}
}
}
/**
* Remove the facet when there's only 1 result.
* Keep facet status when it's a slider
*
* @param array $facets
*/
private function hideUselessFacets(array $facets)
{
foreach ($facets as $facet) {
if ($facet->getWidgetType() === 'slider') {
$facet->setDisplayed(
$facet->getProperty('min') != $facet->getProperty('max')
);
continue;
}
$usefulFiltersCount = 0;
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() > 0) {
++$usefulFiltersCount;
}
}
$facet->setDisplayed(
$usefulFiltersCount > 1
);
}
}
/**
* 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" : '');
}
}

View File

@@ -0,0 +1,110 @@
<?php
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
class Ps_FacetedsearchFacetsURLSerializer
{
public function addFilterToFacetFilters(array $facetFilters, Filter $facetFilter, $facet) {
if ($facet->getProperty('range')) {
$facetFilters[$facet->getLabel()] = [
$facetFilter->getProperty('symbol'),
$facetFilter->getValue()['from'],
$facetFilter->getValue()['to'],
];
} else {
$facetFilters[$facet->getLabel()][$facetFilter->getLabel()] = $facetFilter->getLabel();
}
return $facetFilters;
}
public function removeFilterFromFacetFilters(array $facetFilters, Filter $facetFilter, $facet) {
if ($facet->getProperty('range')) {
unset($facetFilters[$facet->getLabel()]);
} else {
unset($facetFilters[$facet->getLabel()][$facetFilter->getLabel()]);
if (empty($facetFilters[$facet->getLabel()])) {
unset($facetFilters[$facet->getLabel()]);
}
}
return $facetFilters;
}
public function getActiveFacetFiltersFromFacets(array $facets) {
$facetFilters = [];
foreach ($facets as $facet) {
if ($facet->getProperty('range')) {
foreach ($facet->getFilters() as $facetFilter) {
if ($facetFilter->isActive()) {
$facetFilters[$facet->getLabel()] = [
$facetFilter->getProperty('symbol'),
$facetFilter->getValue()['from'],
$facetFilter->getValue()['to'],
];
}
}
} else {
foreach ($facet->getFilters() as $facetFilter) {
if ($facetFilter->isActive()) {
$facetFilters[$facet->getLabel()][$facetFilter->getLabel()] = $facetFilter->getLabel();
}
}
}
}
return $facetFilters;
}
public function serialize(array $facets)
{
$facetFilters = $this->getActiveFacetFiltersFromFacets($facets);
$urlSerializer = new URLFragmentSerializer();
return $urlSerializer->serialize($facetFilters);
}
public function setFiltersFromEncodedFacets(array $facets, $encodedFacets)
{
$urlSerializer = new URLFragmentSerializer();
$facetAndFiltersLabels = $urlSerializer->unserialize($encodedFacets);
foreach ($facetAndFiltersLabels as $facetLabel => $filters) {
foreach ($facets as $facet) {
if ($facet->getLabel() === $facetLabel) {
if (true === $facet->getProperty('range')) {
$symbol = $filters[0];
$from = $filters[1];
$to = $filters[2];
$found = false;
foreach ($facet->getFilters() as $filter) {
if ($from >= $filter->getValue()['from'] && $to <= $filter->getValue()['to']) {
$filter->setActive(true);
$found = true;
}
}
if (!$found) {
$filter = new Filter();
$filter->setValue([
'from' => $from,
'to' => $to,
])->setProperty('symbol', $symbol);
$filter->setActive(true);
$facet->addFilter($filter);
}
} else {
foreach ($filters as $filterLabel) {
foreach ($facet->getFilters() as $filter) {
if ($filter->getLabel() === $filterLabel) {
$filter->setActive(true);
}
}
}
}
}
}
}
return $facets;
}
}

View File

@@ -0,0 +1,169 @@
<?php
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
class Ps_FacetedsearchFiltersConverter
{
public function getFacetsFromFacetedSearchFilters(array $facetedSearchFilters)
{
$facets = [];
foreach ($facetedSearchFilters as $facetArray) {
$facet = new Facet();
$facet
->setLabel($facetArray['name'])
->setMultipleSelectionAllowed(true)
;
switch ($facetArray['type']) {
case 'category':
case 'quantity':
case 'condition':
case 'manufacturer':
case 'id_attribute_group':
case 'id_feature':
$type = $facetArray['type'];
if ($facetArray['type'] == 'quantity') {
$type = 'availability';
} elseif ($facetArray['type'] == 'id_attribute_group') {
$type = 'attribute_group';
$facet->setProperty('id_attribute_group', $facetArray['id_key']);
} elseif ($facetArray['type'] == 'id_feature') {
$type = 'feature';
$facet->setProperty('id_feature', $facetArray['id_key']);
}
$facet->setType($type);
foreach ($facetArray['values'] as $id => $filterArray) {
$filter = new Filter();
$filter
->setType($type)
->setLabel($filterArray['name'])
->setMagnitude($filterArray['nbr'])
->setValue($id)
;
if (isset($facetArray['is_color_group']) && $facetArray['is_color_group']){
if (isset($filterArray['color']) && $filterArray['color'] != '') {
$filter->setProperty('color', $filterArray['color']);
}
if (isset($filterArray['url_name']) && $filterArray['url_name'] != '') {
$filter->setProperty('texture', _THEME_COL_DIR_.$id.'.jpg');
}
}
$facet->addFilter($filter);
}
break;
case 'weight':
case 'price':
$facet
->setType($facetArray['type'])
->setProperty('min', $facetArray['min'])
->setProperty('max', $facetArray['max'])
->setMultipleSelectionAllowed(false)
->setProperty('range', true)
;
foreach ($facetArray['list_of_values'] as $value) {
$filter = new Filter();
$filter
->setType($facetArray['type'])
->setMagnitude($value['nbr'])
->setProperty('symbol', $facetArray['unit'])
->setValue([
'from' => $value[0],
'to' => $value[1],
])
;
$facet->addFilter($filter);
}
break;
}
switch ((int) $facetArray['filter_type']) {
case 0: // checkbox
$facet->setMultipleSelectionAllowed(true);
$facet->setWidgetType('checkboxes');
break;
case 1: // radio
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('radio-buttons');
break;
case 2: // drop down
$facet->setMultipleSelectionAllowed(false);
$facet->setWidgetType('dropdown');
break;
}
$facets[] = $facet;
}
return $facets;
}
/**
* WARNING, this is not the inverse function of
* getFacetsFromFacetedSearchFilters
* because facetedsearch doesn't use the same representation
* of filters in input as in output.
* It is close to the inverse function for our use though, hence the name.
*/
public function getFacetedSearchFiltersFromFacets(array $facets)
{
$facetedSearchFilters = [];
foreach ($facets as $facet) {
switch ($facet->getType()) {
case 'category':
case 'availability':
case 'condition':
case 'manufacturer':
case 'attribute_group':
case 'feature':
$type = $facet->getType();
if ($type === 'availability') {
$type = 'quantity';
} elseif ($type === 'attribute_group') {
$type = 'id_attribute_group';
} elseif ($type === 'feature') {
$type = 'id_feature';
}
if (!isset($facetedSearchFilters[$type])) {
$facetedSearchFilters[$type] = [];
}
foreach ($facet->getFilters() as $filter) {
if (!$filter->isActive()) {
continue;
}
$key = count($facetedSearchFilters[$type]);
$value = $filter->getValue();
if ($type === 'id_attribute_group') {
$key = $value;
$value = $facet->getProperty('id_attribute_group').'_'.$filter->getValue();
}
if ($type === 'id_feature') {
$key = $value;
$value = $facet->getProperty('id_feature').'_'.$filter->getValue();
}
$facetedSearchFilters[$type][$key] = $value;
}
break;
case 'weight':
case 'price':
foreach ($facet->getFilters() as $filter) {
if (!$filter->isActive()) {
continue;
}
$facetedSearchFilters[$facet->getType()] = [
$filter->getValue()['from'],
$filter->getValue()['to'],
];
break;
}
break;
}
}
return $facetedSearchFilters;
}
}

View File

@@ -0,0 +1,292 @@
<?php
require_once __DIR__.DIRECTORY_SEPARATOR.'Ps_FacetedsearchFiltersConverter.php';
require_once __DIR__.DIRECTORY_SEPARATOR.'Ps_FacetedsearchFacetsURLSerializer.php';
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchContext;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchQuery;
use PrestaShop\PrestaShop\Core\Product\Search\ProductSearchResult;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
use PrestaShop\PrestaShop\Core\Product\Search\SortOrder;
class Ps_FacetedsearchProductSearchProvider implements ProductSearchProviderInterface
{
private $module;
private $filtersConverter;
private $facetsSerializer;
public function __construct(Ps_Facetedsearch $module)
{
$this->module = $module;
$this->filtersConverter = new Ps_FacetedsearchFiltersConverter();
$this->facetsSerializer = new Ps_FacetedsearchFacetsURLSerializer();
}
public function getFacetCollectionFromEncodedFacets(
ProductSearchQuery $query
) {
// do not compute range filters, all info we need is encoded in $encodedFacets
$compute_range_filters = false;
$filterBlock = $this->module->getFilterBlock(
[],
$compute_range_filters
);
$queryTemplate = $this->filtersConverter->getFacetsFromFacetedSearchFilters(
$filterBlock['filters']
);
$facets = $this->facetsSerializer->setFiltersFromEncodedFacets(
$queryTemplate,
$query->getEncodedFacets()
);
return (new FacetCollection())->setFacets($facets);
}
private function copyFiltersActiveState(
array $sourceFacets,
array $targetFacets
) {
$copyByLabel = function (Facet $source, Facet $target) {
foreach ($target->getFilters() as $targetFilter) {
foreach ($source->getFilters() as $sourceFilter) {
if ($sourceFilter->getLabel() === $targetFilter->getLabel()) {
$targetFilter->setActive($sourceFilter->isActive());
break;
}
}
}
};
$copyByRangeValue = function (Facet $source, Facet $target) {
foreach ($source->getFilters() as $sourceFilter) {
if ($sourceFilter->isActive()) {
$foundRange = false;
foreach ($target->getFilters() as $targetFilter) {
$tFrom = $targetFilter->getValue()['from'];
$tTo = $targetFilter->getValue()['to'];
$sFrom = $sourceFilter->getValue()['from'];
$sTo = $sourceFilter->getValue()['to'];
if ($tFrom <= $sFrom && $sTo <= $tTo) {
$foundRange = true;
$targetFilter->setActive(true);
break;
}
}
if (!$foundRange) {
$filter = clone $sourceFilter;
$filter->setDisplayed(false);
$target->addFilter($filter);
}
break;
}
}
};
$copy = function (
Facet $source,
Facet $target
) use (
$copyByLabel,
$copyByRangeValue
) {
if ($target->getProperty('range')) {
$strategy = $copyByRangeValue;
} else {
$strategy = $copyByLabel;
}
$strategy($source, $target);
};
foreach ($targetFacets as $targetFacet) {
foreach ($sourceFacets as $sourceFacet) {
if ($sourceFacet->getLabel() === $targetFacet->getLabel()) {
$copy($sourceFacet, $targetFacet);
break;
}
}
}
}
private function getAvailableSortOrders()
{
return [
(new SortOrder('product', 'position', 'asc'))->setLabel(
$this->module->getTranslator()->trans('Relevance', array(), 'Modules.Facetedsearch.Shop')
),
(new SortOrder('product', 'name', 'asc'))->setLabel(
$this->module->getTranslator()->trans('Name, A to Z', array(), 'Shop.Theme.Catalog')
),
(new SortOrder('product', 'name', 'desc'))->setLabel(
$this->module->getTranslator()->trans('Name, Z to A', array(), 'Shop.Theme.Catalog')
),
(new SortOrder('product', 'price', 'asc'))->setLabel(
$this->module->getTranslator()->trans('Price, low to high', array(), 'Shop.Theme.Catalog')
),
(new SortOrder('product', 'price', 'desc'))->setLabel(
$this->module->getTranslator()->trans('Price, high to low', array(), 'Shop.Theme.Catalog')
),
];
}
public function runQuery(
ProductSearchContext $context,
ProductSearchQuery $query
) {
$result = new ProductSearchResult();
$menu = $this->getFacetCollectionFromEncodedFacets($query);
$order_by = $query->getSortOrder()->toLegacyOrderBy(true);
$order_way = $query->getSortOrder()->toLegacyOrderWay();
$facetedSearchFilters = $this->filtersConverter->getFacetedSearchFiltersFromFacets(
$menu->getFacets()
);
$productsAndCount = $this->module->getProductByFilters(
$query->getResultsPerPage(),
$query->getPage(),
$order_by,
$order_way,
$context->getIdLang(),
$facetedSearchFilters
);
$result
->setProducts($productsAndCount['products'])
->setTotalProductsCount($productsAndCount['count'])
->setAvailableSortOrders($this->getAvailableSortOrders())
;
$filterBlock = $this->module->getFilterBlock($facetedSearchFilters);
$facets = $this->filtersConverter->getFacetsFromFacetedSearchFilters(
$filterBlock['filters']
);
$this->copyFiltersActiveState(
$menu->getFacets(),
$facets
);
$this->labelRangeFilters($facets);
$this->addEncodedFacetsToFilters($facets);
$this->hideZeroValues($facets);
$this->hideUselessFacets($facets);
$nextMenu = (new FacetCollection())->setFacets($facets);
$result->setFacetCollection($nextMenu);
$result->setEncodedFacets($this->facetsSerializer->serialize($facets));
return $result;
}
private function labelRangeFilters(array $facets)
{
foreach ($facets as $facet) {
if ($facet->getType() === 'weight') {
$unit = Configuration::get('PS_WEIGHT_UNIT');
foreach ($facet->getFilters() as $filter) {
$filter->setLabel(
sprintf(
'%1$s%2$s - %3$s%4$s',
Tools::displayNumber($filter->getValue()['from']),
$unit,
Tools::displayNumber($filter->getValue()['to']),
$unit
)
);
}
} elseif ($facet->getType() === 'price') {
foreach ($facet->getFilters() as $filter) {
$filter->setLabel(
sprintf(
'%1$s - %2$s',
Tools::displayPrice($filter->getValue()['from']),
Tools::displayPrice($filter->getValue()['to'])
)
);
}
}
}
}
/**
* 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
$activeFacetFilters = $this->facetsSerializer->getActiveFacetFiltersFromFacets($facets);
$urlSerializer = new URLFragmentSerializer();
foreach ($facets as $facet) {
// 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()) {
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->facetsSerializer->removeFilterFromFacetFilters($activeFacetFilters, $filter, $facet);
break;
}
}
}
foreach ($facet->getFilters() as $filter) {
$facetFilters = $activeFacetFilters;
// toggle the current filter
if ($filter->isActive()) {
$facetFilters = $this->facetsSerializer->removeFilterFromFacetFilters($facetFilters, $filter, $facet);
} else {
$facetFilters = $this->facetsSerializer->addFilterToFacetFilters($facetFilters, $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(
$urlSerializer->serialize($facetFilters)
);
}
}
}
private function hideZeroValues(array $facets)
{
foreach ($facets as $facet) {
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() === 0) {
$filter->setDisplayed(false);
}
}
}
}
private function hideUselessFacets(array $facets)
{
foreach ($facets as $facet) {
$usefulFiltersCount = 0;
foreach ($facet->getFilters() as $filter) {
if ($filter->getMagnitude() > 0) {
++$usefulFiltersCount;
}
}
$facet->setDisplayed(
$usefulFiltersCount > 1
);
}
}
}

View File

@@ -0,0 +1,190 @@
<?php
class Ps_FacetedsearchRangeAggregator
{
private function makeNode(array $range, $minColumnIndex, $maxColumnIndex)
{
$min = $range[$minColumnIndex];
$max = $range[$maxColumnIndex];
if ($min === $max) {
$min = $range[$minColumnIndex] > 0 ? $range[$minColumnIndex] - 1 : 0;
$max = $max + 1;
}
return [
'min' => $min,
'max' => $max,
'count' => 1,
'left' => null,
'right' => null,
];
}
private function addNode(array &$target, array $node)
{
if ($node['min'] > $target['max']) {
if ($target['right']) {
$this->addNode($target['right'], $node);
} else {
$target['right'] = $node;
}
} elseif ($node['max'] < $target['min']) {
if ($target['left']) {
$this->addNode($target['left'], $node);
} else {
$target['left'] = $node;
}
} elseif ($node['max'] <= $target['max'] && $node['min'] >= $target['min']) {
++$target['count'];
} else {
$newMin = min($node['min'], $target['min']);
$newMax = max($node['max'], $target['max']);
$target['count'] += $node['count'];
if ($target['left']) {
if ($target['left']['min'] >= $newMin) {
$target['count'] += $target['left']['count'];
$target['left'] = null;
}
}
if ($target['right']) {
if ($target['right']['max'] <= $newMax) {
$target['count'] += $target['right']['count'];
$target['right'] = null;
}
}
$target['min'] = $newMin;
$target['max'] = $newMax;
}
}
public function aggregateRanges(array $ranges, $minColumnIndex, $maxColumnIndex)
{
$rootNode = null;
foreach ($ranges as $range) {
$node = $this->makeNode($range, $minColumnIndex, $maxColumnIndex);
if (null === $rootNode) {
$rootNode = $node;
} else {
$this->addNode($rootNode, $node);
}
}
$flat = $this->flatten($rootNode);
return $flat;
}
private function flatten(array $node)
{
$min = $node['min'];
$max = $node['max'];
$ranges = [[
'min' => $min,
'max' => $max,
'count' => $node['count'],
]];
if ($node['left']) {
$flatLeft = $this->flatten($node['left']);
$min = $flatLeft['min'];
$ranges = array_merge($flatLeft['ranges'], $ranges);
}
if ($node['right']) {
$flatRight = $this->flatten($node['right']);
$max = $flatRight['max'];
$ranges = array_merge($ranges, $flatRight['ranges']);
}
return [
'min' => $min,
'max' => $max,
'ranges' => $ranges,
];
}
public function getRangesFromList(array $list, $valueColumnIndex)
{
$min = null;
$max = null;
$byValue = [];
foreach ($list as $item) {
$n = $item[$valueColumnIndex];
if ($min === null || $n < $min) {
$min = $n;
}
if ($max === null || $n > $max) {
$max = $n;
}
$key = "n$n";
if (!array_key_exists($key, $byValue)) {
$byValue[$key] = [
'count' => 0,
'value' => $n,
];
}
++$byValue[$key]['count'];
}
$ranges = [];
$lastValue = null;
$lastCount = 0;
usort($byValue, function (array $a, array $b) {
return $a['value'] > $b['value'] ? 1 : -1;
});
foreach ($byValue as $countAndValue) {
$value = $countAndValue['value'];
$count = $countAndValue['count'];
if ($lastValue !== null) {
$ranges[] = [
'min' => $lastValue,
'max' => $value,
'count' => $count + $lastCount,
];
} else {
$lastCount = $count;
}
$lastValue = $value;
$lastCount = $count;
}
return [
'min' => $min,
'max' => $max,
'ranges' => $ranges,
];
}
public function mergeRanges(array $ranges, $outputLength)
{
if ($outputLength >= count($ranges)) {
$raw_ranges = $ranges;
} else {
$parts = array_chunk($ranges, floor(count($ranges) / $outputLength));
$raw_ranges = array_map(function (array $ranges) {
$min = $ranges[0]['min'];
$max = $ranges[count($ranges) - 1]['max'];
return [
'min' => $min,
'max' => $max,
'count' => array_reduce($ranges, function ($count, array $range) {
return $count + $range['count'];
}, 0),
];
}, $parts);
}
return $raw_ranges;
}
}

View File

@@ -0,0 +1,129 @@
<?php
/**
* 2007-2019 PrestaShop.
*
* 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.txt.
* 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.
*
* DISCLAIMER
*
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
* versions in the future. If you wish to customize PrestaShop for your
* needs please refer to http://www.prestashop.com for more information.
*
* @author PrestaShop SA <contact@prestashop.com>
* @copyright 2007-2019 PrestaShop SA
* @license https://opensource.org/licenses/AFL-3.0 Academic Free License 3.0 (AFL-3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
namespace PrestaShop\Module\FacetedSearch;
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer;
use PrestaShop\PrestaShop\Core\Product\Search\Filter;
use PrestaShop\PrestaShop\Core\Product\Search\Facet;
class URLSerializer
{
/**
* Add filter
*
* @param array $facetFilters
* @param Filter $facetFilter
* @param Facet $facet
*
* @return array
*/
public function addFilterToFacetFilters(array $facetFilters, Filter $facetFilter, Facet $facet)
{
if ($facet->getProperty('range')) {
$facetValue = $facet->getProperty('values');
$facetFilters[$facet->getLabel()] = [
$facetFilter->getProperty('symbol'),
isset($facetValue[0]) ? $facetValue[0] : $facet->getProperty('min'),
isset($facetValue[1]) ? $facetValue[1] : $facet->getProperty('max'),
];
} else {
$facetFilters[$facet->getLabel()][$facetFilter->getLabel()] = $facetFilter->getLabel();
}
return $facetFilters;
}
/**
* Remove filter
*
* @param array $facetFilters
* @param Filter $facetFilter
* @param Facet $facet
*
* @return array
*/
public function removeFilterFromFacetFilters(array $facetFilters, Filter $facetFilter, $facet)
{
if ($facet->getProperty('range')) {
unset($facetFilters[$facet->getLabel()]);
} else {
unset($facetFilters[$facet->getLabel()][$facetFilter->getLabel()]);
if (empty($facetFilters[$facet->getLabel()])) {
unset($facetFilters[$facet->getLabel()]);
}
}
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;
}
if (!$facet->getProperty('range')) {
$facetFilters[$facet->getLabel()][$facetFilter->getLabel()] = $facetFilter->getLabel();
} else {
$facetValue = $facetFilter->getValue();
$facetFilters[$facet->getLabel()] = [
$facetFilter->getProperty('symbol'),
$facetValue[0],
$facetValue[1],
];
}
}
}
return $facetFilters;
}
/**
* Serialize facets
*
* @param array $facets
*
* @return string
*/
public function serialize(array $facets)
{
$facetFilters = $this->getActiveFacetFiltersFromFacets($facets);
$urlSerializer = new URLFragmentSerializer();
return $urlSerializer->serialize($facetFilters);
}
}