Extend Discounts¶
By extending Discounts, you can increase flexibility and control over how promotions are applied to suit your unique business rules. Together with the existing events and the Discounts PHP API, extending discounts gives you the ability to cover additional use cases related to selling products.
Tip
If you prefer learning from videos, two presentations from Ibexa Summit 2025 cover the Discounts feature:
- Introduction to the Discounts system in Ibexa DXP by Konrad Oboza
- Extending new Discounts to suit your needs by Paweł Niedzielski
Create custom conditions and rules¶
With custom conditions and rules you can create more advanced discounts that apply only in specific scenarios.
For both of them, you need to specify their logic with Symfony's expression language.
Available expressions¶
You can use the following built-in expressions (variables and functions) in your own custom conditions and rules. You can also create your own.
| Type | Name | Value | Available for |
|---|---|---|---|
| Function | get_current_region() |
Region object of the current siteaccess. | Conditions, rules |
| Function | is_in_category() |
true/false, depending if a product belongs to given product categories. |
Conditions, rules |
| Function | is_user_in_customer_group() |
true/false, depending if an user belongs to given customer groups. |
Conditions, rules |
| Function | calculate_purchase_amount() |
Purchase amount, calculated for all products in the cart before the discounts are applied. | Conditions, rules |
| Function | is_product_in_product_codes() |
Parameters: - Product object - array of product codes Returns true if the product is part of the given list. |
Conditions, rules |
| Function | is_valid_discount_code() |
Parameter: discount code (string). Returns true if the discount code is valid for current user. |
Conditions, rules |
| Variable | cart |
Cart object associated with current context. | Conditions, rules |
| Variable | currency |
Currency object of the current siteaccess. | Conditions, rules |
| Variable | customer_group |
Customer group object associated with given price context or the current user. | Conditions, rules |
| Variable | product |
Product object | Conditions, rules |
| Variable | amount |
Original price of the product | Rules |
Custom expressions¶
You can create your own variables and functions to make creating the conditions easier. The examples below show how to add an additional variable and a function to the available ones:
- New variable:
current_user_registration_date
It's a DateTime object with the registration date of the currently logged-in user.
To add it, create a class implementing the DiscountVariablesResolverInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | |
And mark it as a service using the ibexa.discounts.expression_language.variable_resolver service tag:
1 2 3 | |
- New function:
is_anniversary()
It's a function returning a boolean value indicating if today is the anniversary of the date passed as an argument.
The function accepts an optional argument, tolerance, allowing you to extend the range of dates that are acccepted as anniversaries.
This implementation is simplified and does not cover the approach for accounts created on February 29 during leap years.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
Mark it as a service using the ibexa.discounts.expression_language.function service tag and specify the function name in the service definition.
1 2 3 4 | |
Two new expressions are now available for use in custom conditions and rules.
When deciding whether to register a new custom variable or function, consider the following:
- variables are always evaluated by the expression engine and the result is available for all the rules and conditions specified in the discount
- functions are invoked only when the rule or condition using them is evaluated. If there are multiple conditions using them, they will be invoked multiple times
For performance reasons, it's recommended to:
- use variables only for lightweight calculations
- use functions for resource-intensive calculations (for example, checking customer's order history)
- implement caching (for example, in-memory) for function results to avoid redundant calculations when multiple discounts expressions might use the function
- specify the most resource-intensive conditions as the last to evaluate. As all conditions must be met for the discount to apply, it's possible to skip evaluating them if the previous ones won't be met
In a production implementation, you should consider refactoring the current_user_registration_date variable into a get_current_user_registration_date function to avoid always loading the current user object and improve performance.
Implement custom condition¶
The following example creates a new discount condition. It allows you to offer a special discount for customers on the date when their account was created, making use of the expressions added above.
Create the condition by creating a class implementing the DiscountConditionInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | |
This condition can be used in both catalog and cart discounts.
To implement a cart-only discount, additionally implement the marker CartDiscountConditionInterface interface.
The tolerance option is made available for usage in the expression by passing it in the constructor.
The getExpression() method contains the logic of the condition, expressed using the variables and functions available in the expression engine.
The expression must evaluate to true or false, indicating whether the condition is met.
The example uses three expressions:
- the custom
is_anniversary()function, returning a value indicating whether today is user's registration anniversary - the custom
current_user_registration_datevariable, holding the value of current user's registration date - the custom
tolerancevariable, holding the acceptable tolerance (in days) for the calculation
For each custom condition class, you must create a dedicated condition factory, a class implementing the \Ibexa\Discounts\Repository\DiscountCondition\DiscountConditionFactoryInterface inteface.
This allows you to create conditions when working in the context of the Symfony service container.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
Mark it as a service using the ibexa.discounts.condition.factory service tag and specify the condition's identifier.
1 2 3 4 | |
You can now use the condition, for example by using the PHP API or data migrations:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
To learn how to integrate it into the back office, see Extend Discounts wizard.
Implement custom rules¶
The following example implements a purchasing power parity discount, adjusting product's price in the cart based on buyer's region.
You could use it, for example, in regions sharing the same currency and apply the rule only to them by using the IsInRegions condition.
To implement a custom rule, create a class implementing the DiscountRuleInterface.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | |
The getExpression() method contains the logic of the rule, expressed using the variables and functions available in the expression engine.
The expression must return the new price of the product.
It uses three expressions:
- the built-in
amountvariable, holding the purchase amount - the built-in
get_current_region()function, returning the current region - a custom
power_parity_mapvariable, holding the purchasing power partity map. It's defined in the constuctor.
As with conditions, create a dedicated rule factory:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Then, mark it as a service using the ibexa.discounts.rule.factory service tag and specify the rule's type.
1 2 3 4 | |
You can now use the rule with the PHP API, but to use it within the back office and storefront you need to:
- integrate it into the Discounts wizard
- implement a new value formatter
Custom discount value formatting¶
You can adjust how each discount type is displayed when using the ibexa_discounts_render_discount_badge Twig function by implementing a custom formatter.
You must implement a custom formatter for each custom rule.
To do it, create a class implementing the DiscountValueFormatterInterface and use the ibexa.discounts.value.formatter service tag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
1 2 3 4 | |
Change discount priority¶
You can change the the default discount priority by creating a class implementing the DiscountPrioritizationStrategyInterface and aliasing to it the default implementation.
The example below decorates the default implementation to prioritize recently updated discounts above all the others. It uses one of the existing discount search criterions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
1 2 3 4 | |