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.

<?php    
     /**
     * 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.

 

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);
        }
    }
}

La 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 :

<?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;
 //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) //Appel de la fonction spécifique
            );
 
            //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 navigation à facettes est à présent bien visible sur la page des nouveaux produits.

Navigation facettes

La séléction / 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 )

Tri et gestion des facettes

Maintenant que la navigation à facette est fonctionnelle la prochaine étape et 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

 

<?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

 

<?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.
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