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

Implémenter un système de prix libre sur PrestaShop (Name Your Price)

Guide technique pour créer un système de prix libre sur PrestaShop : override Product, validation sécurisée, formulaire front-end et bonnes pratiques 8.x.

En bref : Implémentez un système de prix libre sur PrestaShop via un override de Product (mise à jour du prix) et du ProductController (validation sécurisée avec bornes min/max). Pour la production, préférez les SpecificPrice par client plutôt que la modification directe en base.

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

Pourquoi proposer un prix libre sur PrestaShop ?

Le concept de prix libre (ou *Name Your Price*) permet au client de définir lui-même le montant qu'il souhaite payer pour un produit. Ce mécanisme trouve sa place dans plusieurs contextes e-commerce :

  • **Dons et associations** : laisser le visiteur choisir le montant de sa contribution
  • **Produits numériques** : ebooks, templates, musique — le client paie ce qu'il estime juste
  • **Ventes solidaires** : prix plancher garanti, mais liberté au-delà
  • **Tests de pricing** : mesurer la valeur perçue d'un produit par le marché

PrestaShop ne propose pas cette fonctionnalité nativement. La solution technique repose sur un override du contrôleur produit, une méthode de mise à jour du prix dans la classe Product, et un formulaire front-end avec validation stricte.

Architecture de la solution

La mise en œuvre s'articule en trois couches :

  1. **Classe `Product`** — nouvelle méthode statique pour mettre à jour le prix en base
  2. **Contrôleur `ProductController`** — interception du formulaire et validation des données
  3. **Template front-end** — formulaire de saisie du prix avec feedback utilisateur
  4. Remarque importante : cette approche modifie le prix du produit en base de données. Elle convient pour un usage où chaque client « fixe » son prix avant ajout au panier. Pour un usage multi-visiteurs simultanés, il faudra adapter la logique (voir la section sur les alternatives en fin d'article).

    Étape 1 : Override de la classe Product

    Créez le fichier d'override pour ajouter une méthode de mise à jour du prix.

    PrestaShop 1.6

    Fichier : override/classes/Product.php

    
    <?php
    /**
     * @author Alexandre Carette <contact@alexandrecarette.fr>
     * @copyright 2026 Alexandre Carette
     * @license Propriétaire et Confidentiel
     */
    
    class Product extends ProductCore
    {
        /**
         * Met à jour le prix d'un produit dans toutes les tables concernées
         *
         * @param int $idProduct ID du produit
         * @param float $price Nouveau prix HT
         * @return bool
         */
        public static function updateCustomPrice($idProduct, $price)
        {
            $idProduct = (int) $idProduct;
            $price = (float) $price;
    
            // Mise à jour dans ps_product
            $sql1 = 'UPDATE `' . _DB_PREFIX_ . 'product`
                      SET `price` = ' . $price . '
                      WHERE `id_product` = ' . $idProduct;
    
            // Mise à jour dans ps_product_shop (multiboutique)
            $sql2 = 'UPDATE `' . _DB_PREFIX_ . 'product_shop`
                      SET `price` = ' . $price . '
                      WHERE `id_product` = ' . $idProduct;
    
            $db = Db::getInstance();
    
            if (!$db->execute($sql1) || !$db->execute($sql2)) {
                return false;
            }
    
            // Vider le cache produit pour refléter le nouveau prix
            Cache::clean('Product::getPriceStatic_' . $idProduct . '_*');
    
            return true;
        }
    }
    

    Points critiques de cette implémentation

    Utilisation du préfixe de table : ne jamais écrire ps_product en dur. La constante _DB_PREFIX_ garantit la compatibilité avec toute installation PrestaShop, quel que soit le préfixe configuré.

    Cast des paramètres : le (int) et (float) sont la première ligne de défense contre les injections SQL. Même si la validation se fait en amont dans le contrôleur, la méthode doit être autonome en termes de sécurité.

    Table product_shop : en contexte multiboutique, le prix est stocké à la fois dans ps_product et ps_product_shop. Oublier l'une des deux tables provoque des incohérences difficiles à diagnostiquer.

    Invalidation du cache : PrestaShop met en cache les prix calculés. Sans invalidation, l'ancien prix reste affiché jusqu'à l'expiration du cache.

    PrestaShop 8.x — Approche modernisée

    Sur PrestaShop 8.x, les overrides restent supportés mais la bonne pratique est de passer par un module. Voici l'équivalent propre :

    
    <?php
    // modules/monmodule/src/Service/CustomPriceUpdater.php
    
    namespace MonModule\Service;
    
    use Db;
    use Cache;
    
    class CustomPriceUpdater
    {
        public function updatePrice(int $idProduct, float $price, int $idShop = null): bool
        {
            if ($idProduct <= 0 || $price < 0) {
                return false;
            }
    
            $db = Db::getInstance();
    
            $result = $db->update('product', [
                'price' => $price,
            ], 'id_product = ' . $idProduct);
    
            $shopCondition = $idShop
                ? 'id_product = ' . $idProduct . ' AND id_shop = ' . (int) $idShop
                : 'id_product = ' . $idProduct;
    
            $result &= $db->update('product_shop', [
                'price' => $price,
            ], $shopCondition);
    
            // Invalider le cache Symfony + cache PS natif
            Cache::clean('Product::getPriceStatic_' . $idProduct . '_*');
    
            return (bool) $result;
        }
    }
    

    L'utilisation de Db::update() avec un tableau associatif est plus lisible et bénéficie de l'échappement automatique de PrestaShop.

    Étape 2 : Override du ProductController

    Le contrôleur intercepte la soumission du formulaire, valide les données et appelle la méthode de mise à jour.

    Fichier : override/controllers/front/ProductController.php

    
    <?php
    /**
     * @author Alexandre Carette <contact@alexandrecarette.fr>
     * @copyright 2026 Alexandre Carette
     * @license Propriétaire et Confidentiel
     */
    
    class ProductController extends ProductControllerCore
    {
        public function postProcess()
        {
            // Appel du traitement parent (ajout panier, etc.)
            parent::postProcess();
    
            if (Tools::isSubmit('isChangePrice')) {
                $this->processCustomPrice();
            }
        }
    
        protected function processCustomPrice()
        {
            $idProduct = (int) Tools::getValue('id_product');
            $prix = Tools::getValue('prix');
    
            // --- Validation stricte ---
    
            // Le champ prix est-il rempli ?
            if (empty($prix)) {
                $this->errors[] = $this->trans(
                    'Veuillez saisir un prix.',
                    [],
                    'Shop.Notifications.Error'
                );
                return;
            }
    
            // Le format est-il un prix valide ?
            if (!Validate::isPrice($prix)) {
                $this->errors[] = $this->trans(
                    'Le format du prix n\'est pas valide.',
                    [],
                    'Shop.Notifications.Error'
                );
                return;
            }
    
            $prix = (float) $prix;
    
            // Bornes de prix (à adapter selon votre besoin)
            $prixMin = 10.0;
            $prixMax = 30.0;
    
            if ($prix <= $prixMin) {
                $this->errors[] = $this->trans(
                    'Le prix doit être supérieur à %min% €.',
                    ['%min%' => $prixMin],
                    'Shop.Notifications.Error'
                );
                return;
            }
    
            if ($prix >= $prixMax) {
                $this->errors[] = $this->trans(
                    'Le prix doit être inférieur à %max% €.',
                    ['%max%' => $prixMax],
                    'Shop.Notifications.Error'
                );
                return;
            }
    
            // Le produit existe-t-il ?
            if (!Validate::isLoadedObject(new Product($idProduct))) {
                $this->errors[] = $this->trans(
                    'Produit introuvable.',
                    [],
                    'Shop.Notifications.Error'
                );
                return;
            }
    
            // --- Mise à jour ---
            if (Product::updateCustomPrice($idProduct, $prix)) {
                $this->context->smarty->assign('confirm', true);
                // Redirection propre pour éviter le double-submit
                Tools::redirect(
                    $this->context->link->getProductLink($idProduct)
                    . '?price_updated=1'
                );
            } else {
                $this->errors[] = $this->trans(
                    'Erreur lors de la mise à jour du prix.',
                    [],
                    'Shop.Notifications.Error'
                );
            }
        }
    }
    

    Détail des validations

    ValidationMéthodeRôle Champ vide`empty()`Empêche la soumission sans valeur Format prix`Validate::isPrice()`Vérifie que la chaîne est un nombre décimal valide Borne basseComparaison `<=`Garantit un prix minimum (protection marchande) Borne hauteComparaison `>=`Évite les abus (prix déraisonnablement élevé) Produit valide`Validate::isLoadedObject()`Protection contre la manipulation du champ caché `id_product`

    Sécurité renforcée : la validation native Validate::isPrice() de PrestaShop vérifie le format (/^[0-9]{1,10}(\.[0-9]{1,9})?$/). Combinée au cast (int) sur l'ID produit et (float) sur le prix, cette chaîne de validation protège contre les injections SQL.

    Étape 3 : Template front-end

    PrestaShop 1.6 (Smarty)

    Ajoutez ce bloc dans themes/votre-theme/product.tpl, à l'emplacement souhaité (typiquement sous le prix affiché) :

    
    {* Formulaire de prix libre *}
    <div class="custom-price-form">
        <h3>{l s='Choisissez votre prix'}</h3>
        <p class="price-hint">
            {l s='Vous pouvez définir un prix entre'}
            <strong>11 €</strong> {l s='et'} <strong>29 €</strong>
        </p>
    
        {if isset($confirm) && $confirm}
            <div class="alert alert-success">
                {l s='Prix mis à jour avec succès !'}
            </div>
        {/if}
    
        {if isset($errors) && $errors|@count > 0}
            <div class="alert alert-danger">
                <ul>
                    {foreach from=$errors item=error}
                        <li>{$error}</li>
                    {/foreach}
                </ul>
            </div>
        {/if}
    
        <form action="{$link->getProductLink($product)}" method="post" class="form-inline">
            <input type="hidden" value="{$product->id}" name="id_product">
            <div class="form-group">
                <label for="custom-price">{l s='Votre prix (€ HT)'}</label>
                <input
                    type="number"
                    id="custom-price"
                    name="prix"
                    min="11"
                    max="29"
                    step="0.01"
                    class="form-control"
                    placeholder="15.00"
                    required
                >
            </div>
            <button type="submit" name="isChangePrice" class="btn btn-primary">
                {l s='Valider mon prix'}
            </button>
        </form>
    </div>
    

    PrestaShop 1.7 / 8.x (Twig via module)

    Pour les versions modernes, le formulaire s'intègre via un hook dans un module :

    
    {# views/templates/hook/custom-price-form.tpl #}
    {# Ou en Twig si module Symfony : templates/front/custom-price.html.twig #}
    
    <div class="custom-price-form card mt-3 p-3">
        <h3 class="h5">{{ 'Choisissez votre prix'|trans({}, 'Modules.Monmodule.Shop') }}</h3>
    
        {% if app.request.query.get('price_updated') %}
            <div class="alert alert-success">
                {{ 'Prix mis à jour avec succès !'|trans({}, 'Modules.Monmodule.Shop') }}
            </div>
        {% endif %}
    
        <form method="post" action="{{ product_url }}">
            <input type="hidden" name="id_product" value="{{ product.id }}">
            <div class="form-group">
                <label for="custom-price">{{ 'Votre prix (€ HT)'|trans({}, 'Modules.Monmodule.Shop') }}</label>
                <input
                    type="number"
                    id="custom-price"
                    name="prix"
                    min="{{ price_min }}"
                    max="{{ price_max }}"
                    step="0.01"
                    class="form-control"
                    required
                >
            </div>
            <button type="submit" name="isChangePrice" class="btn btn-primary mt-2">
                {{ 'Valider mon prix'|trans({}, 'Modules.Monmodule.Shop') }}
            </button>
        </form>
    </div>
    

    Limiter le prix libre à une catégorie spécifique

    Dans de nombreux cas, seule une catégorie de produits doit proposer le prix libre (par exemple, une catégorie « Dons » ou « Prix libre »). La condition porte sur la catégorie par défaut du produit :

    En Smarty (1.6)

    
    {if $product->id_category_default == 42}
        {* Afficher le formulaire de prix libre *}
        {include file="./custom-price-form.tpl"}
    {/if}
    

    En PHP (contrôleur ou module)

    
    // Vérifier si le produit appartient à la catégorie "prix libre"
    $idCategoryPrixLibre = (int) Configuration::get('MON_MODULE_CATEGORY_FREE_PRICE');
    
    if ((int) $product->id_category_default === $idCategoryPrixLibre) {
        // Afficher le formulaire
    }
    

    Stocke l'ID de catégorie dans la configuration du module (Configuration::updateValue()) plutôt que de le coder en dur. Cela permet de le modifier depuis le back-office sans toucher au code.

    Bonnes pratiques et points de vigilance

    Sécurité

    • **Token CSRF** : ajoutez un token anti-CSRF dans le formulaire. PrestaShop 1.7+ le gère nativement via `$this->context->controller->getToken()`.
    • **Validation côté serveur ET client** : l'attribut HTML `min`/`max` sur l'input est un confort UX, mais ne remplace jamais la validation PHP.
    • **Ne jamais utiliser `die()`** : dans un contexte web, `die('Erreur')` affiche un message brut sans mise en page. Utilisez `$this->errors[]` pour une gestion propre des erreurs.

    Performance

    • **Invalidation de cache** : après la mise à jour du prix, il faut invalider le cache produit. Sans cela, les pages catégorie et les blocs de prix afficheront l'ancien montant.
    • **Attention au multiboutique** : en contexte multi-shop, filtrez la mise à jour par `id_shop` pour ne pas impacter toutes les boutiques.

    Limites de l'approche par mise à jour directe

    Cette technique modifie le prix du produit en base de données. Cela signifie que :

    • Si deux clients visitent le produit simultanément, le dernier prix soumis écrase le précédent
    • L'historique des prix n'est pas conservé
    • Les règles de prix spécifiques (promotions, groupes clients) peuvent entrer en conflit

    Alternative recommandée pour la production

    Pour un usage en production avec du trafic concurrent, privilégiez une approche par prix spécifique lié au panier :

    
    // Créer un prix spécifique temporaire pour ce client
    $specificPrice = new SpecificPrice();
    $specificPrice->id_product = $idProduct;
    $specificPrice->id_customer = (int) $this->context->customer->id;
    $specificPrice->id_shop = (int) $this->context->shop->id;
    $specificPrice->id_currency = 0;
    $specificPrice->id_country = 0;
    $specificPrice->id_group = 0;
    $specificPrice->price = $prix;
    $specificPrice->from_quantity = 1;
    $specificPrice->reduction = 0;
    $specificPrice->reduction_type = 'amount';
    $specificPrice->from = date('Y-m-d H:i:s');
    $specificPrice->to = date('Y-m-d H:i:s', strtotime('+1 hour'));
    $specificPrice->add();
    

    Cette méthode est thread-safe : chaque client a son propre prix spécifique, sans collision possible.

    Régénérer le cache des classes après un override

    Après avoir créé ou modifié un override, supprimez le fichier de cache des classes :

    
    # PrestaShop 1.6
    rm -f cache/class_index.php
    
    # PrestaShop 1.7 / 8.x
    rm -f var/cache/prod/class_index.php
    rm -f var/cache/dev/class_index.php
    
    # Ou depuis le back-office : Paramètres avancés > Performances > Vider le cache
    

    Sans cette étape, PrestaShop continuera d'utiliser les classes originales et votre override sera ignoré silencieusement.

#override #prix #Product #ProductController #formulaire #validation #name-your-price

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.