Important

You are browsing documentation for version 6.0 of OroCommerce, supported until 2028. Read the documentation for the latest LTS version to get up-to-date information.

See our Release Process documentation for more information on the currently supported and upcoming releases.

Price Rule Expression Language 

Price lists in OroCommerce can be populated automatically based on the product assignment rule and the price calculation rules. Both are written in an expression language, for example:

product.category.id == 12 and product.margin > 0.1        // product assignment rule
pricelist[1].prices.value * 0.9                           // price calculation rule

Use this article to learn how the expression language is implemented and how to extend it. For the syntax used to create expressions, see Filtering Expression Syntax and Price Rule Automation Examples.

Architecture Overview 

The expression language consists of three layers:

  • Oro\Component\Expression — a generic, reusable engine built on top of the Symfony ExpressionLanguage Component. It parses an expression into a tree of nodes and converts this tree into Doctrine query expressions. The component has no knowledge of specific entities.

  • OroProductBundle integration layer (Oro\Bundle\ProductBundle\Expression namespace, oro_product.expression.* services) — binds the engine to the application entities via the query designer, registers the product root alias, and provides the field information and autocomplete data.

  • OroPricingBundle extensions — register the pricelist root alias, the price-specific converters and preprocessors, and compile the price rules into SQL queries.

Expressions are processed in the following stages:

  1. Preprocessing. The raw expression string is passed through the registered preprocessors (ExpressionPreprocessorInterface) which perform string-level substitutions. For example, ProductAssignmentRuleExpressionPreprocessor replaces the pricelist[N].productAssignmentRule reference with the actual assignment rule expression of the referenced price list. Preprocessors run repeatedly until the expression no longer changes.

  2. Parsing. ExpressionParser parses the expression with the Symfony ExpressionLanguage parser and converts the resulting Symfony AST into the Oro node tree with ExpressionLanguageConverter. Parsed trees are cached.

  3. Query conversion. The node tree is converted into a query designer definition (NodeToQueryDesignerConverter) that produces the base Doctrine QueryBuilder with all necessary joins, and QueryExpressionBuilder converts the nodes into Doctrine\ORM\Query\Expr parts of that query.

  4. Execution. The price rule compilers build an INSERT ... SELECT query from the result, so prices are generated by the database in bulk, without hydrating the entities.

Node Types 

ExpressionLanguageConverter produces a node tree consisting of the following node types (the Oro\Component\Expression\Node namespace):

  • NameNode — a reference to an entity field, e.g. product.sku. Holds the container class, the field name, and an optional container id (see below).

  • RelationNode — a reference to a field of a related entity, e.g. product.category.id or pricelist[1].prices.value. A maximum of three segments is allowed; deeper relation paths are rejected with a syntax error.

  • BinaryNode — a binary operation. The node normalizes equivalent operators (=== to ==, && to and, matches to like, and so on) and knows whether it represents a boolean or a math operation.

  • UnaryNodenot, unary - and +.

  • ValueNode — a literal value: number, string, or array.

A reference without a field (product.category) is automatically expanded to the identity field of the related entity (product.category.id). The subscript syntax (pricelist[1]) sets the container id of the node and is allowed for root variables only; this is how a particular price list is referenced. Function calls are not supported by design, as every expression must be convertible to SQL.

Supported Syntax 

  • Math operators: +, -, *, /, %

  • Comparison operators: ==, !=, >, <, >=, <=

  • Logical operators: and, or, not

  • String matching: matches (converted to SQL LIKE)

  • Sets: in, not in with array literals (product.id in [1, 2, 3]) or field references

  • Literals: numbers, strings, arrays, true, false, null (comparison with null is converted to IS NULL / IS NOT NULL)

By default, two root aliases are registered:

  • product — the Product entity, including the price attribute relations (e.g., product.msrp.value).

  • pricelist — the PriceList entity; pricelist[N].prices refers to the prices of the price list with id N, and the virtual fields pricelist[N].assignedProducts and pricelist[N].productAssignmentRule are handled by the pricing-specific extensions described below.

The set of fields available in expressions is controlled by Oro\Bundle\ProductBundle\Expression\FieldsProvider (service oro_product.expression.fields_provider, an implementation of FieldsProviderInterface). It exposes the numeric field types (integer, float, money, decimal) and to-one relations, and supports explicit white and black lists per entity.

Compiling Rules into Prices 

Oro\Bundle\PricingBundle\Compiler\PriceListRuleCompiler (service oro_pricing.compiler.price_list_rule_compiler) compiles a PriceRule into a QueryBuilder that selects all data required to insert the generated ProductPrice records: the product, unit, currency, quantity, and the calculated value. The compiler performs the following steps:

  • combines the rule condition with the calculation expressions and non-null guards into a single expression and converts it into a query;

  • adds division-by-zero safeguard conditions for every division and modulo operation used in the expressions;

  • restricts the result to the products assigned to the price list and skips the products with prices entered manually;

  • rounds the calculated value according to the oro_pricing.price_calculation_precision configuration option and filters out negative values.

Oro\Bundle\PricingBundle\Builder\ProductPriceBuilder then executes the compiled query as an INSERT ... SELECT through the shard query executor, applying the price rules in the order of their priorities. Product assignment rules are processed the same way by ProductAssignmentRuleCompiler.

Autocomplete 

The expression editor in the back-office receives its data from the autocomplete providers (the Oro\Bundle\ProductBundle\Expression\Autocomplete namespace). AutocompleteFieldsProviderInterface::getDataProviderConfig() builds the configuration for the JS expression editor: the root entities (taken from the parser name mappings), the allowed field types, and the field labels. Oro\Bundle\PricingBundle\Form\OptionsConfigurator\PriceRuleEditorOptionsConfigurator passes this configuration to the price rule editor form types. Use AutocompleteFieldsProviderInterface::addSpecialFieldInformation() to expose virtual fields (this is how assignedProducts and productAssignmentRule appear in the autocomplete).

Extending the Expression Language 

The expression services are extended not with service tags but with method calls added to the shared oro_product.expression.* services from a bundle compiler pass. The reference implementation is Oro\Bundle\PricingBundle\DependencyInjection\CompilerPass\ProductExpressionServicesPass, which registers all pricing-specific extensions. A custom bundle can do the same:

namespace Acme\Bundle\DemoBundle\DependencyInjection\CompilerPass;

use Acme\Bundle\DemoBundle\Entity\Warehouse;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class ExpressionServicesPass implements CompilerPassInterface
{
    #[\Override]
    public function process(ContainerBuilder $container)
    {
        // Register a new root alias available in expressions, e.g. warehouse[N]
        $container->getDefinition('oro_product.expression.parser')
            ->addMethodCall('addNameMapping', ['warehouse', Warehouse::class]);

        // Allow a field that is filtered out by default
        $container->getDefinition('oro_product.expression.fields_provider')
            ->addMethodCall('addFieldToWhiteList', [Warehouse::class, 'stockLevels']);

        // Register a custom node-to-DQL converter
        $container->getDefinition('oro_product.expression.query_expression_builder')
            ->addMethodCall(
                'registerConverter',
                [new Reference('acme_demo.expression.stock_levels_converter'), 10]
            );

        // Register a custom string-level preprocessor
        $container->getDefinition('oro_product.expression.preprocessor')
            ->addMethodCall(
                'registerPreprocessor',
                [new Reference('acme_demo.expression.preprocessor')]
            );
    }
}

The available extension points:

  • New root entity. Call addNameMapping($alias, $className) on oro_product.expression.parser. The alias becomes available as a root variable in expressions, and its fields are resolved through the fields provider.

  • Field visibility. Call addFieldToWhiteList($className, $fieldName) or addFieldToBlackList($className, $fieldName) on oro_product.expression.fields_provider to expose or hide specific fields.

  • Custom query conversion. Implement Oro\Component\Expression\QueryExpressionConverter\QueryExpressionConverterInterface and register it with registerConverter($converter, $sortOrder) on oro_product.expression.query_expression_builder. Converters are consulted in the descending sort order until one returns a non-null result, so a converter with a higher sort order takes precedence and can override the default node handling. All out-of-the-box converters are registered with the default sort order 0, so any positive value is enough to be consulted first. Implement ConverterAwareInterface if the converter needs to convert its operand sub-nodes. An example is the pricing AssignedProductsConverter, registered with the sort order 10, that converts the product.id in pricelist[N].assignedProducts condition into an EXISTS subquery on the PriceListToProduct relation.

  • Expression preprocessing. Implement Oro\Component\Expression\Preprocessor\ExpressionPreprocessorInterface and register it with registerPreprocessor() on oro_product.expression.preprocessor to expand macros or virtual references before parsing, as the pricing ProductAssignmentRuleExpressionPreprocessor does for pricelist[N].productAssignmentRule.

  • Query designer columns. Implement Oro\Component\Expression\ColumnInformationProviderInterface and register it with addColumnInformationProvider() on oro_product.expression.node_to_query_designer_converter to teach the query designer converter how to map your nodes to the definition columns.

  • Extra joins. Implement Oro\Bundle\ProductBundle\Expression\QueryConverterExtensionInterface and register it with addExtension() on oro_product.expression.query_converter to add the joins required by your nodes and return the table alias mapping used by the query expression converters. The pricing PriceListQueryConverterExtension adds the price list and product price joins this way.

  • Autocomplete data. Call addSpecialFieldInformation() on oro_product.autocomplete_fields_provider to expose virtual fields in the expression editor autocomplete.

Related Topics