Verified on Shopware 6.7
Understanding the Cart Pipeline
Shopware 6 processes the cart through a pipeline of Collectors and Processors:
Cart Request
→ Collector 1 (gather data)
→ Collector 2 (gather data)
→ ...
→ Processor 1 (modify cart)
→ Processor 2 (modify cart)
→ ...
→ Final Cart
Collectors fetch data needed for calculations (prices, rules, external data).
Processors modify the cart (add items, change prices, add discounts, validate).
Example 1: Minimum Order Validation
Prevent checkout if the cart total is below a threshold:
// src/Cart/MinimumOrderValidator.php
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartProcessorInterface;
use Shopware\Core\Checkout\Cart\CartBehavior;
use Shopware\Core\Checkout\Cart\Error\Error;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
class MinimumOrderValidator implements CartProcessorInterface
{
private const MINIMUM_ORDER_VALUE = 25.00;
public function process(
CartDataCollection $data,
Cart $original,
Cart $toCalculate,
SalesChannelContext $context,
CartBehavior $behavior
): void {
$total = $toCalculate->getPrice()->getTotalPrice();
if ($total > 0 && $total < self::MINIMUM_ORDER_VALUE) {
$toCalculate->addErrors(
new MinimumOrderValueError(self::MINIMUM_ORDER_VALUE, $total)
);
}
}
}
The custom error class:
// src/Cart/Error/MinimumOrderValueError.php
use Shopware\Core\Checkout\Cart\Error\Error;
class MinimumOrderValueError extends Error
{
private float $minimum;
private float $current;
public function __construct(float $minimum, float $current)
{
$this->minimum = $minimum;
$this->current = $current;
parent::__construct();
}
public function getId(): string
{
return 'minimum-order-value';
}
public function getMessageKey(): string
{
return 'minimum-order-value-not-reached';
}
public function getLevel(): int
{
return self::LEVEL_ERROR; // Blocks checkout
}
public function blockOrder(): bool
{
return true;
}
public function getParameters(): array
{
return [
'minimum' => $this->minimum,
'current' => $this->current,
];
}
}
Register in services.xml:
<service id="YourPlugin\Cart\MinimumOrderValidator">
<tag name="shopware.cart.processor" priority="4000"/>
</service>
Example 2: Automatic Percentage Discount
Add an automatic 10% discount when cart exceeds a threshold:
// src/Cart/AutoDiscountProcessor.php
class AutoDiscountProcessor implements CartProcessorInterface
{
private const DISCOUNT_THRESHOLD = 100.00;
private const DISCOUNT_PERCENTAGE = 10;
public function process(
CartDataCollection $data,
Cart $original,
Cart $toCalculate,
SalesChannelContext $context,
CartBehavior $behavior
): void {
$lineItems = $toCalculate->getLineItems()->filterType(LineItem::PRODUCT_LINE_ITEM_TYPE);
$productTotal = $lineItems->getPrices()->sum()->getTotalPrice();
if ($productTotal < self::DISCOUNT_THRESHOLD) {
// Remove discount if it exists and threshold not met
$toCalculate->getLineItems()->remove('auto-discount');
return;
}
// Calculate discount amount
$discountAmount = -1 * ($productTotal * self::DISCOUNT_PERCENTAGE / 100);
$discount = new LineItem(
'auto-discount',
LineItem::DISCOUNT_LINE_ITEM,
null,
1
);
$discount->setLabel(sprintf('%d%% Discount (order over %.2f)', self::DISCOUNT_PERCENTAGE, self::DISCOUNT_THRESHOLD));
$discount->setGood(false);
$discount->setRemovable(false);
$discount->setStackable(false);
$discount->setPriceDefinition(
new AbsolutePriceDefinition($discountAmount)
);
$toCalculate->add($discount);
}
}
Example 3: B2B Customer Group Pricing
Apply different pricing tiers based on customer group:
// src/Cart/B2BPricingCollector.php
class B2BPricingCollector implements CartDataCollectorInterface
{
public function collect(
CartDataCollection $data,
Cart $original,
SalesChannelContext $context,
CartBehavior $behavior
): void {
$customerGroup = $context->getCurrentCustomerGroup();
// Fetch tier configuration for this customer group
$tiers = $this->tierRepository->search(
(new Criteria())->addFilter(
new EqualsFilter('customerGroupId', $customerGroup->getId())
),
$context->getContext()
);
// Store in CartDataCollection for the processor
$data->set('b2b_pricing_tiers', $tiers);
}
}
// src/Cart/B2BPricingProcessor.php
class B2BPricingProcessor implements CartProcessorInterface
{
public function process(
CartDataCollection $data,
Cart $original,
Cart $toCalculate,
SalesChannelContext $context,
CartBehavior $behavior
): void {
$tiers = $data->get('b2b_pricing_tiers');
if (!$tiers || $tiers->count() === 0) {
return;
}
foreach ($toCalculate->getLineItems() as $lineItem) {
if ($lineItem->getType() !== LineItem::PRODUCT_LINE_ITEM_TYPE) {
continue;
}
$quantity = $lineItem->getQuantity();
$applicableTier = $this->findApplicableTier($tiers, $quantity);
if ($applicableTier) {
$discountPercent = $applicableTier->getDiscountPercentage();
$originalPrice = $lineItem->getPrice()->getUnitPrice();
$newPrice = $originalPrice * (1 - $discountPercent / 100);
$lineItem->setPriceDefinition(
new QuantityPriceDefinition(
$newPrice,
$context->buildTaxRules($lineItem->getPrice()->getTaxRules()),
$quantity
)
);
}
}
}
private function findApplicableTier(EntityCollection $tiers, int $quantity): ?TierEntity
{
$applicable = null;
foreach ($tiers as $tier) {
if ($quantity >= $tier->getMinQuantity()) {
if (!$applicable || $tier->getMinQuantity() > $applicable->getMinQuantity()) {
$applicable = $tier;
}
}
}
return $applicable;
}
}
Register both with priorities:
<!- Collector runs first to gather data ->
<service id="YourPlugin\Cart\B2BPricingCollector">
<tag name="shopware.cart.collector" priority="5000"/>
</service>
<!- Processor runs after to apply pricing ->
<service id="YourPlugin\Cart\B2BPricingProcessor">
<tag name="shopware.cart.processor" priority="5000"/>
</service>
Example 4: Bundle Discount (Buy X + Y, Get Z% Off)
class BundleDiscountProcessor implements CartProcessorInterface
{
// Define bundles: if both products are in cart, apply discount
private array $bundles = [
[
'products' => ['PROD-001', 'PROD-002'],
'discount_percent' => 15,
'label' => 'Bundle: Starter Kit -15%',
],
];
public function process(
CartDataCollection $data,
Cart $original,
Cart $toCalculate,
SalesChannelContext $context,
CartBehavior $behavior
): void {
$lineItems = $toCalculate->getLineItems();
foreach ($this->bundles as $bundle) {
$bundleKey = 'bundle-' . md5(implode('-', $bundle['products']));
// Check if all bundle products are in cart
$allPresent = true;
$bundleTotal = 0;
foreach ($bundle['products'] as $productNumber) {
$found = $lineItems->filter(function (LineItem $item) use ($productNumber) {
return $item->getPayloadValue('productNumber') === $productNumber;
});
if ($found->count() === 0) {
$allPresent = false;
break;
}
$bundleTotal += $found->getPrices()->sum()->getTotalPrice();
}
if (!$allPresent) {
$lineItems->remove($bundleKey);
continue;
}
$discountAmount = -1 * ($bundleTotal * $bundle['discount_percent'] / 100);
$discount = new LineItem($bundleKey, LineItem::DISCOUNT_LINE_ITEM, null, 1);
$discount->setLabel($bundle['label']);
$discount->setGood(false);
$discount->setRemovable(false);
$discount->setPriceDefinition(new AbsolutePriceDefinition($discountAmount));
$toCalculate->add($discount);
}
}
}
Priority Guide
Priority determines execution order (higher = earlier):
Priority 10000: Data collectors (fetch external data)
Priority 5000: Price modifications (B2B pricing, special prices)
Priority 4000: Discounts (coupons, bundles, auto-discounts)
Priority 3000: Validation (minimum order, stock checks)
Common Gotchas
- Always use
$toCalculate, not$original-$originalis the unmodified cart,$toCalculateis what you should modify - Remove discounts when conditions aren't met - otherwise stale line items persist
- Price recalculation - after modifying prices, Shopware recalculates totals automatically
- Don't block checkout in collectors - only processors should add errors
- Test with tax - B2B (net) and B2C (gross) calculations differ. Test both.
Need custom cart logic for your Shopware shop? We've built it all.