Nicolas Dabene
Back to blog
05 March 2026 Nicolas Dabene 5 min

Evolution of CartRule.php: PrestaShop 9.0.x → 9.1.x

Context

PrestaShop PHP CartRule Development Feature Flag Discounts 9.1.x Discount System PrestaShop & E-commerce prestashop-ecommerce
Evolution of CartRule.php: PrestaShop 9.0.x → 9.1.x

Context

Context

The 9.1.x branch of PrestaShop introduces a new Discount system, protected behind a feature flag. This structural change is directly reflected in classes/CartRule.php, which serves as the foundation for the new system while maintaining backward compatibility with the legacy cart rules system.

Sources: DevDocs 9.1.x, Official Blog, Core Monthly January 2026, GitHub develop branch.


1. New imports and dependencies

9.0.x

The CartRuleCore class in 9.0.x uses classic PrestaShop imports without dependency on the new discount system.

9.1.x (develop)

New use statements appear at the top of the file:

<span class="kn">use</span> <span class="nc">PrestaShop\PrestaShop\Adapter\ContainerFinder</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">PrestaShop\PrestaShop\Adapter\Discount\Application\DiscountApplicationService</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">PrestaShop\PrestaShop\Core\Domain\CartRule\CartRuleSettings</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">PrestaShop\PrestaShop\Core\Domain\Discount\DiscountSettings</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject\DiscountType</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">PrestaShop\PrestaShop\Core\FeatureFlag\FeatureFlagSettings</span><span class="p">;</span>
<span class="kn">use</span> <span class="nc">PrestaShop\PrestaShop\Core\FeatureFlag\FeatureFlagStateCheckerInterface</span><span class="p">;</span>

Impact: The CartRule class now integrates with:

  • DiscountType: a Value Object that distinguishes application levels (ORDER_LEVEL, PRODUCT_LEVEL)
  • DiscountApplicationService: the service that orchestrates discount application in the new system
  • FeatureFlagSettings / FeatureFlagStateCheckerInterface: to dynamically check if the new Discount system is enabled

2. Discount Feature Flag Integration

New method: isDiscountFeatureFlagEnabled()

A key addition in 9.1.x is the private method that checks the feature flag state:

<span class="k">private</span> <span class="k">function</span> <span class="n">isDiscountFeatureFlagEnabled</span><span class="p">():</span> <span class="kt">bool</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">getFeatureFlagManager</span><span class="p">()</span> <span class="o">!==</span> <span class="kc">null</span>
        <span class="o">&&</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">getFeatureFlagManager</span><span class="p">()</span><span class="o">-></span><span class="nf">isEnabled</span><span class="p">(</span><span class="nc">FeatureFlagSettings</span><span class="o">::</span><span class="no">FEATURE_FLAG_DISCOUNT</span><span class="p">);</span>
<span class="p">}</span>

Role: This method acts as a switch throughout the file. When the flag is disabled (default), behavior remains identical to 9.0.x. When enabled, new calculation logic takes over.

New method: getFeatureFlagManager()

Protected method that instantiates the FeatureFlagStateCheckerInterface from the Symfony container:

<span class="k">protected</span> <span class="k">function</span> <span class="n">getFeatureFlagManager</span><span class="p">():</span> <span class="kt">?FeatureFlagStateCheckerInterface</span>


3. New concept: getType() — Discount Typing

Added in 9.1.x

The getType() method is introduced to classify a cart rule according to the new discount model with 4 types:

Type Description Catalog (PRODUCT_LEVEL) Discount applied to a product or product segment Cart (ORDER_LEVEL) Discount on total cart amount (excluding shipping) Free Shipping Free shipping Free Gift Free product added to order This method returns a DiscountType Value Object based on the internal configuration of the cart rule (fields reduction_product, free_shipping, gift_product, etc.).

Developer impact: Modules that query the discount type can now use $cartRule->getType() instead of manually checking individual fields.


4. Changes in getContextualValue()

The getContextualValue() method — the core of discount calculation — contains the most significant changes.

4.1 Percentage reduction at order level (ORDER_LEVEL)

9.0.x: Percentage reduction is applied only to product total.

9.1.x: When the feature flag is enabled and type is ORDER_LEVEL:

<span class="k">if</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nb">getType</span><span class="p">()</span> <span class="o">===</span> <span class="nc">DiscountType</span><span class="o">::</span><span class="no">ORDER_LEVEL</span>
    <span class="o">&&</span> <span class="nv">$this</span><span class="o">-></span><span class="n">reduction_percent</span> <span class="o">></span> <span class="mf">0.00</span>
    <span class="o">&&</span> <span class="nv">$this</span><span class="o">-></span><span class="n">reduction_product</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$order_products_total</span> <span class="o">=</span> <span class="nv">$context</span><span class="o">-></span><span class="n">cart</span><span class="o">-></span><span class="nf">getOrderTotal</span><span class="p">(</span><span class="nv">$use_tax</span><span class="p">,</span> <span class="nc">Cart</span><span class="o">::</span><span class="no">ONLY_PRODUCTS</span><span class="p">,</span> <span class="nv">$package_products</span><span class="p">);</span>
    <span class="nv">$order_shipping_total</span> <span class="o">=</span> <span class="nv">$context</span><span class="o">-></span><span class="n">cart</span><span class="o">-></span><span class="nf">getOrderTotal</span><span class="p">(</span><span class="nv">$use_tax</span><span class="p">,</span> <span class="nc">Cart</span><span class="o">::</span><span class="no">ONLY_SHIPPING</span><span class="p">,</span> <span class="nv">$package_products</span><span class="p">);</span>
    <span class="nv">$order_total</span> <span class="o">=</span> <span class="nv">$order_products_total</span> <span class="o">+</span> <span class="nv">$order_shipping_total</span><span class="p">;</span>
    <span class="nv">$reduction_value</span> <span class="o">+=</span> <span class="nv">$order_total</span> <span class="o">*</span> <span class="nv">$this</span><span class="o">-></span><span class="n">reduction_percent</span> <span class="o">/</span> <span class="mi">100</span><span class="p">;</span>
<span class="p">}</span>

Key change: The percentage now applies to products + shipping costs instead of products only.

4.2 Fixed amount reduction — capped differently

9.0.x: Reduction amount is capped at product total.

9.1.x: For ORDER_LEVEL with feature flag enabled, the cap includes shipping:

<span class="k">if</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nf">isDiscountFeatureFlagEnabled</span><span class="p">()</span> <span class="o">&&</span> <span class="nv">$this</span><span class="o">-></span><span class="nb">getType</span><span class="p">()</span> <span class="o">===</span> <span class="nc">DiscountType</span><span class="o">::</span><span class="no">ORDER_LEVEL</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$max_reduction_amount</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">reduction_tax</span>
        <span class="o">?</span> <span class="nv">$cart_amount_ti</span> <span class="o">+</span> <span class="nv">$context</span><span class="o">-></span><span class="n">cart</span><span class="o">-></span><span class="nf">getOrderTotal</span><span class="p">(</span><span class="kc">true</span><span class="p">,</span> <span class="nc">Cart</span><span class="o">::</span><span class="no">ONLY_SHIPPING</span><span class="p">,</span> <span class="nv">$package_products</span><span class="p">)</span>
        <span class="o">:</span> <span class="nv">$cart_amount_te</span> <span class="o">+</span> <span class="nv">$context</span><span class="o">-></span><span class="n">cart</span><span class="o">-></span><span class="nf">getOrderTotal</span><span class="p">(</span><span class="kc">false</span><span class="p">,</span> <span class="nc">Cart</span><span class="o">::</span><span class="no">ONLY_SHIPPING</span><span class="p">,</span> <span class="nv">$package_products</span><span class="p">);</span>
    <span class="nv">$reduction_amount</span> <span class="o">=</span> <span class="nb">min</span><span class="p">(</span><span class="nv">$reduction_amount</span><span class="p">,</span> <span class="nv">$max_reduction_amount</span><span class="p">);</span>
<span class="p">}</span>

4.3 Current cart amount calculation

When a fixed amount reduction is applied (reduction_amount > 0), the $current_cart_amount calculation now includes shipping costs for ORDER_LEVEL discounts:

<span class="k">if</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nf">isDiscountFeatureFlagEnabled</span><span class="p">()</span> <span class="o">&&</span> <span class="nv">$this</span><span class="o">-></span><span class="nb">getType</span><span class="p">()</span> <span class="o">===</span> <span class="nc">DiscountType</span><span class="o">::</span><span class="no">ORDER_LEVEL</span><span class="p">)</span> <span class="p">{</span>
    <span class="nv">$current_cart_amount</span> <span class="o">+=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">reduction_tax</span>
        <span class="o">?</span> <span class="nv">$context</span><span class="o">-></span><span class="n">cart</span><span class="o">-></span><span class="nf">getOrderTotal</span><span class="p">(</span><span class="kc">true</span><span class="p">,</span> <span class="nc">Cart</span><span class="o">::</span><span class="no">ONLY_SHIPPING</span><span class="p">,</span> <span class="nv">$package_products</span><span class="p">)</span>
        <span class="o">:</span> <span class="nv">$context</span><span class="o">-></span><span class="n">cart</span><span class="o">-></span><span class="nf">getOrderTotal</span><span class="p">(</span><span class="kc">false</span><span class="p">,</span> <span class="nc">Cart</span><span class="o">::</span><span class="no">ONLY_SHIPPING</span><span class="p">,</span> <span class="nv">$package_products</span><span class="p">);</span>
<span class="p">}</span>


5. Bug fix: variable inconsistency (PR #40424)

PR #40424 by @pjouglet fixes variable usage inconsistency throughout the file.

Before: Some variables were used inconsistently (e.g., mixing $cart_rule->name and htmlspecialchars($cart_rule->name)).

After: Normalization of variable usage throughout the file, particularly in voucher compatibility error messages:

<span class="c1">// 9.1.x - with htmlspecialchars for XSS security</span>
<span class="k">return</span> <span class="p">(</span><span class="o">!</span><span class="nv">$display_error</span><span class="p">)</span> <span class="o">?</span> <span class="kc">false</span> <span class="o">:</span> <span class="nv">$this</span><span class="o">-></span><span class="nf">trans</span><span class="p">(</span>
    <span class="s1">'This voucher is not combinable with an other voucher already in your cart: %s'</span><span class="p">,</span>
    <span class="p">[</span><span class="nb">htmlspecialchars</span><span class="p">(</span><span class="nv">$cart_rule</span><span class="o">-></span><span class="n">name</span><span class="p">)],</span>
    <span class="s1">'Shop.Notifications.Error'</span>
<span class="p">);</span>


6. New discount compatibility system

9.0.x

The compatibility system relies on the ps_cart_rule_combination table and the cart_rule_restriction field. Each forbidden combination is stored as a pair in the table, causing exponential data explosion with many rules.

9.1.x

The new system introduces compatibility by discount type:

  1. Discounts are no longer ordered by promo code vs. automatic
  2. Fixed application order:
    • Catalog (product) → Cart (cart) → Free Shipping → Free Gift
  3. Within the same type: sorted by priority (lower = applied first), then by creation date
  4. Dynamic re-evaluation on each cart modification

7. Impact on CartRuleCalculator.php

While this file is separate from CartRule.php, the changes are intrinsically linked. The CartRuleCalculator (in src/Core/Cart/) integrates the same feature flag checks:

<span class="k">if</span> <span class="p">(</span><span class="nv">$cartRule</span><span class="o">-></span><span class="nb">getType</span><span class="p">()</span> <span class="o">===</span> <span class="nc">DiscountType</span><span class="o">::</span><span class="no">ORDER_LEVEL</span>
    <span class="o">&&</span> <span class="p">(</span><span class="n">float</span><span class="p">)</span> <span class="nv">$cartRule</span><span class="o">-></span><span class="n">reduction_percent</span> <span class="o">></span> <span class="mi">0</span>
    <span class="o">&&</span> <span class="nv">$cartRule</span><span class="o">-></span><span class="n">reduction_product</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nv">$this</span><span class="o">-></span><span class="nf">isDiscountFeatureFlagEnabled</span><span class="p">())</span> <span class="p">{</span>
        <span class="c1">// New calculation: products + shipping</span>
        <span class="nv">$initialShippingFees</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">calculator</span><span class="o">-></span><span class="nf">getFees</span><span class="p">()</span><span class="o">-></span><span class="nf">getInitialShippingFees</span><span class="p">();</span>
        <span class="nv">$productsTotal</span> <span class="o">=</span> <span class="nv">$this</span><span class="o">-></span><span class="n">calculator</span><span class="o">-></span><span class="nf">getRowTotal</span><span class="p">();</span>
        <span class="nv">$orderTotal</span> <span class="o">=</span> <span class="nv">$productsTotal</span><span class="o">-></span><span class="nf">add</span><span class="p">(</span><span class="nv">$initialShippingFees</span><span class="p">);</span>
        <span class="c1">// ...</span>
    <span class="p">}</span>
<span class="p">}</span>


8. Associated database changes

The 9.1.x introduces schema modifications to support the new discount system (visible in install-dev/upgrade/sql/9.1.0.sql). These changes are linked to the feature flag and don’t alter existing tables as long as the flag remains disabled.


9. Changes summary table

Aspect 9.0.x 9.1.x Imports No Discount dependency DiscountType, DiscountSettings, FeatureFlagSettings Feature Flag Absent isDiscountFeatureFlagEnabled() present Discount typing Implicit (individual fields) Explicit via getType()DiscountType % on ORDER_LEVEL Products only Products + shipping Fixed amount cap Product total Product total + shipping (ORDER_LEVEL) XSS security messages $cart_rule->name htmlspecialchars($cart_rule->name) Discount compatibility cart_rule_combination table By type with fixed application order Backward compatibility — ✅ Flag disabled = 9.0.x behavior ---

10. Recommendations for module developers

  1. Don’t override CartRule.php if you plan to support 9.1.x — the file is actively evolving.

  2. Check the feature flag in your modules if you interact with discounts: ``` $featureFlagManager = $this->get(FeatureFlagStateCheckerInterface::class); if ($featureFlagManager->isEnabled(FeatureFlagSettings::FEATURE_FLAG_DISCOUNT)) { // New system logic }

  3. Test both modes (flag enabled and disabled) as merchants can switch between both.

  4. Use getType() instead of manually checking reduction_product, free_shipping, gift_product etc.

  5. Be careful with shipping calculation: if your module calculates discount totals, behavior changes based on discount type in 9.1.x.

🔑#### Key Takeaways — CartRule.php in PrestaShop 9.1.x

What every PrestaShop module developer must remember about the CartRule evolution from 9.0.x to 9.1.x:

  1. The feature flag protects backward compatibility. The new Discount system is disabled by default — 9.1.x behaves exactly like 9.0.x as long as the flag remains off. No forced migration pressure.
  2. ORDER_LEVEL calculation changes fundamentally. With the flag enabled, percentage and fixed-amount discounts now apply to products + shipping, not products alone. Your cart calculation tests must cover both modes.
  3. Use getType() instead of individual fields. The new method cleanly encapsulates the discount type (Catalog, Cart, Free Shipping, Free Gift) — no more manually checking reduction_product, free_shipping, gift_product.
  4. Don’t override CartRule.php. This file is actively evolving. Use hooks and Symfony services instead to interact with the discount system.
  5. Test both flag states in your CI: flag disabled (9.0.x behavior) and flag enabled (new system). It’s the only way to guarantee compatibility across your clients’ stores.

Sources and references

LinkedIn

Follow my AI and e-commerce analysis

I share practical notes on AI agents, PrestaShop architecture, MCP and automation for e-commerce teams.

Follow on LinkedIn