Cart

Version

6.0.0 or newer

Table of contents

Repository Link: https://github.com/shopware/platform/tree/master/src/Core/Checkout/Cart

Shopping cart management is a central feature of Shopware 6. The shopping cart resides in the checkout bundle and is a central part of the checkout process.

Design goals

The cart was designed with a few design goals in mind.

Adaptability
Although many services exist to make working with the cart simple and intuitive, the cart itself can be changed through many different processes and adapt to many different use cases.
Performance
With the no waste philosophy the cart is designed by identifying key processes and optimizing upon them. Therefore the amount of calculations, queries and iterations is kept to a minimum and a clear state management is implemented.
Independence
The cart is independent from many core entities of Shopware 6. It does not itself know that product, surcharges or discounts exist but communicates through interfaces with its line items.

Cart Struct

\Shopware\Core\Checkout\Cart\Cart

An instance of this class represents one single Cart. As you can see relations to central Entities of the System are omitted. This allows Shopware 6 to manage multiple carts per user and per SalesChannel, or one across all sales channels. The only identification is a token hash.

The diagram below illustrates the data that a cart holds.

cart struct

This is a highly mutable data structure that is acted upon from requests and calculated and validated through services. It contains:

Line Items
A line item represents an order position. It may be a shippable good, a download article or even a bundle of many products.
Line items contain properties that tell the cart how to handle changes in line items. (eg. stackable - quantity can be changed, removable - removable through the api, ...)
A line item is the main extension point for the cart process. Therefore a promotion, a discount or a surcharge is also a line item.
A line item can even contain other line items. So a single order position can be the composition of multiple single line items.
Transaction
A payment in the cart. Contains a payment handler and the amount.
Delivery
A shipment in the cart. Contains a date, a method, a target location and the line items that should be shipped together.
Error
Validation errors that prevent ordering that cart.
Tax
The calculated tax rate for the cart.
Price
The price of all line items including tax, delivery costs and vouchers discounts and surcharges.

Shopware 6 manages the carts state through different services. The different states a cart can inhabit are illustrated in the diagram below:

cart state

In the next chapters we will take a look at the calculation and data enrichment as well as the control of a cart in the system.

Calculation

Calculating a cart is one of the more costly operations a eCommerce System must support. Therefore the cut of the interfaces and the design of the process follows the no waste philosophy of Shopware 6 very closely. Calculation is a multi stage process that revolves around the mutation of data structure of the cart struct.

calculation steps

Cart enrichment

Enrichment secures the Independence and Adaptability of Shopware 6. Basically the Cart is able to create and contain line items that are initially empty and will only be loaded (=enriched) during calculation. The following code snippet illustrates this behaviour:

<?php 

use Shopware\Core\Checkout\Cart\Cart;
use Shopware\Core\Checkout\Cart\LineItem\LineItem;

$lineItem = new LineItem(/* ... */);
/** @var $cart Cart */
$cart->getLineItems()->add($lineItem);

$lineItem->getPrice(); // is now null
// enrich the cart
$lineItem->getPrice(); // now set up

This process is transparently controlled from the cart but executed through implementations of the \Shopware\Core\Checkout\Cart\CollectorInterface. This interface is cut in order to reduce the number database calls necessary to setup the cart's data structure for price calculation and inspection (meaning: rendering in a storefront, reading from the API).

The default collectors implemented in Shopware 6:

service idtask
Shopware\Core\Content\Product\Cart\ProductCollectorenrich all referenced products
Shopware\Core\Checkout\Promotion\Cart\CartPromotionsCollectorenrich add, remove and validate promotions
Shopware\Core\Checkout\Shipping\Cart\ShippingMethodPriceCollectorhandle shipping prices

And this is the call order:

enrichment with multiple collectors

Cart Processors - Price Calculation And Validation

The fully enriched cart is then processed. The price information for all individual LineItems is now set up, so the sums can be calculated. This happens in the \Shopware\Core\Checkout\Cart\Processor.

Steps:

  • The lineItem prices by applying the quantity and the tax rate
  • Sets up the deliveries and calculates costs
  • Sums the different Cart prices (incl, excl. vat, inc. excl. shipping)

Afterwards the calculation of prices is done and the cart can be inspected from the rule system.

Context Rules

The cart is then validated against the included rules which can lead to a change in the carts data, so a revalidation becomes necessary. Scenario:

Suppose you sell cars and have the following rules:

  • Everybody buying a car gets a pair of sunglasses for free
  • Every Cart containing two products gets a discount of 2%

Let's see what happens if a car is bought:

cart validation

As you can see the cart is modified during the enrichment process to at first contain the sunglasses and then again to contain the discount and result in the expected state.

Cart storage

Contrary to other entities in the System the Cart is not managed through the Data Abstraction Layer. The Cart can only be written and retrieved as a whole. This is done for one reason mainly: The cart does only make sense as a whole. As discussed in the sections the workload of Shopware 6 can only be performed on the whole object in memory.

Cart Control

The state changes and cart mutation is handled automatically by a facade the \Shopware\Core\Checkout\Cart\SalesChannel\CartService. It controlls, sets up and modifies the cart struct.

The Interface therefore handles alike:

Create a new cart


<?php declare(strict_types=1);
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Symfony\Component\Routing\Annotation\Route;
class NewCartController
{
    /** @var CartService */
    private $cartService;
    /**
     * @Route("/", name="cart.test")
     */
    public function createNewCart(): void
    {
        // Load the token through the request or from other sources
        $token = '596a70b408014230a140fd5d94d3402b';
        $cart = $this->cartService->createNew($token);
    }
}

Get current cart


<?php declare(strict_types=1);
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\Routing\Annotation\Route;
class GetCartController
{
    /** @var CartService */
    private $cartService;
    /**
     * @Route("/", name="cart.test")
     */
    public function getCart(SalesChannelContext $salesChannelContext): void
    {
        // if not cart exists, a new one will be created
        $cart = $this->cartService->getCart(
            '596a70b408014230a140fd5d94d3402b',
            $salesChannelContext
        );
    }
}

Add a line item to cart


<?php declare(strict_types=1);
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\Content\Product\Cart\ProductLineItemFactory;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\Routing\Annotation\Route;
class AddToCartController
{
    /** @var CartService */
    private $cartService;
    /**
     * @Route("/", name="cart.test")
     */
    public function addToCart(SalesChannelContext $salesChannelContext): void
    {
        // unique identifier to reference the line item, usually the source id, but can be random
        // and id of the referenced product
        $product = (new ProductLineItemFactory())->create('407f9c24dd414da485501085e3ead678', ['quantity' => 5]);
        $cart = $this->cartService->getCart('596a70b408014230a140fd5d94d3402b', $salesChannelContext);
        $this->cartService->add($cart, $product, $salesChannelContext);
    }
}

*Note: The add method of the cart service always triggers a recalculation of the cart.

Change line item quantity


<?php declare(strict_types=1);
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\Routing\Annotation\Route;
class ChangeQuantityController
{
    /** @var CartService */
    private $cartService;
    /**
     * @Route("/", name="cart.test")
     */
    public function changeLineItemQuantity(SalesChannelContext $salesChannelContext): void
    {
        $cart = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
        $this->cartService->changeQuantity($cart, '407f9c24dd414da485501085e3ead678', 9, $salesChannelContext);
    }
}

Remove line item


<?php declare(strict_types=1);
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\Routing\Annotation\Route;
class RemoveController
{
    /** @var CartService */
    private $cartService;
    /**
     * @Route("/", name="cart.test")
     */
    public function removeLineItem(SalesChannelContext $salesChannelContext): void
    {
        $cart = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
        $this->cartService->remove($cart, '407f9c24dd414da485501085e3ead678', $salesChannelContext);
    }
}

Get deliveries


<?php declare(strict_types=1);
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\Routing\Annotation\Route;
class GetDeliveriesController
{
    /** @var CartService */
    private $cartService;
    /**
     * @Route("/", name="cart.test")
     */
    public function getDeliveries(SalesChannelContext $salesChannelContext): void
    {
        $cart = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
        $deliveries = $cart->getDeliveries();
    }
}

Order


<?php declare(strict_types=1);
use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Symfony\Component\Routing\Annotation\Route;
class PlaceOrderController
{
    /** @var CartService */
    private $cartService;
    /**
     * @Route("/", name="cart.test")
     */
    public function order(SalesChannelContext $salesChannelContext): void
    {
        $cart = $this->cartService->getCart($salesChannelContext->getToken(), $salesChannelContext);
        $this->cartService->order($cart, $salesChannelContext);
    }
}