---
title: Cart Processing Pipeline and Order Conversion | Shopware Community Hub
description: >-
  Understand how cart collectors, cart processors, and validators work together
  during cart recalculation and how the validated cart becomes the basis for
  order…
canonical_url: >-
  https://hub.shopware.com/learn/unit/cart-processing-pipeline-and-order-conversion
---

# Cart Processing Pipeline and Order Conversion

<LearningObjectives>

- Understand how cart recalculation fits into the checkout and order conversion flow.
- Distinguish the responsibilities of cart collectors, cart processors, and cart validators.
- Understand how `CartDataCollection` connects collectors and processors during one recalculation.
- Explain the difference between `$original` and `$toCalculate` in a cart processor.
- Understand why cart processors must be safe to run multiple times.

</LearningObjectives>

# Cart Processing Pipeline and Order Conversion

In the previous learning unit, you learned what a cart is, how line items are structured, and why Shopware recalculates the cart when relevant cart data changes.

This learning unit zooms into what happens during that recalculation and how the calculated cart becomes the basis for checkout and order conversion.

You will learn how cart collectors, cart processors, and cart validators work together, and why each part has a separate responsibility in the cart calculation flow.

## From Recalculation to Order Conversion

Before an order can be placed, Shopware must prepare the cart, calculate it, validate it, and only then use the validated cart data for order creation.

A simplified flow looks like this:

Cart changes
→ Cart recalculation starts
→ Collectors prepare required data
→ Processors apply cart logic
→ Validators check the calculated cart
→ Validated cart can continue to checkout
→ Cart data is converted into order data

This flow is important for plugin development because different extension points have different responsibilities. Loading data, changing the cart, and validating the final result should not all happen in the same place.

## Cart Processing Pipeline

The cart processing pipeline is the sequence of steps that turns a changed cart into a calculated and validated cart. A simplified pipeline looks like this:

```txt
┌─────────────────────────────────────────────────────────────────┐
│                    CART PROCESSING PIPELINE                     │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────┐
│ Cart change             │
│ ┌─────────────────────┐ │
│ │ - Product added     │ │
│ │ - Quantity changed  │ │
│ │ - Context changed   │ │
│ └─────────────────────┘ │
└─────────┬───────────────┘
          │
          ▼
┌───────────────────────────┐
│ Cart recalculation starts │
└─────────┬─────────────────┘
          │
          ▼
┌─────────────────────────┐
│ Cart Collectors         │
│ ┌─────────────────────┐ │
│ │ - Load data         │ │
│ │ - Prepare data      │ │
│ └─────────────────────┘ │
└─────────┬───────────────┘
          │ Stores data in "CartDataCollection"
          ▼
┌─────────────────────────┐
│ Cart Processors         │
│ ┌──────────────────┐    │
│ │ Apply cart logic │    │ 
│ │ "$toCalculate"   │    │ 
│ └──────────────────┘    │
└─────────┬───────────────┘
          │ Modifies the cart state
          ▼
┌─────────────────────────┐
│ Cart Validators         │
│ ┌─────────────────────┐ │
│ │ Check whether the   │ │ 
│ │ calculated cart is  │ │ 
│ │ allowed to continue │ │ 
│ └─────────────────────┘ │
└─────────┬───────────────┘
          │
          ▼
┌─────────────────────────┐
│ Validated Cart          │
│ ┌─────────────────────┐ │
│ │ Can continue to     │ │ 
│ │ checkout and order  │ │ 
│ │ conversion          │ │ 
│ └─────────────────────┘ │
└─────────────────────────┘
```

Each step has a different responsibility:

- **Cart Collectors**: Load or prepare data that is needed by cart processors.
- **Cart Processors**: Apply business logic to the cart.
- **Cart Validators**: Check whether the final calculated cart is valid.

This separation keeps the cart recalculation and order conversion process organized and makes cart extensions easier to maintain.

A collector should not change the cart. A processor should not load expensive data. A validator should not calculate prices.

The more complex your checkout becomes, the more important this separation is. It helps you understand where a problem belongs and where a custom extension should be implemented.

## Cart Collector

Cart collectors prepare data for the cart calculation and run before cart processors.

Their main task is to load or prepare data and store it in the `CartDataCollection`. Cart processors can then read this data later during the same cart recalculation.

Technically, a cart collector is a class that implements the `CartDataCollectorInterface` and provides a `collect()` method.

### When Should You Use a Cart Collector?

Use a cart collector when you need to prepare data before cart logic is applied by the cart processor.

Typical use cases are:

- Loading additional entity data from the database.
- Reading plugin configuration.
- Fetching customer information.
- Loading product information (prices, stock, taxes, etc.).
- Calling an external API to fetch data.
- Preparing flags or metadata for later use.
- Other ways to get data into the cart.

As a rule of thumb, data preparation belongs in a cart collector.

### Minimal Example

The following example shows a small cart collector that loads additional data from a service and stores it in the `CartDataCollection`.

The service class is only an example of getting data from an external service. In a real-world scenario, you would use a service that interacts with the database or an external API.

```php
<?php declare(strict_types=1);

namespace Swag\BasicExample\Core\Checkout\Cart;

use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartBehavior;
use Shopware\Core\Checkout\Cart\CartDataCollectorInterface;
use Shopware\Core\Checkout\Cart\LineItem\CartDataCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class CustomCartCollector implements CartDataCollectorInterface
{
    public function __construct(
        private readonly ExampleDataService $exampleDataService
    ) { 
    }

    public function collect(
        CartDataCollection $data, 
        Cart $original, 
        SalesChannelContext $context, 
        CartBehavior $behavior
    ): void 
    {      
        // Example only
        // Assume this method loads data from an external API
        $exampleData = $this->exampleDataService->loadExampleData($context);
        
        // Store the prepared data under a unique key so processors can read it later
        $data->set('academy.exampleData', $exampleData);
    }
}
```

The important part is this: `$data->set('academy.exampleData', $exampleData);`. The `CartDataCollection` stores the data under a unique key and acts as a shared data container between cart collectors and cart processors during the same recalculation.

Use clear and unique keys, usually prefixed with your plugin or feature name. This helps avoid collisions with other cart extensions.

## Cart Processor

Cart processors apply cart logic during the cart calculation. They run after cart collectors and can use the data stored in the `CartDataCollection`.

Technically, a cart processor is a class that implements the `CartProcessorInterface` and provides a `process()` method.

### When Should You Use a Cart Processor?

Use a cart processor when you need to apply logic to the cart during recalculation.

Typical use cases are:

- Calculating or adjusting line item prices.
- Adding and removing custom line items.
- Applying price definitions and price calculators.
- Applying custom cart logic, such as project-specific discounts.
- Calculate taxes.
- Calculate shipping costs.
- Setting custom payload data.
- Other ways to modify the cart.

### Minimal Example

The following example shows a small cart processor that reads data prepared by a cart collector and shows where cart logic would be applied.

```php
<?php declare(strict_types=1);

namespace Swag\BasicExample\Core\Checkout\Cart;

use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartBehavior;
use Shopware\Core\Checkout\Cart\CartProcessorInterface;
use Shopware\Core\Checkout\Cart\LineItem\CartDataCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class CustomCartProcessor implements CartProcessorInterface 
{ 
    public function process(
        CartDataCollection $data,
        Cart $original,
        Cart $toCalculate,
        SalesChannelContext $context,
        CartBehavior $behavior
    ): void 
    { 
       // Read data that was prepared by a cart collector
       $exampleData = $data->get('academy.exampleData');
       if (null === $exampleData) { 
          return;
       }
       
       // Apply your cart logic to $toCalculate here.
       // This is the cart object Shopware continues to calculate.
       foreach ($toCalculate->getLineItems() as $lineItem) { 
        // Example only:
        // You could set payload data, add/remove custom line items,
        // or apply price definitions and calculators in real projects.
       }
    }
}
```

The processor reads data from the `CartDataCollection` prepared by the cart collector and applies the necessary changes to the `$toCalculate` cart.

The `process()` method also receives two parameters of the type `Cart`.

- The `$original` represents the cart state before this calculation step, so the actual state of the cart.
- The `$toCalculate` is the cart that is currently being calculated. It will become the cart of your target state, which is the final result of the calculation.

So if your processor changes the cart, apply those changes to `$toCalculate`.

<Callout title="Important" type="info">

Implement early return conditions to avoid unnecessary processing. For example, if the customer is not a B2B customer, return early without modifying the cart.

</Callout>

<Callout title="Did You Know?" type="info">

Shopware's cart calculation is designed to be stateless and cacheable.

Because cart recalculation can run multiple times, cart processors should avoid introducing side effects that could break this design principle.

</Callout>

### Why Cart Processors Must Be Safe to Run Multiple Times

Cart processors are not guaranteed to run only once during a cart calculation.

Shopware may process the cart multiple times until the calculated cart and the matching rules are stable. This is necessary because cart data and rules can depend on each other.

For example:

1. The cart is processed.
2. A processor adds or removes a line item, changes a price definition, or affects the cart total.
3. Shopware checks which rules match the newly calculated cart.
4. If the matching rules or relevant cart data changed, Shopware processes the cart again.

This is why the same cart processor can be executed multiple times during one cart recalculation flow.

Cart recalculation can also be triggered again later when the cart or context changes, for example when a product is added, a quantity changes, a promotion code is applied, the customer logs in or out, or the shipping or payment method changes.

For this reason, cart processors should always base their logic on the current cart state and avoid applying the same change repeatedly.

#### The Risk: Adding the Same Result Again

For example, imagine a processor that adds a custom discount line item.

A problematic implementation could add a new discount line item every time the processor runs:

```txt
Recalculation #1 → Discount added
Recalculation #2 → Discount added again
Recalculation #3 → Discount added again
```

The cart would contain duplicate discount entries.

#### Example: Problematic Pattern – New Line Item ID on Every Run

Here is a simplified example of the problem:

```php
<?php declare(strict_types=1);

namespace ExamplePlugin\Cart\Processor;

use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartBehavior;
use Shopware\Core\Checkout\Cart\LineItem\CartDataCollection;
use Shopware\Core\Checkout\Cart\CartProcessorInterface;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\Price\QuantityPriceCalculator;
use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class ExampleProcessor implements CartProcessorInterface
{
    public function __construct(
        private readonly QuantityPriceCalculator $quantityPriceCalculator
    ) {
    }

    public function process(
        CartDataCollection $data,
        Cart $original,
        Cart $toCalculate,
        SalesChannelContext $context,
        CartBehavior $behavior
    ): void {
        if (false === $this->hasProductLineItems($toCalculate)) {
            return;
        }

        // Problem: This creates a new ID on every processor run.
        $discount = $this->createDiscountLineItem(Uuid::randomHex(), $context);

        $toCalculate->add($discount);
    }

    private function createDiscountLineItem(string $id, SalesChannelContext $context): LineItem
    {
        $discount = new LineItem($id, LineItem::DISCOUNT_LINE_ITEM);
        $discount->setLabel('Custom discount');
        $discount->setGood(false);
        $discount->setStackable(false);

        $priceDefinition = new QuantityPriceDefinition(
            -10.00,
            new TaxRuleCollection(),
            1
        );

        $discount->setPriceDefinition($priceDefinition);
        $calculatedPrice = $this->quantityPriceCalculator->calculate($priceDefinition, $context);
        $discount->setPrice($calculatedPrice);

        return $discount;
    }

    private function hasProductLineItems(Cart $cart): bool
    {
        return $cart->getLineItems()
            ->filterType(LineItem::PRODUCT_LINE_ITEM_TYPE)
            ->count() > 0;
    }
}
```

**Explanation**

This processor creates a new line item ID every time it runs. Shopware cannot treat the new discount as the same discount as before, because it receives a different ID on each run.

The discount is marked as not being a physical good and not stackable. This is useful metadata for a discount line item, but it does not solve the duplicate problem. The problem is the changing line item ID.

If the cart is recalculated multiple times, the cart can slowly accumulate more and more generated discount line items:

```txt
Product A
Product A + Discount B
Product A + Discount B (1) + Discount B (2) + ... + Discount B (N)
```

This can happen because the cart calculation may run more than once. After the first run, Shopware evaluates the newly calculated cart again. If relevant cart data or matching rules are changed, the processor can be called again with the updated cart state.

In this simplified example, `hasProductLineItems()` is a small method to check whether the cart contains at least one product line item, so the example logic only applies when products are in the cart.

In a real project, the condition can be more specific, for example, checking a customer group, a custom line item type, a sales channel, or data prepared by a cart collector.

#### Safer Pattern: Stable ID and Rebuild

The safer pattern is to make the processor rebuild its own expected result for the current cart. In this example, the processor should leave exactly one custom discount line item in the cart when the condition matches, and no custom discount line item when it does not match:

```php
<?php declare(strict_types=1);

namespace ExamplePlugin\Cart\Processor;

use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartBehavior;
use Shopware\Core\Checkout\Cart\LineItem\CartDataCollection;
use Shopware\Core\Checkout\Cart\CartProcessorInterface;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\Price\QuantityPriceCalculator;
use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class ExampleProcessor implements CartProcessorInterface
{
    private const string CUSTOM_DISCOUNT_ID = 'example-custom-discount';

    public function __construct(
        private readonly QuantityPriceCalculator $quantityPriceCalculator
    ) {
    }

    public function process(
        CartDataCollection $data,
        Cart $original,
        Cart $toCalculate,
        SalesChannelContext $context,
        CartBehavior $behavior
    ): void {
        if (false === $this->hasProductLineItems($toCalculate)) {
            if (true === $toCalculate->has(self::CUSTOM_DISCOUNT_ID)) {
                $toCalculate->remove(self::CUSTOM_DISCOUNT_ID);
            }

            return;
        }

        // Remove the generated result from a previous calculation.
        if (true === $toCalculate->has(self::CUSTOM_DISCOUNT_ID)) {
            $toCalculate->remove(self::CUSTOM_DISCOUNT_ID);
        }

        $discount = $this->createDiscountLineItem(self::CUSTOM_DISCOUNT_ID, $context);

        $toCalculate->add($discount);
    }

    private function createDiscountLineItem(string $id, SalesChannelContext $context): LineItem
    {
        $discount = new LineItem($id, LineItem::DISCOUNT_LINE_ITEM);
        $discount->setLabel('Custom discount');
        $discount->setGood(false);
        $discount->setStackable(false);

        $priceDefinition = new QuantityPriceDefinition(
            -10.00,
            new TaxRuleCollection(),
            1
        );

        $discount->setPriceDefinition($priceDefinition);
        $calculatedPrice = $this->quantityPriceCalculator->calculate($priceDefinition, $context);
        $discount->setPrice($calculatedPrice);

        return $discount;
    }

    private function hasProductLineItems(Cart $cart): bool
    {
        return $cart->getLineItems()
            ->filterType(LineItem::PRODUCT_LINE_ITEM_TYPE)
            ->count() > 0;
    }
}
```

**Explanation:**

The important fixes are:

- The generated discount uses a stable line item ID: `example-custom-discount`.
- If the condition no longer matches, the processor removes its own discount line item if it already exists.
- If the condition matches, the processor removes the old generated discount first if it already exists and then rebuilds it once.
- The generated discount is marked as not being a physical good and not stackable.

In practice, this means your processor should not treat its own previously generated line item as a reason to add another generated result. Instead, identify the result that belongs to your processor, remove or update it when it exists, and then rebuild the expected result from the relevant cart state.

This keeps the processor result stable. No matter how often the processor runs, the cart should contain at most one generated discount line item from this processor.

#### Identify Only Your Own Generated Line Item

Do not check only whether the cart contains any discount line item. Other processors, promotions, or plugins may also add discount line items. Always identify your own generated line item by a stable ID that belongs to your feature.

This example intentionally uses a simple fixed discount amount so the focus stays on stable processor behavior. More realistic discount logic, including dynamic amounts and richer price calculation patterns, is covered in the later learning unit about promotions and discounts.

#### What to Remember

Instead, processors should apply changes in a stable and predictable way. Common approaches are:

- Updating existing data instead of adding duplicate data.
- Using stable line item IDs.
- Overwriting payload values instead of appending new ones.
- Removing and recreating custom line items when necessary.
- Calculating from the current cart state instead of from an already modified value.

**The rule of thumb is**: Do not use each processor run to add "one more" result. Use each run to bring the cart back to the one expected result for the current cart state.

## Cart Validator

Cart validators check whether the final calculated cart is valid and is allowed to continue in the checkout flow. They run after cart collectors and cart processors.

Typical use cases are:

- Checking stock or availability.
- Checking minimum or maximum order values.
- Validating customer-specific restrictions.
- Validating country-specific restrictions.
- Validating payment and shipping conditions.
- Adding blocking or non-blocking cart errors.
- Other ways to check if the cart is valid.

A cart validator usually adds errors or warnings to the cart when something is not valid or blocks the checkout flow (blocking errors), for example, due to restriction of a shipping method or customer-specific restrictions or other business specifications.

<Callout title="Note" type="info">

The next learning unit will cover cart validation in more detail.

</Callout>

## Registration in the DI Container

Cart collectors, cart processors, and cart validators are regular Symfony services.

To make Shopware execute them during cart recalculation, you need to register them in the DI container and add the correct service tag.

```xml
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="Swag\BasicExample\Core\Checkout\Cart\CustomCartCollector">
            <argument type="service" id="Swag\BasicExample\Core\Checkout\Cart\ExampleDataService"/>
            <tag name="shopware.cart.collector" priority="100"/>
        </service>

        <service id="Swag\BasicExample\Core\Checkout\Cart\CustomCartProcessor">
            <tag name="shopware.cart.processor" priority="100"/>
        </service>

        <service id="Swag\BasicExample\Core\Checkout\Cart\CustomCartValidator">
            <tag name="shopware.cart.validator"/>
        </service>
    </services>
</container>
```

The tags tell Shopware when the services should be executed:

- `shopware.cart.collector`: Executes the service during the collection phase.
- `shopware.cart.processor`: Executes the service during the processing phase.
- `shopware.cart.validator`: Executes the service during the validation phase.

The optional `priority` attribute controls the execution order. Services with a higher priority are executed earlier. Use priorities carefully. Only set a custom priority if your collector, processor, or validator must run before or after another cart service.

---

Together, collectors, processors, and validators form a structured pipeline that ensures the cart is complete, correctly calculated, and safe to convert into an order.

## From Validated Cart to Order Conversion

After the cart has been calculated and validated, Shopware can convert it into an order.

This conversion happens in several steps to make sure the order contains everything needed for fulfillment (payment, shipping, and all line items).

```txt
┌─────────────────────────────────────────────────────────────────┐
│                    CART TO ORDER CONVERSION                     │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────┐
│ Validated Cart  │ ← Cart has passed all validation rules
└─────────┬───────┘
          │
          ▼
┌─────────────────┐
│ Order Creation  │ ← Create order entity with basic data
│ ┌─────────────┐ │
│ │ • Customer  │ │
│ │ • Sales     │ │
│ │   Channel   │ │
│ │ • Currency  │ │
│ └─────────────┘ │
└─────────┬───────┘
          │
          ▼
┌─────────────────┐
│ Line Items      │ ← Convert cart line items to order line items
│ Processing      │
│ ┌─────────────┐ │
│ │ • Products  │ │
│ │ • Prices    │ │
│ │ • Quantities│ │
│ └─────────────┘ │
└─────────┬───────┘
          │
          ▼
┌─────────────────┐
│ Payment         │ ← Process payment and create transaction
│ Processing      │
│ ┌─────────────┐ │
│ │ • Payment   │ │
│ │   Method    │ │
│ │ • Amount    │ │
│ │ • Status    │ │
│ └─────────────┘ │
└─────────┬───────┘
          │
          ▼
┌─────────────────┐
│ Delivery        │ ← Create delivery and shipping information
│ Processing      │
│ ┌─────────────┐ │
│ │ • Shipping  │ │
│ │   Address   │ │
│ │ • Method    │ │
│ │ • Costs     │ │
│ └─────────────┘ │
└─────────┬───────┘
          │
          ▼
┌─────────────────┐
│ Order Ready     │ ← Complete order ready for fulfillment
└─────────────────┘
```

## Best Practices for Cart Processors

Cart processors can change the calculated cart state. Because of that, they must be implemented carefully.

When implementing custom cart processors, follow these guidelines:

### Performance Considerations

- **Minimize database queries**: Cache frequently accessed data and avoid N+1 queries inside the cart processor.
- **Use appropriate priorities**: Higher priority processors run first but don't set unnecessarily high values.
- **Check conditions early**: Return early if your processor doesn't apply to avoid unnecessary processing.

### Error Handling

- **Validate input data**: Always check if required data exists before processing.
- **Handle edge cases**: Consider empty carts, invalid line items, and missing context data.
- **Log processing issues**: Use appropriate log levels for debugging and monitoring.

### Testing

- **Unit-test your logic**: Test eligibility rules, payload updates, and edge cases.
- **Mock external dependencies**: Use mocks for `SalesChannelContext` and other dependencies in your tests.
- **Test different scenarios**: For example, B2B vs. B2C customers, different quantities, and invalid data.

### Code Organization

- **Single responsibility**: Each processor should handle one clear business rule.
- **Clear naming**: Use descriptive class and method names that reflect the business logic.
- **Documentation**: Add PHPDoc comments explaining the business rules and cart changes.

### Integration Considerations

- **Consider existing processors**: Be aware of how your processor interacts with built-in ones.
- **Respect cart state**: Avoid modifying the cart in a way that breaks other processors, repeats the same calculation, or leads to inconsistent results.
- **Use appropriate data structures**: Leverage Shopware's cart data structures properly.

## Summary

Well done! In this learning unit, you learned how the cart processing pipeline works in Shopware and how it prepares the cart for checkout and order conversion:

- Cart recalculation prepares the cart before checkout and order conversion.
- Cart collectors load or prepare data and store it in `CartDataCollection`.
- Cart processors apply cart logic to `$toCalculate`, the cart Shopware continues to calculate.
- Cart validators check whether the calculated cart is allowed to continue.
- Collectors, processors, and validators have separate responsibilities.
- Cart processors can run multiple times, so their logic must be stable and based on the current cart state.
- Real discount calculation belongs in a dedicated discount pattern with price definitions and calculators.

With this knowledge, you can decide where custom checkout logic belongs and extend the cart flow without mixing data preparation, cart changes, and validation in the same place.
