💻 DéveloppementIntermédiaire PS 1.7 PS 8.x

Afficher des produits d'une catégorie sur une fiche produit PrestaShop

Guide complet pour afficher dynamiquement des produits d'une même catégorie sur une fiche produit PrestaShop 1.7 et 8.x, avec filtrage par caractéristique.

En bref : Pour afficher des produits d'une catégorie sur une fiche produit PrestaShop avec filtrage par caractéristique, créez un override de Category.php avec une méthode getProductsByFeature() qui joint les tables produit et caractéristique, puis appelez-la depuis un hook de module avec un template Smarty réutilisant les miniatures natives.

Publié le 21 mars 2026 8 min de lecture Alexandre Carette

Le besoin : afficher des produits associés par catégorie

Sur une fiche produit PrestaShop, il est fréquent de vouloir afficher des produits issus de la même catégorie — voire filtrés par une caractéristique précise (couleur, matière, gamme). PrestaShop propose nativement deux mécanismes : les accessoires produit (association manuelle) et le bloc "Produits dans la même catégorie" (module ps_categoryproducts). Mais aucun des deux ne permet un filtrage fin par caractéristique.

Dans cet article, je vous montre comment créer une solution sur mesure en combinant un override de la classe Category avec un affichage personnalisé dans le template produit.

Étape 0 : bien identifier le mécanisme existant

Avant de coder quoi que ce soit, identifiez quel module gère actuellement l'affichage des produits associés sur votre fiche produit :

  • **Module `ps_categoryproducts`** : affiche automatiquement les produits de la même catégorie par défaut. C'est le plus courant.
  • **Accessoires produit** : association manuelle dans l'onglet "Modules" de la fiche produit en back-office. Chaque produit est sélectionné individuellement.
  • **Module tiers** : certains modules comme "Related Products" ajoutent des logiques de recommandation.

Pour vérifier, inspectez le template ou utilisez le mode debug de Smarty :


{* Dans votre template produit, pour identifier les variables disponibles *}
{$product|@var_dump}

Astuce : En PrestaShop 8.x, le mode debug Smarty s'active dans Paramètres avancés > Performances > Mode debug. Cela affiche les variables Smarty disponibles dans chaque template.

Étape 1 : créer un override de `Category.php`

La classe Category de PrestaShop dispose d'une méthode getProducts() qui récupère les produits d'une catégorie donnée. Nous allons l'enrichir d'une méthode getProductsByFeature() qui ajoute un filtre par caractéristique.

Créez le fichier override/classes/Category.php :


<?php
/**
 * Override de la classe Category
 * Ajoute le filtrage des produits par caractéristique (feature)
 */
class Category extends CategoryCore
{
    /**
     * Récupère les produits d'une catégorie filtrés par caractéristique
     *
     * @param int $id_lang ID de la langue
     * @param int $p Numéro de page
     * @param int $n Nombre de produits par page
     * @param string|null $order_by Champ de tri
     * @param string|null $order_way Direction du tri (ASC/DESC)
     * @param int $id_feature ID de la caractéristique
     * @param int $id_feature_value ID de la valeur de caractéristique
     * @param bool $get_total Retourner uniquement le total
     * @param bool $active Produits actifs uniquement
     * @param bool $random Tri aléatoire
     * @param int $random_number_products Nombre de produits aléatoires
     * @param bool $check_access Vérifier les droits d'accès
     * @param Context|null $context Contexte PrestaShop
     * @return array|int Liste des produits ou total
     */
    public static function getProductsByFeature(
        $id_lang,
        $p = 1,
        $n = 10,
        $order_by = null,
        $order_way = null,
        $id_feature = 0,
        $id_feature_value = 0,
        $get_total = false,
        $active = true,
        $random = false,
        $random_number_products = 1,
        $check_access = true,
        Context $context = null
    ) {
        if (!$context) {
            $context = Context::getContext();
        }

        $id_shop = $context->shop->id;

        // Requête de base pour compter le total
        if ($get_total) {
            $sql = 'SELECT COUNT(DISTINCT p.`id_product`) AS total
                    FROM `' . _DB_PREFIX_ . 'product` p
                    ' . Shop::addSqlAssociation('product', 'p') . '
                    INNER JOIN `' . _DB_PREFIX_ . 'feature_product` fp
                        ON fp.`id_product` = p.`id_product`
                    WHERE product_shop.`visibility` IN ("both", "catalog")
                    AND product_shop.`active` = 1
                    AND fp.`id_feature` = ' . (int) $id_feature . '
                    AND fp.`id_feature_value` = ' . (int) $id_feature_value;

            return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
        }

        // Validation du tri
        if ($order_by === null) {
            $order_by = 'position';
        }
        if ($order_way === null) {
            $order_way = 'ASC';
        }

        $order_by_whitelist = [
            'id_product', 'price', 'date_add',
            'date_upd', 'name', 'position', 'reference'
        ];
        if (!in_array($order_by, $order_by_whitelist)) {
            $order_by = 'position';
        }
        if (!in_array(strtoupper($order_way), ['ASC', 'DESC'])) {
            $order_way = 'ASC';
        }

        $sql = 'SELECT DISTINCT p.id_product, p.*, product_shop.*, pl.*,
                    image_shop.`id_image` AS id_image,
                    il.`legend`,
                    m.`name` AS manufacturer_name,
                    DATEDIFF(product_shop.`date_add`, DATE_SUB(
                        "' . date('Y-m-d') . ' 00:00:00",
                        INTERVAL ' . (int) Configuration::get('PS_NB_DAYS_NEW_PRODUCT') . ' DAY
                    )) > 0 AS new
                FROM `' . _DB_PREFIX_ . 'product` p
                ' . Shop::addSqlAssociation('product', 'p') . '
                INNER JOIN `' . _DB_PREFIX_ . 'feature_product` fp
                    ON fp.`id_product` = p.`id_product`
                LEFT JOIN `' . _DB_PREFIX_ . 'product_lang` pl
                    ON p.`id_product` = pl.`id_product`
                    AND pl.`id_lang` = ' . (int) $id_lang . '
                    AND pl.`id_shop` = ' . (int) $id_shop . '
                LEFT JOIN `' . _DB_PREFIX_ . 'image_shop` image_shop
                    ON image_shop.`id_product` = p.`id_product`
                    AND image_shop.`cover` = 1
                    AND image_shop.`id_shop` = ' . (int) $id_shop . '
                LEFT JOIN `' . _DB_PREFIX_ . 'image_lang` il
                    ON image_shop.`id_image` = il.`id_image`
                    AND il.`id_lang` = ' . (int) $id_lang . '
                LEFT JOIN `' . _DB_PREFIX_ . 'manufacturer` m
                    ON m.`id_manufacturer` = p.`id_manufacturer`
                WHERE product_shop.`visibility` IN ("both", "catalog")
                AND product_shop.`active` = 1
                AND fp.`id_feature` = ' . (int) $id_feature . '
                AND fp.`id_feature_value` = ' . (int) $id_feature_value;

        if ($random) {
            $sql .= ' ORDER BY RAND() LIMIT ' . (int) $random_number_products;
        } else {
            $sql .= ' ORDER BY ' . pSQL($order_by) . ' ' . pSQL($order_way) . '
                      LIMIT ' . (int) (($p - 1) * $n) . ', ' . (int) $n;
        }

        $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);

        if (!$result) {
            return [];
        }

        // Enrichir les produits avec les données complètes (prix, images, etc.)
        return Product::getProductsProperties($id_lang, $result);
    }
}

Important : Après avoir créé cet override, supprimez le fichier de cache des classes :


# PrestaShop 1.7
rm var/cache/prod/class_index.php
rm var/cache/dev/class_index.php

# PrestaShop 8.x
rm var/cache/prod_/class_index.php
rm var/cache/dev_/class_index.php

Étape 2 : appeler la méthode depuis un module ou un controller

Vous pouvez exploiter cette méthode de plusieurs façons. Voici l'approche la plus propre : un widget dans un module personnalisé accroché au hook displayProductAdditionalInfo ou displayFooterProduct.


<?php
// Dans votre module, méthode hookDisplayFooterProduct()
public function hookDisplayFooterProduct($params)
{
    $product = $params['product'];
    $id_lang = $this->context->language->id;

    // ID de la caractéristique "Couleur" (à adapter)
    $id_feature = 3;

    // Récupérer la valeur de cette caractéristique pour le produit courant
    $features = $product->getFeatures();
    $id_feature_value = 0;

    foreach ($features as $feature) {
        if ((int) $feature['id_feature'] === $id_feature) {
            $id_feature_value = (int) $feature['id_feature_value'];
            break;
        }
    }

    if (!$id_feature_value) {
        return '';
    }

    // Récupérer les produits partageant cette caractéristique
    $related_products = Category::getProductsByFeature(
        $id_lang,
        1,        // page
        8,        // nombre de produits
        'position',
        'ASC',
        $id_feature,
        $id_feature_value
    );

    // Exclure le produit courant
    $related_products = array_filter($related_products, function ($p) use ($product) {
        return (int) $p['id_product'] !== (int) $product->id;
    });

    $this->context->smarty->assign([
        'related_products' => $related_products,
        'feature_name' => Feature::getFeature($id_lang, $id_feature)['name'] ?? '',
    ]);

    return $this->display(__FILE__, 'views/templates/hook/related-by-feature.tpl');
}

Étape 3 : créer le template Smarty

Créez le fichier views/templates/hook/related-by-feature.tpl :


{if $related_products|count > 0}
<section class="related-by-feature">
    <h2>{l s='Produits similaires par' mod='votre_module'} {$feature_name}</h2>
    <div class="products row">
        {foreach from=$related_products item=product}
            <div class="col-xs-6 col-md-3">
                {include file="catalog/_partials/miniatures/product.tpl" product=$product}
            </div>
        {/foreach}
    </div>
</section>
{/if}

Ce template réutilise le miniature produit natif de PrestaShop, ce qui garantit la cohérence visuelle avec le reste de votre catalogue et hérite automatiquement des personnalisations de votre thème.

Étape 4 : ajuster l'affichage en CSS

Pour contrôler la grille et l'espacement, ajoutez quelques règles CSS dans votre thème ou module :


.related-by-feature {
    margin-top: 2rem;
    padding: 1.5rem 0;
    border-top: 1px solid #e5e5e5;
}

.related-by-feature h2 {
    font-size: 1.25rem;
    margin-bottom: 1rem;
    font-weight: 600;
}

.related-by-feature .product-miniature {
    margin-bottom: 1rem;
}

Alternative rapide : les accessoires produit

Si votre catalogue est petit (moins de 100 produits), la solution la plus simple reste les accessoires produit natifs de PrestaShop :

  1. Allez dans **Catalogue > Produits > [votre produit] > Modules**
  2. Configurez le module **"Produits dans la même catégorie"** ou utilisez les **accessoires**
  3. Saisissez les produits associés un par un
  4. Pour accélérer l'association en masse, vous pouvez insérer directement en base de données :

    
    -- Associer le produit 42 comme accessoire du produit 10
    INSERT INTO ps_accessory (id_product_1, id_product_2)
    VALUES (10, 42);
    
    -- Association bidirectionnelle (recommandé)
    INSERT INTO ps_accessory (id_product_1, id_product_2)
    VALUES (42, 10);
    

    Bonnes pratiques et considérations de performance

    Mise en cache

    La méthode getProductsByFeature() effectue une requête SQL à chaque chargement de page. Sur un catalogue volumineux, pensez à mettre en cache le résultat :

    
    $cache_key = 'Category::getProductsByFeature_'
        . $id_feature . '_' . $id_feature_value . '_' . $id_lang;
    
    if (!Cache::isStored($cache_key)) {
        $products = Category::getProductsByFeature($id_lang, 1, 8, 'position', 'ASC', $id_feature, $id_feature_value);
        Cache::store($cache_key, $products);
    } else {
        $products = Cache::retrieve($cache_key);
    }
    

    Index de base de données

    Assurez-vous que la table ps_feature_product dispose d'un index sur (id_feature, id_feature_value) :

    
    ALTER TABLE ps_feature_product
    ADD INDEX idx_feature_value (id_feature, id_feature_value);
    

    PrestaShop 8.x : préférer les services Symfony

    Sur PrestaShop 8.x, les overrides restent fonctionnels mais l'approche recommandée évolue vers les services Symfony et la couche CQRS. Pour un projet neuf sur PS 8, envisagez de créer un service injectable plutôt qu'un override de classe :

    
    // src/Service/ProductByFeatureProvider.php dans votre module
    namespace YourModule\Service;
    
    use Doctrine\DBAL\Connection;
    
    class ProductByFeatureProvider
    {
        private Connection $connection;
        private string $dbPrefix;
    
        public function __construct(Connection $connection, string $dbPrefix)
        {
            $this->connection = $connection;
            $this->dbPrefix = $dbPrefix;
        }
    
        public function getProductIdsByFeature(int $featureId, int $featureValueId, int $limit = 8): array
        {
            $qb = $this->connection->createQueryBuilder();
            $qb->select('DISTINCT fp.id_product')
               ->from($this->dbPrefix . 'feature_product', 'fp')
               ->innerJoin('fp', $this->dbPrefix . 'product_shop', 'ps', 'ps.id_product = fp.id_product')
               ->where('fp.id_feature = :featureId')
               ->andWhere('fp.id_feature_value = :featureValueId')
               ->andWhere('ps.active = 1')
               ->setParameter('featureId', $featureId)
               ->setParameter('featureValueId', $featureValueId)
               ->setMaxResults($limit);
    
            return array_column($qb->execute()->fetchAllAssociative(), 'id_product');
        }
    }
    

    Débogage : identifier les variables Smarty disponibles

    Si vous travaillez sur un template existant et que vous ne savez pas quelles données sont accessibles, utilisez var_dump directement dans Smarty :

    
    {* Afficher toutes les variables assignées au template *}
    {$subcategories|@var_dump}
    
    {* Ou pour un produit spécifique *}
    {$product|@var_dump}
    

    Cela vous permet d'identifier précisément les clés disponibles avant de construire vos boucles foreach.

#override #Category #produits similaires #ps_featured #caractéristiques #fiche produit

Questions fréquentes

Tout ce que vous devez savoir sur ce sujet.

Un projet PrestaShop ?

Discutons-en directement.

★★★★★

193 projets livrés

Gratuit & sans engagement — réponse sous 24h

Alexandre Carette

Alexandre Carette

Expert PrestaShop & Architecture E-commerce

Développeur PrestaShop depuis 2014, 193 projets livrés. Je conçois des architectures headless Nuxt + PrestaShop et des outils d'automatisation IA pour les e-commerçants.