Composite Pattern in Magento 2 | Price Rules and Tree Structures
AI generated
Magento 2 · Composite Pattern

Composite Pattern
in Magento 2

The Composite Pattern makes single objects and entire tree structures usable through the same interface. In Magento 2 this shows up especially in price rules, Sales Rule Conditions, layout containers and configuration trees.

12 min read PHP 8.4 Magento 2.4.8

1. What is the Composite Pattern?

The Composite Pattern in Magento 2 describes a structural pattern where single objects and groups of objects are treated through the same interface. A single element is often called a "leaf." A group that can contain further elements is the "composite." The calling code does not need to know whether it is currently processing a single object or an entire object tree.

The benefit becomes clear as soon as you model tree structures. A price rule can consist of a single condition: "category is 12." But it can also contain a group: "category is 12 AND customer group is B2B AND cart value is greater than 100." That group can in turn contain subgroups. Structures like this are ideal for the Composite Pattern in Magento 2, because every condition and every group can offer the same method, for example validate().

The pattern reduces special cases. Without composite, your code would constantly have to ask: "Is this a single node or a group?" With composite, the code simply processes an interface. A group internally calls the same method on its children. A single element only checks itself. This makes rules, layouts, menus, categories, permission trees and other nested models significantly easier to maintain.


<?php
declare(strict_types=1);

/**
 * Common contract for a rule condition node.
 */
interface ConditionInterface
{
    /**
     * Validates the given subject against this condition node.
     */
    public function validate(array $subject): bool;
}

/**
 * Leaf condition that checks one field.
 */
final readonly class FieldCondition implements ConditionInterface
{
    public function __construct(
        private string $field,
        private mixed $expectedValue
    ) {}

    public function validate(array $subject): bool
    {
        return ($subject[$this->field] ?? null) === $this->expectedValue;
    }
}

/**
 * Composite condition that validates all child conditions.
 */
final class AllConditions implements ConditionInterface
{
    /**
     * @param ConditionInterface[] $conditions
     */
    public function __construct(
        private readonly array $conditions
    ) {}

    public function validate(array $subject): bool
    {
        foreach ($this->conditions as $condition) {
            if (!$condition->validate($subject)) {
                return false;
            }
        }

        return true;
    }
}

This simplified example shows the core idea: FieldCondition and AllConditions implement the same interface. The group knows its children, but the client only knows ConditionInterface. The Composite Pattern in Magento 2 is therefore not just an academic pattern, but a very practical solution for shop rules.

2. Composite Pattern in Magento 2

In Magento 2, the Composite Pattern appears in several places. It is most visible in Sales Rules and Catalog Price Rules. There, condition trees are built from individual conditions and groups. A rule might check, for example, whether a product belongs to a category, whether a cart value has been reached, or whether a customer belongs to a specific customer group. These conditions are not processed as a flat list, but as a tree.

The layout system is also a good example. A single block can render HTML. A container can hold multiple blocks or containers. For template code that is often irrelevant: it calls getChildHtml(), and Magento processes the underlying structure. This uniformity is exactly what is typical for the Composite Pattern in Magento 2.

Further examples include configuration trees, menu structures, category hierarchies, UI component structures, and to some extent price calculations. Not every one of these is a textbook-perfect implementation. Magento has grown historically. But the architectural principle is clear: nested structures are traversed, validated or rendered in a uniform way.


<?php
declare(strict_types=1);

namespace Mironsoft\Rule\Service;

use Magento\SalesRule\Api\Data\RuleInterface;
use Magento\Quote\Api\Data\CartInterface;

/**
 * Demonstrates how a rule service can stay independent from concrete condition trees.
 */
final class CartRuleValidator
{
    /**
     * Validates the cart by delegating to the rule condition tree.
     */
    public function isRuleApplicable(RuleInterface $rule, CartInterface $cart): bool
    {
        $conditions = $rule->getCondition();

        if ($conditions === null) {
            return true;
        }

        return (bool) $conditions->validate($cart);
    }
}

The decisive point here is not the individual class, but the shape of the collaboration. The validator does not need to know every possible condition. It delegates to the condition tree. New conditions can be added without the central flow needing to know all the details. This is how the Composite Pattern in Magento 2 supports the Open/Closed Principle: extensions arrive as new nodes, not as ever growing if-else blocks.

3. Price rules and Sales Rule Conditions

Price rules are the most practical entry point into the Composite Pattern in Magento 2. In Adminhtml, the merchant assembles conditions visually: "If ALL conditions are true" or "If ANY condition is true." Underneath sit individual product, cart or customer conditions. Magento stores this structure and later reconstructs it as a condition tree.

A condition group is itself a condition again. It just has a different job: it does not decide based on a field, but based on its children. An "ALL" group only returns true if every child is valid. An "ANY" group returns true as soon as one child is valid. Individual conditions, on the other hand, check concrete values. This shared interface is exactly what makes the Composite Pattern in Magento 2 such a good fit for price rules.

In Magento projects, bugs often arise when developers bypass the condition structure and write their own special-case logic next to the rule engine. That works in the short term but becomes hard to maintain. It is better to integrate custom conditions cleanly into the existing structure. Then the merchant can use them in the admin, and the rule engine processes them like any other condition.


<?php
declare(strict_types=1);

namespace Mironsoft\SalesRule\Model\Rule\Condition;

use Magento\Rule\Model\Condition\AbstractCondition;

/**
 * Custom condition that checks whether the cart contains a configured product attribute value.
 */
class HasProductAttributeValue extends AbstractCondition
{
    /**
     * Defines the condition metadata for the admin condition tree.
     */
    public function loadAttributeOptions(): self
    {
        $this->setAttributeOption([
            'mironsoft_product_attribute_value' => __('Product has configured attribute value')
        ]);

        return $this;
    }

    /**
     * Validates the quote item or quote context against the configured condition.
     */
    public function validate(\Magento\Framework\Model\AbstractModel $model): bool
    {
        $attributeCode = (string) $this->getAttribute();
        $expectedValue = (string) $this->getValue();
        $product = $model->getProduct();

        if (!$product) {
            return false;
        }

        return (string) $product->getData($attributeCode) === $expectedValue;
    }
}

This example is deliberately compact. In a real module you would build the condition more cleanly via dependency injection, configuration and admin options. Still, the idea comes through: a custom condition becomes part of the tree. It behaves like a leaf node and can be combined by groups. The Composite Pattern in Magento 2 ensures that the rule engine does not need to distinguish between a core condition and a custom condition.

4. Layout trees and containers

Alongside price rules, the layout system is a second important composite structure. Magento layouts consist of blocks, containers and nested children. A block renders concrete output. A container organizes children and positioning. Templates frequently work with $block->getChildHtml(). The calling code does not always need to know how deep the structure underneath goes.

This is especially relevant for Hyva projects. The Hyva block approach should not be replaced. When a template iterates via $block->getChildNames() or uses getChildHtml(), the layout structure stays extensible. Modules can add new blocks via layout XML without hard-coding changes into the template. This is composite thinking in everyday Magento frontend work.

Anyone who instead hard-codes direct HTML fragments into templates loses this extensibility. A composite system depends on parent nodes being able to accept children and on the render process staying uniform. That is why changes should be controlled cleanly through layout XML.


<?php
declare(strict_types=1);

/**
 * Simplified template example for rendering child blocks.
 *
 * @var \Magento\Framework\View\Element\Template $block
 */
?>

<div class="footer-links">
    <?php foreach ($block->getChildNames() as $childName): ?>
        <?= $block->getChildHtml($childName) ?>
    <?php endforeach; ?>
</div>

The template treats every child the same. Whether behind it lies a simple link block, a container with several children, or a more complex component, the layout decides. This is precisely why the Composite Pattern in Magento 2 also fits the frontend architecture. The structure stays extensible without every caller needing to know the details.

5. Building a custom composite

Custom composite structures make sense when your module needs nested rules, validations or calculations. Examples include B2B approvals, product feed filters, dynamic price surcharges, customer segment rules or shipping logic. The important thing is to first define a clear interface. Leaf nodes and composite nodes implement this interface together.

The final design should stay simple. If a node validates, call the method validate(). If a node renders, call it render(). If a node calculates an amount, call it calculate(). The Composite Pattern in Magento 2 works well precisely when the shared contract is genuinely natural.


<?php
declare(strict_types=1);

namespace Mironsoft\Approval\Api;

/**
 * Defines a common approval rule node.
 */
interface ApprovalRuleInterface
{
    /**
     * Checks whether the approval request satisfies this rule node.
     */
    public function isSatisfiedBy(ApprovalContextInterface $context): bool;
}

<?php
declare(strict_types=1);

namespace Mironsoft\Approval\Model\Rule;

use Mironsoft\Approval\Api\ApprovalContextInterface;
use Mironsoft\Approval\Api\ApprovalRuleInterface;

/**
 * Leaf rule that checks the order total.
 */
final readonly class MinimumOrderTotalRule implements ApprovalRuleInterface
{
    public function __construct(
        private float $minimumTotal
    ) {}

    public function isSatisfiedBy(ApprovalContextInterface $context): bool
    {
        return $context->getOrderTotal() >= $this->minimumTotal;
    }
}

<?php
declare(strict_types=1);

namespace Mironsoft\Approval\Model\Rule;

use Mironsoft\Approval\Api\ApprovalContextInterface;
use Mironsoft\Approval\Api\ApprovalRuleInterface;

/**
 * Composite rule that requires every child rule to be satisfied.
 */
final readonly class AllApprovalRules implements ApprovalRuleInterface
{
    /**
     * @param ApprovalRuleInterface[] $rules
     */
    public function __construct(
        private array $rules
    ) {}

    public function isSatisfiedBy(ApprovalContextInterface $context): bool
    {
        foreach ($this->rules as $rule) {
            if (!$rule->isSatisfiedBy($context)) {
                return false;
            }
        }

        return true;
    }
}

This structure is testable. You can test leaf rules in isolation and test composite rules with mocks or real small rules. At the same time it is extensible. New rules require no change to the composite itself. That is the most important advantage over a single central class with many conditions.

6. Comparison: Composite vs. Strategy vs. Decorator

The Composite Pattern in Magento 2 is easily confused with other patterns. Strategy encapsulates interchangeable algorithms. Decorator dynamically extends an object with behavior. Composite, on the other hand, models part-whole structures. If you have a tree in which individual nodes and groups should be treated the same way, Composite is usually the right candidate.

Pattern Purpose Magento example
Composite Treat single objects and groups uniformly Sales Rule Conditions, layout trees, menu structures
Strategy Encapsulate interchangeable algorithms Shipping carrier, payment methods, price calculation
Decorator Extend the behavior of an object Service extension via wrappers or plugins
Chain of Responsibility Pass a request through multiple handlers Router, validator chains, import processing

The test is simple: if you have nested nodes and every node should perform the same operation, that strongly favors Composite. If you only want to choose one of several calculation methods, Strategy is better. If you want to extend existing behavior with additional logic, Decorator, or in Magento often a plugin, is the better fit.

Mironsoft

Magento 2 architecture, price rules and Hyva frontends

Need to model complex Magento rules cleanly?

We build Magento 2 modules with clear service contracts, testable rule structures, clean layout extensions and Hyva-compatible frontends without Luma dependencies.

Price rules

Extend Sales Rule Conditions and Catalog Rule logic maintainably

Architecture

Correctly separate Composite, Strategy, Repository and Plugins

Hyva

Extend layout trees and the block system cleanly via XML

8. Summary

The Composite Pattern in Magento 2 is especially valuable for tree structures. Price rules, Sales Rule Conditions, layout containers and menu structures benefit from having individual nodes and groups processed through the same interface. This keeps central services lean, since they do not need to know every special case.

In your own modules, you should use Composite when you are modeling nested rules or part-whole structures. Define a clear interface, keep leaf nodes small, and make composite nodes responsible for combining their children. Business decisions stay in services, while the structure itself stays in the composite.

Composite Pattern in Magento 2, the essentials at a glance

Main purpose

Treat individual objects and groups of objects through the same interface.

Magento examples

Sales Rule Conditions, Catalog Price Rules, layout trees, containers and menu structures.

Good use cases

Nested rules, validation trees, render trees and hierarchical configurations.

Caution

Do not force composite onto flat algorithms. For interchangeable calculations, Strategy is often the better fit.

9. FAQ: Composite Pattern in Magento 2

1 What is the Composite Pattern in Magento 2?
A pattern where individual objects and groups are treated through the same interface. In Magento 2 this shows up in price rules, layout trees and containers.
2 Why does Composite fit price rules?
Price rules consist of individual conditions and condition groups. Groups contain further conditions and are themselves validated like a condition.
3 What is a leaf?
A leaf is a single node without children, for example a condition for a product attribute, customer group or cart value.
4 What is a composite node?
A composite node contains further nodes. For rules, this is a group that combines child conditions with ALL or ANY logic.
5 Is the layout system a composite?
Conceptually yes. Blocks and containers form nested render structures that are extended via layout XML and output via getChildHtml().
6 How do you extend Sales Rule Conditions?
Via a custom condition class that is integrated into the condition selection. The rule engine then processes it like other nodes in the tree.
7 Composite or Strategy?
Composite fits nested nodes and part-whole structures. Strategy fits interchangeable algorithms without a tree structure.
8 Should Composite always be used?
No. It pays off for genuine tree structures. For simple flat workflows, Composite would be unnecessary complexity.
9 How do you test composite structures?
Test leaf nodes in isolation, test composite nodes with small rules or mocks. AND, OR and short-circuit logic are especially important to cover.
10 Does Composite fit Hyva?
Yes. Hyva templates should respect the Magento layout structure and render children via getChildNames() or getChildHtml() instead of hard-replacing them.