---
title: Cart Validation Strategies | Shopware Community Hub
description: >-
  Implement custom cart validation to ensure data integrity and prevent invalid
  orders.
canonical_url: 'https://hub.shopware.com/learn/unit/cart-validation-strategies'
---

# Cart Validation Strategies

<LearningObjectives>

- Understand how Shopware's cart validation works and when it is executed.
- Create custom cart validators for business rules and constraints.
- Work with blocking and non-blocking validation errors.
- Test validation logic with unit and integration tests.

</LearningObjectives>

# Cart Validation Strategies

Cart validation is like having a smart bouncer at a club — it checks everyone before they enter and stops problems before they become disasters.

In Shopware, cart validation runs **after the cart has been fully calculated**. At this point, prices, deliveries, discounts and other cart changes are already applied by cart collector and cart processor.

Validation mainly evaluates whether the calculated cart is in a valid state and can proceed to checkout. In most cases, it's used to prevent invalid orders from being placed rather than to change the cart.

In this learning unit, you'll learn how to create custom validation rules, handle different types of errors, and ensure your validation logic integrates seamlessly with the checkout process using our `AcademyCartExamples` plugin.

## Plugin Implementation

The `AcademyCartExamples` plugin demonstrates the cart validation concepts covered in this learning unit:

```shell
# Clone the repository into your custom/plugins directory of your Shopware project
git clone https://github.com/ShopwareAcademy/AcademyCartExamples.git custom/plugins/AcademyCartExamples

# Checkout the specific tag for this learning unit
cd custom/plugins/AcademyCartExamples
git checkout LP-03-CO-05-LU-03-cart-validation
```

## Why Validate The Cart?

Cart validation protects your business from:

- **Invalid orders** – Out of stock items, discontinued products.
- **Business rule violations** – Minimum order values, customer limits.
- **Data integrity issues** – Missing required information, corrupted data.
- **Security problems** – Price manipulation, unauthorized access.

Without validation, customers could place orders that can't be fulfilled, leading to disappointed customers and operational headaches.

## Types of Validation Errors

Shopware distinguishes between two types of cart errors:

<Callout title="Blocking Errors" type="error">

These prevent the order from being placed:

- Out of stock items
- Invalid shipping addresses
- Missing required customer data
- Payment method not available

</Callout>

<Callout title="Non-Blocking Errors" type="warning">

These show warnings but allow checkout to continue:

- Items with limited availability
- Alternative shipping options are available
- Promotional offers that could apply

</Callout>

## How Validation Works In Shopware

Cart validation happens during the **Validate** phase of cart calculation. At this point, the cart is already fully calculated by collectors and processors.

The validation process works as follows:

1. **Cart is calculated** – All prices, discounts, and shipping are calculated.
2. **Validators are called** – Each registered validator inspects the calculated cart.
3. **Errors are collected** – Validators add blocking or non-blocking errors to the cart.
4. **Cart state is updated** – The cart is marked as valid or invalid based on the collected errors.
5. **User sees results** – Validation errors or warnings are displayed in the storefront.

## Creating A Custom Error Class

First, let's create a custom error class that extends Shopware's base `Error` class:

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

namespace AcademyCartExamples\Cart\Error;

use Shopware\Core\Checkout\Cart\Error\Error;

class MinimumOrderValueError extends Error
{
    private const string KEY = 'academy-minimum-order-value';

    public function __construct(
        private readonly float $currentValue,
        private readonly float $minimumValue,
        private readonly float $missing
    )
    {
        parent::__construct();
    }

    public function getId(): string
    {
        return self::KEY;
    }

    public function getMessageKey(): string
    {
        return self::KEY;
    }

    public function getLevel(): int
    {
        return self::LEVEL_ERROR;
    }

    public function blockOrder(): bool
    {
        return true;
    }

    public function getParameters(): array
    {
        return [
            'currentValue' => $this->currentValue,
            'minimumValue' => $this->minimumValue,
            'missing' => $this->missing
        ];
    }
}
```

This error class is responsible for:

- Identifying the error (ID and message key)
- Defining the log level (error, warning, info)
- Determining whether the order should be blocked (true = block, false = non-blocking)
- Providing parameters for translation

## Creating a Custom Cart Validator

At this point, you know that the validation runs after the cart has been fully calculated. And you have created a custom error class.

Now let's build a cart validator for our `AcademyCartExamples` plugin that enforces a minimum order value for B2B customers:

**Cart Validator:**

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

namespace AcademyCartExamples\Cart;

use AcademyCartExamples\Cart\Error\MinimumOrderValueError;
use AcademyCartExamples\Service\AcademyCartService;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartValidatorInterface;
use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class MinimumOrderValueValidator implements CartValidatorInterface
{
    public function __construct(
        private readonly AcademyCartService $academyCartService
    ) {
    }

    public function validate(Cart $cart, ErrorCollection $errors, SalesChannelContext $context): void
    {
        // Only validate for logged-in B2B customers
        $isB2BCustomer = $this->academyCartService->isB2BCustomer($context);
        if (false === $isB2BCustomer) {
            return;
        }

        $total = $cart->getPrice()->getTotalPrice();
        $minimumValue = AcademyCartService::MINIMUM_ORDER_VALUE; // €100 minimum for B2B

        if ($total < $minimumValue) {
            $missing = $minimumValue - $total;
            $errors->add(new MinimumOrderValueError($total, $minimumValue, $missing));
        }
    }
}

```

**Service Class:**

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

namespace AcademyCartExamples\Service;

use Shopware\Core\System\SalesChannel\SalesChannelContext;

class AcademyCartService
{
    public const float MINIMUM_ORDER_VALUE = 100.00; // Fix value defined here or get from plugin config

    public function isB2BCustomer(SalesChannelContext $context): bool
    {
        // Check if customer has B2B role or company
        $customer = $context->getCustomer();
        if (null === $customer) {
            return false;
        }

        if (null === $customer->getCompany()) {
            return false;
        }

        return true;
    }
}
```

The `validate()` method checks the calculated cart. In this example it verifies whether the cart total is below the minimum order value for B2B customers. If so, it adds a new error to the cart.

## Adding Translation Snippets

Create translation files for the error messages. You can use parameters from the error class to provide contextual information:

**English (`academyCart.en-GB.json`):**

```json
{
    "checkout": {
        "academy-minimum-order-value-missing": "Minimum order value not reached."
    },
    "error": {
        "academy-minimum-order-value": "Minimum order value not reached."
    }
}
```

**German (`academyCart.de-DE.json`):**

```json
{
    "checkout": {
        "academy-minimum-order-value": "Mindestbestellwert nicht erreicht."
    },
    "error": {
        "academy-minimum-order-value": "Mindestbestellwert nicht erreicht."
    }
}
```

The parameters defined in the error class are available in the translation files under the key "checkout". under this you define your snippets and add your parameters as wished.

**Example:**

```json
{
    "checkout": {
        "academy-minimum-order-value": "Mindestbestellwert nicht erreicht. Der aktuelle Wert beträgt %currentValue%. Es fehlen noch %missing%."
    }
}
```

## Registering the Validator

Add the validator to your `services.xml`:

```xml
<service id="AcademyCartExamples\Cart\MinimumOrderValueValidator">
    <argument type="service" id="AcademyCartExamples\Service\AcademyCartService" />
    <tag name="shopware.cart.validator"/>
</service>
```

The tag `<tag name="shopware.cart.validator"/>` tells Shopware to treat this service as a cart validator and execute it during cart validation.

## Testing the Validator

Here's a simple unit test for the validator:

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

namespace AcademyCartExamples\Test\Cart;

use AcademyCartExamples\Cart\MinimumOrderValueValidator;
use PHPUnit\Framework\TestCase;
use Shopware\Core\Framework\Test\TestCaseBase\KernelTestBehaviour;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
use Shopware\Core\Checkout\Cart\Tax\Struct\CalculatedTaxCollection;
use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection;
use Shopware\Core\Checkout\Customer\CustomerEntity;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class MinimumOrderValueValidatorTest extends TestCase
{
    use KernelTestBehaviour;

    private MinimumOrderValueValidator $validator;

    protected function setUp(): void
    {
        $this->validator = $this->getContainer()->get(MinimumOrderValueValidator::class);
    }

    public function testValidatesB2BCustomerMinimumOrder(): void
    {
        $cart = $this->createCartWithTotal(50.00); // Below minimum
        $context = $this->createB2BContext();
        $errors = new ErrorCollection();

        $this->validator->validate($cart, $errors, $context);

        $this->assertTrue($errors->count() > 0);
        $this->assertCount(1, $errors);
        
        $error = $errors->first();
        $this->assertEquals('academy-minimum-order-value', $error->getId());
        $this->assertEquals('academy-minimum-order-value', $error->getMessageKey());
    }

    public function testIgnoresRegularCustomers(): void
    {
        $cart = $this->createCartWithTotal(50.00);
        $context = $this->createRegularCustomerContext();
        $errors = new ErrorCollection();

        $this->validator->validate($cart, $errors, $context);

        $this->assertCount(0, $errors);
    }

    public function testAllowsB2BCustomerAboveMinimum(): void
    {
        $cart = $this->createCartWithTotal(150.00); // Above minimum
        $context = $this->createB2BContext();
        $errors = new ErrorCollection();

        $this->validator->validate($cart, $errors, $context);

        $this->assertCount(0, $errors);
    }

    private function createCartWithTotal(float $total): Cart
    {
        $cart = new Cart('test-cart');
        $cart->setPrice(new CartPrice(
            $total, // netPrice
            $total, // totalPrice
            $total, // positionPrice
            new CalculatedTaxCollection(), // calculatedTaxes
            new TaxRuleCollection(), // taxRules
            CartPrice::TAX_STATE_GROSS // taxStatus
        ));
        
        return $cart;
    }

    private function createB2BContext(): SalesChannelContext
    {
        $customer = new CustomerEntity();
        $customer->setCompany('Acme Corp');
        
        $context = $this->createMock(SalesChannelContext::class);
        $context->method('getCustomer')->willReturn($customer);
        
        return $context;
    }

    private function createRegularCustomerContext(): SalesChannelContext
    {
        $customer = new CustomerEntity();
        // No company set = B2C customer
        
        $context = $this->createMock(SalesChannelContext::class);
        $context->method('getCustomer')->willReturn($customer);
        
        return $context;
    }
}
```

The full example is available in the repository referenced earlier in this learning unit.

## Advanced Validation Patterns

### Line Item Validation

Sometimes you want to validate a specific line item (e.g., regular product line item, discount line item, or custom line item created by your plugin).

Keep the cart validator lightweight and delegate actual checks to a service class.

**Cart Validator:**

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

namespace AcademyCartExamples\Cart;

use AcademyCartExamples\Service\AcademyCartService;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartValidatorInterface;
use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

class ProductAvailabilityValidator implements CartValidatorInterface
{
    public function __construct(
        private readonly AcademyCartService $academyCartService
    ) {
    }

    public function validate(Cart $cart, ErrorCollection $errors, SalesChannelContext $context): void
    {
        // Only validate if cart has line items
        if (empty($cart->getLineItems()->getElements())) {
            return;
        }

        foreach ($cart->getLineItems() as $lineItem) {
            if ($lineItem->getType() === LineItem::PRODUCT_LINE_ITEM_TYPE) {
                $this->academyCartService->validateProductLineItem($lineItem, $errors);
            }
        }
    }
}
```

**Service Class:**

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

namespace AcademyCartExamples\Service;

use Shopware\Core\Checkout\Cart\Error\Error;
use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
use Shopware\Core\Checkout\Cart\Error\GenericCartError;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;

class AcademyCartService
{
    // ...
    // Your existing methods
    // ...
    
    public function validateProductLineItem(LineItem $lineItem, ErrorCollection $errors): void
    {
        // Check if product is still available
        if (false === $this->isProductAvailable($lineItem->getReferencedId())) {
            $errors->add(new GenericCartError(
                'ACADEMY_PRODUCT_UNAVAILABLE',
                'Product is no longer available',
                ['productId' => $lineItem->getReferencedId()],
                Error::LEVEL_ERROR,
                true,  // blockOrder
                false, // persistent
                false  // blockResubmit
            ));
        }
    }

    private function isProductAvailable(string $productId): bool
    {
        // In a real implementation, you would check the product's availability
        // For this example, we'll simulate some products being unavailable
        $unavailableProducts = ['unavailable-product-1', 'discontinued-product-2'];

        if (true === in_array($productId, $unavailableProducts, true)) {
            return false;
        }

        return true;
    }
}
```

**Registration in the service.xml file:**

```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="AcademyCartExamples\Service\AcademyCartService"/>
      
        <service id="AcademyCartExamples\Cart\ProductAvailabilityValidator">
            <argument type="service" id="AcademyCartExamples\Service\AcademyCartService" />
            <tag name="shopware.cart.validator"/>
        </service>

      
    </services>
</container>
```

### Conditional Validation

Make validation rules context-aware:

```php
public function validate(Cart $cart, ErrorCollection $errors, SalesChannelContext $context): void
{
    // Different rules for different sales channels
    if ($context->getSalesChannel()->getTypeId() === 'b2b-sales-channel') {
        $this->validateB2BRules($cart, $errors, $context);
    } else {
        $this->validateB2CRules($cart, $errors, $context);
    }
}
```

## Cart Validation: Best Practices

When working with cart validation, keep the following best practices in mind:

- **Keep validators focused** – One validator per business rule.
- **Make errors actionable** – Tell users how to fix the problem.
- **Use meaningful error IDs** – Help with debugging and translations.
- **Test thoroughly** – Validation bugs can break checkout.
- **Consider performance** – Avoid expensive database queries in validators.

## Common Validation Scenarios

In real-world projects, cart validation if often used for use cases like:

- **Stock validation** – Ensure products are available.
- **Customer limits** – Apply per-customer order restrictions.
- **Geographic restrictions** – Restrict shipping to certain countries.
- **Time-based rules** – Validate promotions with time limits.
- **Payment method validation** – Check whether a payment method is available.
- **Address validation** – Verify shipping or billing addresses.

## Summary

In this learning unit, you learned how cart validation works in Shopware:

- When the cart validation is executed.
- How to create a custom error class.
- How to create a simple cart validator for business rules and register it in the DI container.
- How validation errors are collected and displayed to the customer.
- How blocking and non-blocking errors work.
- Common validation scenarios and best practices.

With this knowledge, you can validate calculated carts, prevent invalid orders, and implement clear, maintainable business rules in the checkout process.
