Prestashop 1.7 : Ajouter une navigation à facettes dans les listings

Ce tutoriel est compatible avec les versions de Prestashop suivantes :
1.5 1.7 1.7.8 8.1 +
Cet article est assez ancien, malgré toute l'attention que j' apporte à mes contenus il est possible que celui-ci ne soit plus d'actualité.
N'hésitez pas à me le signaler si nécessaire via le formulaire de contact.

La gestion des listings à complétement été réécrite sur Prestashop 1.7 , et la bonne nouvelle et qu’ils gèrent maintenant nativement la navigation à facettes ( Tout du moins la partie front 😉 )
Nous allons voir ensemble comment mettre en place une navigation à facette basique sur la page des nouveaux produits.

Sommaire :

  1. Fonctionnement général
  2. Affichage de facettes
  3. Tri et gestion des facettes

Cet article est plus dans une démarche d’explication du fonctionnement, que dans la réalisation d’un module purement fonctionnel, et la réalisation est assez chronophage, dans la majorité des cas il sera préférable de passer par un module qui fera cela.
( Le module de navigation à facette de Prestashop le permets uniquement sur les catégories )

Fonctionnement général

Le premier point essentiel à noter et qu’il n’est pas nécessaire de faire de surcharge, tout peut être géré via un module 🙂

Avant de voir ce qu’il faut coder, il est important de comprendre comment fonctionnent les pages de listing sur prestashop 1.7
Dans mon exemple on va utiliser le controller NewProductsController ( controllers/front/listing/NewProductsController.php ) qui gère les nouveaux produits.

Lors de son initialisation il appelle la fonction doProductSearch de la classe parente ProductListingFrontController

    public function init()
    {
        parent::init();
        $this->doProductSearch('catalog/listing/new-products');
    }

Celle-ci appelle la fonction getProductSearchVariables ( j’omets volontairement le cas getAjaxProductSearchVariables qui est parlant puisqu’il rajoute des information spécifiques uniquement lors d’un appel ajax )

Le code est un peu long, mais toute la logique des controller listing sous Prestashop est dans cette fonction :
Je laisse les commentaires de base en anglais qui sont plutôt clairs.

     /**
     * This returns all template variables needed for rendering
     * the product list, the facets, the pagination and the sort orders.
     *
     * @return array variables ready for templating
     */
    protected function getProductSearchVariables()
    {
        /*
         * To render the page we need to find something (a ProductSearchProviderInterface)
         * that knows how to query products.
         */
 
        // the search provider will need a context (language, shop...) to do its job
        $context = $this->getProductSearchContext();
 
        // the controller generates the query...
        $query = $this->getProductSearchQuery();
 
        // ...modules decide if they can handle it (first one that can is used)
        // Dans notre cas c'est ICI que le module devra être appellé
        $provider = $this->getProductSearchProviderFromModules($query);
 
        // if no module wants to do the query, then the core feature is used
        if (null === $provider) {
            $provider = $this->getDefaultProductSearchProvider();
        }
 
        $resultsPerPage = (int) Tools::getValue('resultsPerPage');
        if ($resultsPerPage <= 0 || $resultsPerPage > 36) {
            $resultsPerPage = Configuration::get('PS_PRODUCTS_PER_PAGE');
        }
 
        // we need to set a few parameters from back-end preferences
        $query
            ->setResultsPerPage($resultsPerPage)
            ->setPage(max((int) Tools::getValue('page'), 1))
        ;
 
        // set the sort order if provided in the URL
        if (($encodedSortOrder = Tools::getValue('order'))) {
            $query->setSortOrder(SortOrder::newFromString(
                $encodedSortOrder
            ));
        }
 
        // get the parameters containing the encoded facets from the URL
        $encodedFacets = Tools::getValue('q');
 
        /*
         * The controller is agnostic of facets.
         * It's up to the search module to use /define them.
         *
         * Facets are encoded in the "q" URL parameter, which is passed
         * to the search provider through the query's "$encodedFacets" property.
         */
 
        $query->setEncodedFacets($encodedFacets);
 
        // We're ready to run the actual query!
 
        $result = $provider->runQuery(
            $context,
            $query
        );
 
        // sort order is useful for template,
        // add it if undefined - it should be the same one
        // as for the query anyway
        if (!$result->getCurrentSortOrder()) {
            $result->setCurrentSortOrder($query->getSortOrder());
        }
 
        // prepare the products
        $products = $this->prepareMultipleProductsForTemplate(
            $result->getProducts()
        );
 
        // render the facets
        if ($provider instanceof FacetsRendererInterface) {
            // with the provider if it wants to
            $rendered_facets = $provider->renderFacets(
                $context,
                $result
            );
            $rendered_active_filters = $provider->renderActiveFilters(
                $context,
                $result
            );
        } else {
            // with the core
            $rendered_facets = $this->renderFacets(
                $result
            );
            $rendered_active_filters = $this->renderActiveFilters(
                $result
            );
        }
 
        $pagination = $this->getTemplateVarPagination(
            $query,
            $result
        );
 
        // prepare the sort orders
        // note that, again, the product controller is sort-orders
        // agnostic
        // a module can easily add specific sort orders that it needs
        // to support (e.g. sort by "energy efficiency")
        $sort_orders = $this->getTemplateVarSortOrders(
            $result->getAvailableSortOrders(),
            $query->getSortOrder()->toString()
        );
 
        $sort_selected = false;
        if (!empty($sort_orders)) {
            foreach ($sort_orders as $order) {
                if (isset($order['current']) && true === $order['current']) {
                    $sort_selected = $order['label'];
                    break;
                }
            }
        }
 
        $searchVariables = array(
            'label' => $this->getListingLabel(),
            'products' => $products,
            'sort_orders' => $sort_orders,
            'sort_selected' => $sort_selected,
            'pagination' => $pagination,
            'rendered_facets' => $rendered_facets,
            'rendered_active_filters' => $rendered_active_filters,
            'js_enabled' => $this->ajax,
            'current_url' => $this->updateQueryString(array(
                'q' => $result->getEncodedFacets(),
            )),
        );
 
        Hook::exec('filterProductSearch', array('searchVariables' => &$searchVariables));
        Hook::exec('actionProductSearchAfter', $searchVariables);
 
        return $searchVariables;
    }

En raccourci dans cette fonction notre module pourra renvoyer une classe de provider spécifique grâce à cet appel :

 $provider = $this->getProductSearchProviderFromModules($query);

Ensuite c’est la fonction

// We're ready to run the actual query! 
$result = $provider->runQuery( $context, $query );

qui va appeller la fonction runQuery du module en lui passant le contexte et la requête de recherche.

La suite de l’affichage ( facettes , tris, et pagination ) seront gérés automatiquement à partir des informations retournées par le provider de notre module.

&nbsp

Création du module et affichage de facettes

Maintenant que la base est comprise nous pouvons passer à la création du module, celui-ci s’appellera hh_facetedSearch

Voici le code de base du module

require_once dirname(__FILE__).'/classes/Hh_facetedSearchProductSearchProvider.php';
class Hh_FacetedSearch extends Module
{
 
    public function __construct()
    {
 
        $this->author = 'hhennes';
        $this->name = 'hh_facetedsearch';
        $this->tab = 'test';
        $this->version = '0.1.0';
        $this->bootstrap = true;
        parent::__construct();
 
        $this->displayName = $this->l('HH Faceted Search');
        $this->description = $this->l('HH Sample Facets Implementation');
 
 
    }
 
    /**
     * Module installation.
     *
     * @return bool Success of the installation
     */
    public function install()
    {
        return parent::install()
            && $this->registerHook('productSearchProvider');
    }
 
    /**
     * Dans ce hook on intercepte la requête des nouveaux produits pour y ajouter des facettes
     * @param $params
     * @return Hh_facetedSearchProductSearchProvider
     */
    public function hookProductSearchProvider($params)
    {
        $query = $params['query'];
        if ($query->getQueryType() == 'new-products') {
            return new Hh_facetedSearchProductSearchProvider($this);
        }
    }
}

Le point essentiel est qu’il doit être greffé sur le hook productSearchProvider et qu’il doit retourner une classe qui implémente l’interface PrestaShop\PrestaShop\Core\Product\Search\ProductSearchProviderInterface
Nous créerons le fichier dans classes/Hh_facetedSearchProductSearchProvider.php

Dans cette étape nous allons uniquement afficher des facettes ce qui est géré nativement par le controller prestashop.
Voici à présent le code du Product Search Provider, avec les commentaires explicatifs pour faire cela :

 
//Use pour la recherche standard 
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\SortOrderFactory; 
 
//Use pour les facettes 
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection; #Collection de facettes 
use PrestaShop\PrestaShop\Core\Product\Search\Facet; #Classe de la facette 
use PrestaShop\PrestaShop\Core\Product\Search\Filter; #Classe des filtres 
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer; #Pour transformer l'url 

//Provider par défaut 
use PrestaShop\PrestaShop\Adapter\NewProducts\NewProductsProductSearchProvider; 
 
class Hh_facetedSearchProductSearchProvider implements ProductSearchProviderInterface { 
 
private $module; 
private $sortOrderFactory; 
 
/** 
* Instanciation de la classe 
* @param Hh_FacetedSearch $module 
*/ 
public function __construct( Hh_FacetedSearch $module ) { 
 $this->module = $module;
 //Récupération des tris disponibles par défaut
 $this->sortOrderFactory = new SortOrderFactory($this->module->getTranslator());
 }
 
 /**
 * @param ProductSearchContext $context
 * @param ProductSearchQuery $query
 * @return ProductSearchResult
 */
 public function runQuery(
 ProductSearchContext $context,
 ProductSearchQuery $query
 )
 {
 
 //Récupération des produits ( aucun changement par rapport au listing des nouveaux produits )
 if (!$products = $this->getProductsOrCount($context, $query, 'products')) {
 $products = array();
 }
 
 $count = $this->getProductsOrCount($context, $query, 'count');
 
 /**
 * Gestion du résulat
 * Envoi de la productSearchResult
 */
 $results = new ProductSearchResult();
 
 
 if (!empty($products)) {
 
 //Définition des résultats des produits
 $results
 ->setTotalProductsCount($count)
 ->setProducts($products);
 //Définition des tris disponibles ( Utilisation de ceux par défaut )
 $results->setAvailableSortOrders(
 $this->sortOrderFactory->getDefaultSortOrders()
 
 );
 
 //Récupération des filtres actifs , il sont situés dans l'url sous la forme q=filter-1|filtre-2 ect..
 $activeFilters = explode('|',$query->getEncodedFacets());
 //Définition des facettes disponibles ( c'est ici qu'on va définir nos facettes )
 $results->setFacetCollection(
 $this->getSampleFacets($activeFilters)
 );
 
 //Définition des facettes actuellement utilisées
 $results->setEncodedFacets(
 $query->getEncodedFacets()
 
 );
 
 }
 return $results;
 
 }
 
 
 
 /**
 * Récupération des produits et du décompte
 * Dans cette partie on reprends le fonctionnement du controller des nouveaux produits
 * 
 * @param ProductSearchContext $context
 * @param ProductSearchQuery $query
 * @param type $type
 * @return type
 */
 
 private function getProductsOrCount(
 ProductSearchContext $context,
 ProductSearchQuery $query,
 $type = 'products'
 ) {
 
 return Product::getNewProducts(
 $context->getIdLang(),
 $query->getPage(),
 $query->getResultsPerPage(),
 $type !== 'products',
 $query->getSortOrder()->toLegacyOrderBy(),
 $query->getSortOrder()->toLegacyOrderWay()
 );
 
 }
 
 
 
 /**
 * Fonction d'explication sur comment afficher des facettes
 * @return FacetCollection
 */
 protected function getSampleFacets($activeFilters)
 
 { 
 
 //Gestion des filtres actifs
 $activeFiltersQueryString ='';
 $activeFiltersQueryString .= implode('|',$activeFilters);
 
 
 //Création d'une collection de facettes
 $collection = new FacetCollection();
 
 //Création d'une facette
 $facet = new Facet();
 $facet->setLabel('Facette 1')
 ->setType('custom')
 ->setDisplayed(true) //Flag pour afficher ou nom la facette
 ->setWidgetType('checkbox') //Type de widget
 ->setMultipleSelectionAllowed(true); //Défini si on peut cocher plusieurs variantes
 
 
 //Ajout de filtres à cette facette
 $encodedFactetsUrl1 = $activeFiltersQueryString != '' ? $activeFiltersQueryString."|test-1": "test-1";
 
 $filter1 = new Filter();
 $filter1->setLabel('filtre 1') //Libellé du filtre
 ->setDisplayed(true) //Flag pour afficher ou nom le filtre
 ->setActive(in_array("test-1",$activeFilters) ? true : false ) //Définition si le filtre est actif ou non
 ->setType('test') // Type du filtre
 ->setValue('2') //Valeur du filtre
 ->setNextEncodedFacets($encodedFactetsUrl1) //Url pour afficher la filtre
 ->setMagnitude(1); //Nombre de résultats du filtre
 
 //Ajout du filtre à la facette
 $facet->addFilter($filter1); 
 
 $encodedFactetsUrl2 = $activeFiltersQueryString != '' ? $activeFiltersQueryString."|test-2": "test-2";
 
 //Idem pour un 2ème filtre
 $filter2 = new Filter();
 $filter2->setLabel('filtre 2') //Libellé du filtre
 ->setDisplayed(true) //Flag pour afficher ou nom le filtre
 ->setActive(in_array("test-2",$activeFilters) ? true : false ) //Définition si le filtre est actif ou non
 ->setType('test') // Type du filtre
 ->setValue('2') //Valeur du filtre
 ->setNextEncodedFacets($encodedFactetsUrl2) //Url pour afficher la filtre
 ->setMagnitude(3); //Nombre de résultats du filtre
 //Ajout du filtre à la facette
 $facet->addFilter($filter2); 
 
 
 //Ajout de la facette à la collection
 $collection->addFacet($facet);
 
 //Renvoi de la collection de facette
 return $collection;
 }
 
 /**
 * Provider de recherche par défaut
 * @return NewProductsProductSearchProvider
 */
 protected function getDefaultProductSearchProvider()
 {
 return new NewProductsProductSearchProvider(
 Context::getContext()->getTranslator()
 );
 }
}

La sélection / déselection est fonctionnelle lorsqu’on clique sur les différents éléments et tous les paramètres sont bien passés.
Notre navigation à facette est en place ( sans prise en compte des données )

Navigation facettes

Tri et gestion des facettes

Maintenant que la navigation à facette est fonctionnelle la prochaine étape est de renvoyer uniquement les facettes et les produits disponibles pour la sélection demandées.
Pour cette partie nous allons donc rajouter un filtre sur les catégories par défaut des produits.

Pour cela nous allons devoir rajouter 2 nouvelles fonction dans notre module :

  • getNewProducts => Reprends la fonction Product::getNewProducts et lui appliquera les filtres sélectionnés
  • getNewProductsCategoryFilters => Renverra la liste des filtres disponibles pour la séléection des produits

Voici le code complet du module fonctionnel qui permets de filtrer par les catégories par défaut du produit.
Fichier classes/Hh_facetedSearchProductSearchProvider.php

 //Use pour la recherche standard 
 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\SortOrderFactory; //Provider par défaut 
 use PrestaShop\PrestaShop\Adapter\NewProducts\NewProductsProductSearchProvider; 
 
 class Hh_facetedSearchProductSearchProvider implements ProductSearchProviderInterface { 
 
 private $module; 
 private $sortOrderFactory; 
 
 public function __construct( Hh_FacetedSearch $module ) { 
		$this->module = $module;
        $this->sortOrderFactory = new SortOrderFactory($this->module->getTranslator());
    }
 
    /**
     * @param ProductSearchContext $context
     * @param ProductSearchQuery $query
     * @return ProductSearchResult
     */
    public function runQuery(
        ProductSearchContext $context,
        ProductSearchQuery $query
    )
    {
 
        //Récupération des filtres actifs
        $activeFilters = explode('|',$query->getEncodedFacets());
 
        //Récupération des résultats initiaux ( page des nouveaux produits )
        if (!$products = $this->getProductsOrCount($context, $query, 'products',$activeFilters)) {
            $products = array();
        }
        $count = $this->getProductsOrCount($context, $query, 'count',$activeFilters);
 
 
        //Récupération des filtres de catégories dispo pour la sélection de produits
        $categoryfilters = $this->module->getNewProductsCategoryFilters($products,$activeFilters);
 
        /**
         * Dernière Etape : Gestion du résulat
         * Envoi de la productSearchResult
         */
        $results = new ProductSearchResult();
 
        //Définition des résultats des produits
        $results
            ->setTotalProductsCount($count)
            ->setProducts($products);
 
        //Définition des tris disponibles
        $results->setAvailableSortOrders(
            $this->sortOrderFactory->getDefaultSortOrders()
        );
 
        //Définition des facettes disponibles
        if ( sizeof($categoryfilters->getFacets())){
            $results->setFacetCollection(
                $categoryfilters //C'est ici qu'on assigne les filtres de notre fonction
            );
        }
 
        //Définition des facettes utilisées
        $results->setEncodedFacets(
          $query->getEncodedFacets()
        );
 
        return $results;
 
    }
 
    private function getProductsOrCount(
        ProductSearchContext $context,
        ProductSearchQuery $query,
        $type = 'products',
        $activeFilter = array()
    ) {
        //La fonction appellée ici est celle du module
        return $this->module->getNewProducts(
            $context->getIdLang(),
            $query->getPage(),
            $query->getResultsPerPage(),
            $type !== 'products',
            $query->getSortOrder()->toLegacyOrderBy(),
            $query->getSortOrder()->toLegacyOrderWay(),
            Context::getContext(),
            $activeFilter //Rajout d'un paramètre pour passer les filtre sélectionnés
        );
    }
 
    /**
     * Provider de recherche par défaut
     * @return SearchProductSearchProvider
     */
    protected function getDefaultProductSearchProvider()
    {
        return new NewProductsProductSearchProvider(
            Context::getContext()->getTranslator()
        );
    }
 
}

Et le fichier du module. hh_facetedsearch.php

require_once dirname(__FILE__).'/classes/Hh_facetedSearchProductSearchProvider.php'; 
//Use pour les facettes 
use PrestaShop\PrestaShop\Core\Product\Search\FacetCollection; #Collection de facettes 
use PrestaShop\PrestaShop\Core\Product\Search\Facet; #Classe de la facette 
use PrestaShop\PrestaShop\Core\Product\Search\Filter; #Classe des filtres 
use PrestaShop\PrestaShop\Core\Product\Search\URLFragmentSerializer; #Pour transformer l'url 

class Hh_FacetedSearch extends Module { 
 
     public function __construct() { 
	     $this->author = 'hhennes';
        $this->name = 'hh_facetedsearch';
        $this->tab = 'test';
        $this->version = '0.1.0';
        $this->bootstrap = true;
        parent::__construct();
 
        $this->displayName = $this->l('HH Faceted Search');
        $this->description = $this->l('HH Sample Facets Implementation');
 
 
    }
 
    /**
     * Module installation.
     *
     * @return bool Success of the installation
     */
    public function install()
    {
        return parent::install()
            && $this->registerHook('productSearchProvider');
    }
 
    /**
     * Dans ce hook on intercepte la requête des nouveaux produits pour y ajouter des facettes
     * @param $params
     * @return Hh_facetedSearchProductSearchProvider
     */
    public function hookProductSearchProvider($params)
    {
        //dump($this->context->controller);
        $query = $params['query'];
        if ($query->getQueryType() == 'new-products') {
            return new Hh_facetedSearchProductSearchProvider($this);
        }
    }
 
    /**
     * A partir des produits sélectionnés on déduit les filtres de catégories à afficher
     * @param array $activeFilters
     * @return FacetCollection
     */
    public function getNewProductsCategoryFilters($products , array $activeFilters)
    {
        //Récupération des catégories des produits et de leurs filtres
        $categoriesArray = [];
        foreach ( $products as $product )
        {
            if ( !array_key_exists($product['id_category_default'],$categoriesArray)){
                $categoriesArray[$product['id_category_default']] = 1;
            } else {
                $categoriesArray[$product['id_category_default']] = (int)$categoriesArray[$product['id_category_default']]+1;
            }
        }
 
        $activeFiltersQueryString ='';
        $activeFiltersQueryString .= implode('|',$activeFilters);
 
        //Création d'une collection de facettes
        $collection = new FacetCollection();
 
        if ( sizeof($categoriesArray)) {
 
            //Création d'une facette
            $facet = new Facet();
            $facet->setLabel($this->l('Catégories'))
                ->setType('category')
                ->setDisplayed(true)
                ->setWidgetType('checkbox')
                ->setMultipleSelectionAllowed(true);
 
            //Création des filtres avec les categories disponibles
            foreach ( $categoriesArray as $categoryId => $categoryCount) {
 
                $encodedFactetsUrl = $activeFiltersQueryString != '' ? $activeFiltersQueryString."|cat-".$categoryId : "cat-".$categoryId;
 
                $category = new Category($categoryId,$this->context->language->id);
                $filter = new Filter();
                $filter->setLabel($category->name)
                    ->setDisplayed(true)
                    ->setActive(in_array("cat-".$categoryId,$activeFilters) ? true : false )
                    ->setType('category')
                    ->setValue($categoryId)
                    ->setNextEncodedFacets($encodedFactetsUrl)
                    ->setMagnitude($categoryCount);
 
                //Ajout du filtre à la facette
                $facet->addFilter($filter);
            }
 
            //Ajout de la facette à la collection
            $collection->addFacet($facet);
        }
 
 
        return $collection;
    }
 
    /**
     * Get new products
     * ( Fonction modifiée de la classe produit avec gestion des filtres )
     *
     * @param int $id_lang Language id
     * @param int $pageNumber Start from (optional)
     * @param int $nbProducts Number of products to return (optional)
     * @return array New products
     */
    public function getNewProducts(
        $id_lang,
        $page_number = 0,
        $nb_products = 10,
        $count = false,
        $order_by = null,
        $order_way = null,
        Context $context = null ,
        $applieds_filters = array() //Nouveau paramètre de test
    )
    {
        $now = date('Y-m-d') . ' 00:00:00';
        if (!$context) {
            $context = Context::getContext();
        }
 
        $front = true;
        if (!in_array($context->controller->controller_type, array('front', 'modulefront'))) {
            $front = false;
        }
 
        if ($page_number < 1) {
            $page_number = 1;
        }
        if ($nb_products < 1) { $nb_products = 10; } if (empty($order_by) || $order_by == 'position') { $order_by = 'date_add'; } if (empty($order_way)) { $order_way = 'DESC'; } if ($order_by == 'id_product' || $order_by == 'price' || $order_by == 'date_add' || $order_by == 'date_upd') { $order_by_prefix = 'product_shop'; } elseif ($order_by == 'name') { $order_by_prefix = 'pl'; } if (!Validate::isOrderBy($order_by) || !Validate::isOrderWay($order_way)) { die(Tools::displayError()); } $sql_groups = ''; if (Group::isFeatureActive()) { $groups = FrontController::getCurrentCustomerGroups(); $sql_groups = ' AND EXISTS(SELECT 1 FROM `'._DB_PREFIX_.'category_product` cp JOIN `'._DB_PREFIX_.'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` '.(count($groups) ? 'IN ('.implode(',', $groups).')' : '= '.(int)Configuration::get('PS_UNIDENTIFIED_GROUP')).') WHERE cp.`id_product` = p.`id_product`)'; } if (strpos($order_by, '.') > 0) {
            $order_by = explode('.', $order_by);
            $order_by_prefix = $order_by[0];
            $order_by = $order_by[1];
        }
 
        $nb_days_new_product = (int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT');
 
        if ($count) {
 
            //Gestion des conditions dans le count
            if ( sizeof($applieds_filters)) {
                $sqlFilters = "";
                foreach ($applieds_filters as $applieds_filter) {
 
                    if ( $applieds_filter == "" ) {
                        continue;
                    }
                    $catId = str_replace('cat-','',$applieds_filter);
                    if ( $catId == 0 ) {
                        continue;
                    }
                    $sqlFilters .= " INNER JOIN "._DB_PREFIX_."product ps_cat_".$catId.' ON  p.id_product = ps_cat_'.$catId.'.id_product AND ps_cat_'.$catId.'.id_category_default = '.$catId.' ';
                }
            }
 
            $sql = 'SELECT COUNT(p.`id_product`) AS nb
					FROM `'._DB_PREFIX_.'product` p 
                    '.$sqlFilters.'
					'.Shop::addSqlAssociation('product', 'p').'
					WHERE product_shop.`active` = 1
					AND product_shop.`date_add` > "'.date('Y-m-d', strtotime('-'.$nb_days_new_product.' DAY')).'"
					'.($front ? ' AND product_shop.`visibility` IN ("both", "catalog")' : '').'
					'.$sql_groups;
 
            return (int)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
        }
 
        $sql = new DbQuery();
        $sql->select(
            'p.*, product_shop.*, stock.out_of_stock, IFNULL(stock.quantity, 0) as quantity, pl.`description`, pl.`description_short`, pl.`link_rewrite`, pl.`meta_description`,
			pl.`meta_keywords`, pl.`meta_title`, pl.`name`, pl.`available_now`, pl.`available_later`, image_shop.`id_image` id_image, il.`legend`, m.`name` AS manufacturer_name,
			(DATEDIFF(product_shop.`date_add`,
				DATE_SUB(
					"'.$now.'",
					INTERVAL '.$nb_days_new_product.' DAY
				)
			) > 0) as new'
        );
 
        $sql->from('product', 'p');
        $sql->join(Shop::addSqlAssociation('product', 'p'));
        $sql->leftJoin('product_lang', 'pl', '
			p.`id_product` = pl.`id_product`
			AND pl.`id_lang` = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl')
        );
        $sql->leftJoin('image_shop', 'image_shop', 'image_shop.`id_product` = p.`id_product` AND image_shop.cover=1 AND image_shop.id_shop='.(int)$context->shop->id);
        $sql->leftJoin('image_lang', 'il', 'image_shop.`id_image` = il.`id_image` AND il.`id_lang` = '.(int)$id_lang);
        $sql->leftJoin('manufacturer', 'm', 'm.`id_manufacturer` = p.`id_manufacturer`');
 
        //Gestion des filtres ( Pour l'exemple on ne gère que des catégories )
        if ( sizeof($applieds_filters)) {
            foreach ($applieds_filters as $applieds_filter) {
 
                if ( $applieds_filter == "") {
                    continue;
                }
 
                $catId = str_replace('cat-','',$applieds_filter);
                if ( $catId == 0 ) {
                    continue;
                }
                $sql->innerJoin('product','ps_cat_'.$catId,'p.id_product = ps_cat_'.$catId.'.id_product AND ps_cat_'.$catId.'.id_category_default = '.$catId);
            }
        }
 
        //Fin Gestion des filtres
 
        $sql->where('product_shop.`active` = 1');
        if ($front) {
            $sql->where('product_shop.`visibility` IN ("both", "catalog")');
        }
        $sql->where('product_shop.`date_add` > "'.date('Y-m-d', strtotime('-'.$nb_days_new_product.' DAY')).'"');
        if (Group::isFeatureActive()) {
            $groups = FrontController::getCurrentCustomerGroups();
            $sql->where('EXISTS(SELECT 1 FROM `'._DB_PREFIX_.'category_product` cp
				JOIN `'._DB_PREFIX_.'category_group` cg ON (cp.id_category = cg.id_category AND cg.`id_group` '.(count($groups) ? 'IN ('.implode(',', $groups).')' : '= 1').')
				WHERE cp.`id_product` = p.`id_product`)');
        }
 
        $sql->orderBy((isset($order_by_prefix) ? pSQL($order_by_prefix).'.' : '').'`'.pSQL($order_by).'` '.pSQL($order_way));
        $sql->limit($nb_products, (int)(($page_number-1) * $nb_products));
 
        if (Combination::isFeatureActive()) {
            $sql->select('product_attribute_shop.minimal_quantity AS product_attribute_minimal_quantity, IFNULL(product_attribute_shop.id_product_attribute,0) id_product_attribute');
            $sql->leftJoin('product_attribute_shop', 'product_attribute_shop', 'p.`id_product` = product_attribute_shop.`id_product` AND product_attribute_shop.`default_on` = 1 AND product_attribute_shop.id_shop='.(int)$context->shop->id);
        }
        $sql->join(Product::sqlStock('p', 0));
 
        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
 
        if (!$result) {
            return false;
        }
 
        if ($order_by == 'price') {
            Tools::orderbyPrice($result, $order_way);
        }
        $products_ids = array();
        foreach ($result as $row) {
            $products_ids[] = $row['id_product'];
        }
        // Thus you can avoid one query per product, because there will be only one query for all the products of the cart
        Product::cacheFrontFeatures($products_ids, $id_lang);
        return Product::getProductsProperties((int)$id_lang, $result);
    }
 
}

Une fois tout cela en place vous devriez avoir une navigation à facette sur les catégories des produits en place comme sur la capture ci-dessous.<br
Ce principe est applicable à toutes les pages prestashop étendant les listings.
Et il est également possible de créer des pages de listings personnalisées ( j’y reviendrais peut être dans un article ultérieurement )
Navigation à facettes prestashop

8 réflexions sur “Prestashop 1.7 : Ajouter une navigation à facettes dans les listings”

  1. Bonjour,
    J’ai suivi le tuto, cela marche bien mais je m’attendais à voir toutes les options paramétrées dans mon modèle de navigation à facettes (prix, attributs, caractéristiques, …). Vous savez comment faire cela ?
    Merci.

    1. Bonjour,
      Effectivement cet article explique le principe de fonctionnement de la navigation à facettes.
      Sur les listings des produits standards cette fonctionnalité est gérée par le module ps_facetedsearch.
      Celui-ci suit le mode de fonctionnement expliqué ci joint en y rajoutant une logique de filtrage complète ( qui est beaucoup plus complexe et nécessite beaucoup plus de lignes de code )
      En consultant et en adaptant le code de celui-ci vous pourrez obtenir ce que vous souhaitez.

      Cordialement,
      Hervé

  2. Bonjour,

    L’article est très intéressant.

    A la fin de celui-ci, vous dîtes :

    Et il est également possible de créer des pages de listings personnalisées ( j’y reviendrais peut être dans un article ultérieurement )

    Et c’est justement ce qui m’intéresse. Pensez vous faire un tutu sur ce sujet.

    Car j’amarrais sur mon site afficher une page listant les produits dont le prix est compris entre 0 et 50 euros.

    J’ai crée un contrôler qui étend le frontcontroller, j’arrive à afficher la liste mais pas la pagination et le filtre.

    Pouvez-Vous m’aider sur ce point?

    Merci d’avance

    1. Bonjour,
      La logique à reprendre est exactement la même.
      Il faut créer une classe custom qui implémente l’interface ProductSearchProviderInterface comme la classe NewProductsProductSearchProvider par exemple
      Dans votre controller vous pouvez la définir comme classe par défaut sous la forme :
      protected function getDefaultProductSearchProvider()
      {
      return new NewProductsProductSearchProvider(
      $this->getTranslator()
      );
      }

      C’est dans cette classe que les paginations et tris sont gérés.

      Cordialement,
      Hervé

  3. Salut,

    J’aimerai m’inspirer de ton code pour ajouter une filtre par Brand sur la page des promotions. Le but est de pouvoir lister uniquement les produits en spécial pour une marque X.
    A priori selon ce que je comprends, je dois changer le controller NewProductsController, puisque l’on utilise pas les nouveaux produits mais les PricesDrop Products, donc le controller controllers/front/listing/PricesDropController.php.
    Mais ensuite dans ton code de module je me perds, ….
    Je change le query type New-products, mais je bloque sur le provider et puis je suis incapable de continuer….
    Je ne comprends d’ailleurs pas que Prestashop ne mette pas ce genre de recherche en standard sur sa page de promotion…..

    1. Bonjour,

      Ce n’est pas tout à fait ça, le NewsProductController est un exemple pour appliquer la modification sur la page des nouveaux produits.
      Dans ton cas la base de la récupération des produits va être le PriceDropController.
      Et la fonction de récupération des produits à modifier Product::getPricesDrop

      Pour le reste la logique reste la même, c’est sur qu’elle n’est pas évidente à implémenter.
      Le plus simple reste de trouver un module qui gère ça sur addons.

      Cordialement,
      Hervé

  4. Bonjour,

    Super-tutoriel. J’ai suivi à la lettre vos instructions et les filtre par catégorie s’affiches très bien je peux sélectionner un filtre en cliquant dessus. Malheureusement le choix multiple ne fonctionne pas lors du click sur l’une des catégories et lorsque le résultat est affiché tous les autres choix ont disparu de plus lorsque je veux supprimer un filtre actif au lieu de me renvoyer vers l’URL classique de la page des nouveaux produits celui-ci me renvoies une URL de ce type : https://dev.monsite.com/nouveaux-produits?q=cat-245%7Ccat-245

    Ai-je raté quelque chose ? (je suis sur PS 1.7.5). Si vous pouviez m’aider j’en serais ravie ! Cordialement.

    1. A ce stade, j’ai l’impression que le module ne fait que rajouter des filtres, il faut l’adapter je pense.
      Peut-être une mise à jour du module/ tuto est en écriture?

Répondre à herve Annuler la réponse

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *