The Quote

A Magento cart is a quote: one row with its items, addresses, stored totals, and a masked id for guest access over REST.

Syntax$quote = $cartRepository->getActive((int) $cartId);

A quote is Magento's cart. One row in the quote table is a shopper's in-progress order: the store, the customer or guest, whether it is still active, and the money totals as last calculated. Each cart line is a row in quote_item. Addresses go to quote_address, the chosen payment method to quote_payment. The API calls it a cart and the database calls it a quote. They are the same object.

Loading one

Magento\Quote\Api\CartRepositoryInterface is the supported way in. get($cartId) returns a quote whatever its state. getActive($cartId) throws NoSuchEntityException once the quote has become an order, because placing an order sets is_active to 0: the quote is kept for reporting, not reused. In the storefront only, Magento\Checkout\Model\Session::getQuote() returns the visitor's current quote. A console command runs in the global area and has no checkout session, so there you pass an id.

The masked id

A guest cart has no customer to authorise against, so Magento never exposes its numeric id over REST. It stores a random string per quote in quote_id_mask, and the /V1/guest-carts/... endpoints take that masked id. Convert between the two with Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface and QuoteIdToMaskedQuoteIdInterface.

Totals are stored, not computed

Quote totals are columns. Change items in code and the figures stay stale until collection runs, so call $quote->collectTotals() before saving. A new class under app/code needs bin/magento setup:di:compile in production mode, and a new console command only appears after bin/magento cache:flush.

Example

Example · php
<?php
// app/code/Vendor/CartTools/Console/Command/ShowCart.php
declare(strict_types=1);

namespace Vendor\CartTools\Console\Command;

use Magento\Framework\Console\Cli;
use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

class ShowCart extends Command
{
    public function __construct(
        private readonly CartRepositoryInterface $cartRepository,
        private readonly MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId,
        ?string $name = null
    ) {
        parent::__construct($name);
    }

    protected function configure(): void
    {
        $this->setName('vendor:cart:show');
        $this->setDescription('Print the items in a guest cart.');
        $this->addArgument('masked_id', InputArgument::REQUIRED, 'Masked quote id');
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $maskedId = (string) $input->getArgument('masked_id');
        $quoteId = $this->maskedQuoteIdToQuoteId->execute($maskedId);
        $quote = $this->cartRepository->get($quoteId);

        $output->writeln(sprintf('quote %d  active=%d', (int) $quote->getId(), (int) $quote->getIsActive()));

        foreach ($quote->getItems() ?? [] as $item) {
            $output->writeln(sprintf('  %-12s x%s', $item->getSku(), $item->getQty()));
        }

        return Cli::RETURN_SUCCESS;
    }
}

// $ bin/magento vendor:cart:show 8f2c1d9ab4e7
// quote 142  active=1
//   24-MB01      x2
//   24-WG084     x1

When to use it

  • Dump a reported cart from the command line so support can see what the shopper had.
  • Accept a cart id from a headless front end and look the quote up without trusting a numeric id.
  • Show a live item count in a custom header block.
  • Work out why a cart's grand total does not match the sum of its items.

More examples

Reading the current visitor's cart

The checkout session exists only in the frontend area, so this view model works in a storefront template and would fail in a console command or an admin controller. getQuote() creates a quote for a visitor who has none, which is why cart output belongs in a private, uncached block rather than in full-page-cached HTML. The two constructor forms shown are identical; promotion is the short way to write the assignment Magento's dependency injection relies on.

Example · php
<?php
// app/code/Vendor/CartTools/ViewModel/CartSummary.php
declare(strict_types=1);

namespace Vendor\CartTools\ViewModel;

use Magento\Checkout\Model\Session as CheckoutSession;
use Magento\Framework\View\Element\Block\ArgumentInterface;

class CartSummary implements ArgumentInterface
{
    // Shorthand: constructor property promotion (PHP 8.0 and later).
    public function __construct(
        private readonly CheckoutSession $checkoutSession
    ) {
    }

    // Longhand, exactly the same thing:
    //
    // private CheckoutSession $checkoutSession;
    //
    // public function __construct(CheckoutSession $checkoutSession)
    // {
    //     $this->checkoutSession = $checkoutSession;
    // }

    public function getItemCount(): int
    {
        return (int) $this->checkoutSession->getQuote()->getItemsCount();
    }

    public function isEmpty(): bool
    {
        return $this->getItemCount() === 0;
    }
}

Adding an item to a cart by SKU

CartItemInterface carries data, so it cannot be injected; you ask a generated factory for a new one. The repository validates the SKU, merges with an existing line for the same product, and recollects totals, which direct writes to quote_item would skip. Reach for this when importing a saved list or rebuilding an abandoned cart.

Example · php
<?php
// app/code/Vendor/CartTools/Model/AddBySku.php
declare(strict_types=1);

namespace Vendor\CartTools\Model;

use Magento\Quote\Api\CartItemRepositoryInterface;
use Magento\Quote\Api\Data\CartItemInterfaceFactory;

class AddBySku
{
    public function __construct(
        private readonly CartItemRepositoryInterface $cartItemRepository,
        private readonly CartItemInterfaceFactory $cartItemFactory
    ) {
    }

    public function execute(int $cartId, string $sku, float $qty): void
    {
        $item = $this->cartItemFactory->create();
        $item->setQuoteId($cartId);
        $item->setSku($sku);
        $item->setQty($qty);

        $this->cartItemRepository->save($item);
    }
}

Handing a cart to a headless front end

A quote_id_mask row is not created for every quote. Magento inserts one when a guest cart is created through Magento\Quote\Model\GuestCart\GuestCartManagement::createEmptyCart() or its GraphQL equivalent, so a customer's cart normally has no mask row at all. QuoteIdToMaskedQuoteId::execute() throws NoSuchEntityException only when the quote itself does not exist; when the quote exists but has no mask row it returns an empty string, so create the quote_id_mask row yourself before handing the id out. This is the conversion to use when a storefront session has to hand its cart to a JavaScript or mobile client that will continue over REST. getActiveForCustomer throws NoSuchEntityException when the customer has no open cart, so call it inside a try block if an empty cart is a normal state for your caller. This is the conversion to use when a storefront session has to hand its cart to a JavaScript or mobile client that will continue over REST. getActiveForCustomer throws NoSuchEntityException when the customer has no open cart, so call it inside a try block if an empty cart is a normal state for your caller.

Example · php
<?php
// app/code/Vendor/CartTools/Model/CartHandoff.php
declare(strict_types=1);

namespace Vendor\CartTools\Model;

use Magento\Quote\Api\CartRepositoryInterface;
use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface;

class CartHandoff
{
    public function __construct(
        private readonly CartRepositoryInterface $cartRepository,
        private readonly QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId
    ) {
    }

    /**
     * Returns the id a guest client may use for /V1/guest-carts/<id>/... calls.
     */
    public function maskedIdForCustomerCart(int $customerId): string
    {
        $quote = $this->cartRepository->getActiveForCustomer($customerId);

        return $this->quoteIdToMaskedQuoteId->execute((int) $quote->getId());
    }
}

Discussion

  • Be the first to comment on this lesson.