Shopware 6.7 introduces significant enhancements to the checkout process, particularly through the evolution of cart processors. These components provide developers with powerful tools to implement custom business logic during the shopping cart and checkout workflow. In this technical deep dive, we'll explore how to leverage cart processors to create sophisticated checkout logic that can handle complex scenarios like dynamic pricing, conditional shipping rules, and custom validation.
Understanding Cart Processors in Shopware 6.7
Cart processors in Shopware 6.7 are services that modify the shopping cart state during various checkout phases. They're executed at specific points in the cart lifecycle and allow developers to inject custom logic without modifying core functionality. The key advantage is that these processors can be registered as services and configured through the dependency injection container, making them highly reusable and maintainable.
The cart processor system operates through a well-defined interface: Shopware\Core\Checkout\Cart\CartProcessorInterface. This interface defines two primary methods:
process()- for modifying cart datapreProcess()- for preparing cart data before processing
Setting Up Custom Cart Processor Service
To implement custom checkout logic, we first need to create a service that implements the cart processor interface. Here's a practical example of a custom processor that handles loyalty-based discounts:
<?php declare(strict_types=1);
namespace MyCompany\CustomCheckout\Cart;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartProcessorInterface;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
use Shopware\Core\Checkout\Cart\Price\Struct\PriceCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
class LoyaltyDiscountProcessor implements CartProcessorInterface
{
public function process(Cart $cart, Cart $originalCart, SalesChannelContext $context): void
{
// Get customer loyalty points from context or database
$loyaltyPoints = $this->getCustomerLoyaltyPoints($context);
if ($loyaltyPoints >= 100) {
$this->applyDiscount($cart, $loyaltyPoints);
}
}
private function getCustomerLoyaltyPoints(SalesChannelContext $context): int
{
// Implementation to fetch loyalty points from customer data
return $context->getCustomer()?->getLoyaltyPoints() ?? 0;
}
private function applyDiscount(Cart $cart, int $points): void
{
$discountAmount = min($points * 0.1, 50); // Max 50€ discount
foreach ($cart->getLineItems() as $lineItem) {
if ($lineItem->getType() === LineItem::PRODUCT_LINE_ITEM_TYPE) {
$currentPrice = $lineItem->getPrice();
$newPrice = new CalculatedPrice(
$currentPrice->getUnitPrice() - ($discountAmount / $lineItem->getQuantity()),
$currentPrice->getTotalPrice() - $discountAmount,
$currentPrice->getCalculatedTaxes(),
$currentPrice->getTaxRules()
);
$lineItem->setPrice($newPrice);
}
}
}
}
Registering the Cart Processor
The custom processor needs to be registered as a service in the dependency injection container. This is done through the services.xml file:
<service id="MyCompany\CustomCheckout\Cart\LoyaltyDiscountProcessor">
<tag name="shopware.cart.processor"/>
<argument type="service" id="Shopware\Core\System\SalesChannel\SalesChannelContext"/>
</service>
Advanced Cart Processor Implementation
For more complex scenarios, you might need to create a processor that handles multiple business rules. Here's an example of a processor that manages conditional shipping based on product categories and customer location:
<?php declare(strict_types=1);
namespace MyCompany\CustomCheckout\Cart;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartProcessorInterface;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;
use Shopware\Core\Checkout\Shipping\Cart\ShippingCosts;
use Shopware\Core\Checkout\Shipping\Cart\ShippingCostsCalculatorInterface;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
class ConditionalShippingProcessor implements CartProcessorInterface
{
private ShippingCostsCalculatorInterface $shippingCostsCalculator;
public function __construct(ShippingCostsCalculatorInterface $shippingCostsCalculator)
{
$this->shippingCostsCalculator = $shippingCostsCalculator;
}
public function process(Cart $cart, Cart $originalCart, SalesChannelContext $context): void
{
if ($this->shouldApplyFreeShipping($cart, $context)) {
$this->applyFreeShipping($cart);
} else {
$this->calculateCustomShipping($cart, $context);
}
}
private function shouldApplyFreeShipping(Cart $cart, SalesChannelContext $context): bool
{
// Check if customer has premium status
$isPremiumCustomer = $context->getCustomer()?->getGroup()?->getName() === 'Premium';
// Check cart total amount
$cartTotal = $cart->getPrice()->getTotalPrice();
// Check for specific product categories
$hasSpecialProducts = false;
foreach ($cart->getLineItems() as $lineItem) {
if ($lineItem->getType() === LineItem::PRODUCT_LINE_ITEM_TYPE) {
$product = $lineItem->getProduct();
if ($product && in_array($product->getCategory()->getName(), ['Electronics', 'Luxury'])) {
$hasSpecialProducts = true;
break;
}
}
}
return ($isPremiumCustomer && $cartTotal > 100) ||
($hasSpecialProducts && $cartTotal > 500);
}
private function applyFreeShipping(Cart $cart): void
{
// Remove existing shipping costs
foreach ($cart->getDeliveries() as $delivery) {
$delivery->setShippingCosts(new ShippingCosts(0, 0));
}
// Add free shipping line item
$cart->addNewLineItem(
LineItem::SHIPPING_COSTS_LINE_ITEM_TYPE,
'free-shipping',
'Free Shipping',
1,
new CalculatedPrice(0, 0, [], [])
);
}
private function calculateCustomShipping(Cart $cart, SalesChannelContext $context): void
{
// Custom shipping calculation logic
$shippingCosts = $this->shippingCostsCalculator->calculate(
$cart,
$context,
$this->getCustomShippingRules($context)
);
// Apply calculated shipping costs to cart
foreach ($cart->getDeliveries() as $delivery) {
$delivery->setShippingCosts($shippingCosts);
}
}
private function getCustomShippingRules(SalesChannelContext $context): array
{
// Implementation for custom shipping rules based on customer location, cart contents, etc.
return [
'weight' => 5,
'destination' => $context->getCustomer()?->getDefaultShippingAddress()?->getCountry()->getIso(),
'premium' => $context->getCustomer()?->getGroup()?->getName() === 'Premium'
];
}
}
Handling Asynchronous Processing
Shopware 6.7 also supports asynchronous cart processing through event listeners and background tasks. This is particularly useful for expensive operations like external API calls or complex calculations:
<?php declare(strict_types=1);
namespace MyCompany\CustomCheckout\Cart;
use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\CartProcessorInterface;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Shopware\Core\Checkout\Cart\Event\BeforeCartCalculatedEvent;
class AsyncCartProcessor implements CartProcessorInterface, EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
BeforeCartCalculatedEvent::class => 'onCartCalculate'
];
}
public function process(Cart $cart, Cart $originalCart, SalesChannelContext $context): void
{
// Queue async processing task
$this->queueAsyncProcessing($cart, $context);
}
public function onCartCalculate(BeforeCartCalculatedEvent $event): void
{
// Handle event-based cart calculations
$cart = $event->getCart();
$context = $event->getSalesChannelContext();
$this->performAsyncCalculations($cart, $context);
}
private function queueAsyncProcessing(Cart $cart, SalesChannelContext $context): void
{
// Implementation for queuing async tasks
// This could use Symfony Messenger or custom queue system
}
private function performAsyncCalculations(Cart $cart, SalesChannelContext $context): void
{
// Perform expensive calculations in background
// Update cart with results when complete
}
}
Performance Considerations
When implementing custom cart processors, performance is crucial. Here are key considerations:
- Minimize Database Queries: Cache frequently accessed data and use batch operations where possible.
- Lazy Loading: Only process items that require modification.
- Caching Strategies: Implement proper caching for expensive calculations.
- Memory Management: Be mindful of memory usage during cart processing.
private function optimizeProcessing(Cart $cart, SalesChannelContext $context): void
{
// Cache expensive operations
static $customerCache = [];
if (!isset($customerCache[$context->getCustomer()?->getId()])) {
$customerCache[$context->getCustomer()?->getId()] = $this->fetchCustomerData($context);
}
// Use cached data for processing
$customerData = $customerCache[$context->getCustomer()?->getId()];
// Process only necessary items
foreach ($cart->getLineItems() as $lineItem) {
if ($this->shouldProcessItem($lineItem, $customerData)) {
$this->processItem($lineItem, $customerData);
}
}
}
Testing Custom Cart Processors
Comprehensive testing is essential for cart processors. Shopware provides utilities for creating test carts and contexts:
public function testLoyaltyDiscountProcessor(): void
{
$cart = $this->createTestCart();
$context = $this->createTestContext();
$processor = new LoyaltyDiscountProcessor();
$processor->process($cart, $cart, $context);
$this->assertEquals(95.0, $cart->getPrice()->getTotalPrice());
}
Conclusion
Shopware 6.7's cart processor system provides a robust framework for implementing custom checkout logic without compromising system stability. By leveraging these processors, developers can create sophisticated business rules that enhance the customer experience while maintaining clean, maintainable code.
The key to successful implementation lies in understanding when and how cart processors are executed, optimizing performance through caching and lazy loading, and thoroughly testing edge cases. As you implement more complex scenarios, consider combining multiple processors and using event-driven architecture for maximum flexibility.
Remember to always follow Shopware's best practices for service registration, error handling, and performance optimization to ensure your custom checkout logic integrates seamlessly with the platform's existing functionality.