Cart module

Module id: cart. Dependencies: catalog, customer.

What it adds:

  • A persistent cart per visitor (anonymous via session cookie, then upgraded to a customer-bound cart on login).
  • Per-item snapshots (price, name, tax class) so a later product price change doesn't retroactively rewrite cart totals.
  • Theme helpers for cart page, mini-cart widget, count / total badges.
  • /cart routes for add / update / remove / clear / view.

The cart on its own is read-only-checkout. To accept orders, also enable Checkout.

What's per-website

Nothing. No cart_settings table, no admin panel. Cart behaviour is fixed by design:

  • One cart per anonymous session (session cookie). Cookie lifetime follows Symfony's framework.session defaults — 1 hour idle.
  • On login, the anonymous cart's items are merged into the customer's persistent cart (if any). Subsequent logout doesn't drop the customer cart — it's loaded again on next login.
  • Stock is snapshotted at add-time for display, but re-checked at order-place time by Checkout. So is whether each line may still be bought (published, and visible to this visitor).

If you need to change any of this (different session lifetime, "guest checkout only" mode, etc.) it's a code change in cms-core, not a per-tenant knob.

Theme helpers (when cart is enabled)

Full list in ../../twig-helpers.md#cart-module-cart.

{# Header — show count + a quick total. #}
<a href="/cart" class="header-cart">
    Cart
    {% if cms_cart_count() > 0 %}
        <span class="badge">{{ cms_cart_count() }}</span>
        <span class="total">{{ cms_cart_total() }}</span>
    {% endif %}
</a>

{# A drop-down mini-cart preview. #}
{{ cms_mini_cart() }}

{# The full cart page (themes can override the partial — see below). #}
{% if cms_cart() %}
    {{ cms_cart_view() }}
{% else %}
    <p>Your cart is empty.</p>
    <a href="/products" class="btn">Browse products</a>
{% endif %}

{# After-action notice flashed by /cart/add etc. #}
{{ cms_cart_notice() }}

cms_cart() returns the current Cart entity, or null when there are no items. Walking cart.items gives you CartItem entities with the snapshotted name + unit price + line totals.

Public routes

Route Method Path Purpose
cms_cart_show GET /cart Render the cart page.
cms_cart_add POST /cart/add Add product (or variant) + qty.
cms_cart_update POST /cart/update Change line quantities.
cms_cart_remove POST /cart/remove Remove a single line.
cms_cart_clear POST /cart/clear Empty the cart.

/cart/add only accepts a product the visitor may see. A product whose access is set to logged-in customers, or to customer groups the visitor is not in, is refused with the same "This product is not available." notice and the same redirect as an unpublished product or an id that does not exist. There is no login redirect from here (the product page itself does send a guest to the login form): a different answer would tell anyone posting ids which ones are restricted products. Themes need no change for this.

A line already in a cart can stop being for sale: the product or its variant is unpublished, the product is restricted after it went in, the customer is taken out of the group, or a guest filled the cart before /cart/add checked access. Such a line stays in the cart and can still be lowered or removed, but /cart/update refuses to raise it, and the checkout sends the shopper back to the cart, naming the line ("Dealer price list" is no longer available. Remove it from your cart to place your order.), until it is gone (see checkout.md).

The forms posting to these routes are conventional <form> tags in the theme (no form_* helpers needed — they don't carry user-typed content the way contact forms do). The page-edit cog isn't relevant here; this is template-defined behaviour.

A typical Add-to-cart form:

<form method="post" action="/cart/add">
    <input type="hidden" name="product_id" value="{{ p.id }}">
    {% if p.hasVariants %}
        <select name="variant_id" required>
            {% for v in p.variants %}
                {% if v.isAvailable %}
                    <option value="{{ v.id }}">{{ v.name }}</option>
                {% endif %}
            {% endfor %}
        </select>
    {% endif %}
    <input type="number" name="quantity" value="1" min="1" max="99">
    <button type="submit">Add to cart</button>
</form>

Public-side templates (themes can override)

Default Override at
@GsidCmsCore/pages/cart.html.twig @website/pages/cart.html.twig
@GsidCmsCore/cart/_view.html.twig @website/cart/_view.html.twig
@GsidCmsCore/cart/_mini_cart.html.twig @website/cart/_mini_cart.html.twig
@GsidCmsCore/cart/_notice.html.twig @website/cart/_notice.html.twig

cms_cart_view() calls _view.html.twig, cms_mini_cart() calls _mini_cart.html.twig, cms_cart_notice() calls _notice.html.twig. Override the partials, not the helpers.

Things this module does NOT do

  • No discount codes of its own. Promo codes live in the optional Discount module, which depends on Cart. Enable it and the cart gains an apply/remove endpoint plus discount / discountType / freeShipping keys in the cms_cart_view() summary.
  • No gift wrapping, gift messages, ship-by date. Add-to-cart takes product/variant + qty only.
  • No "save for later" / wishlist. The cart is the only persistence surface.
  • No multi-currency carts. The cart's currency is the website's; every product must be priced in the same currency. (Multi-currency is on the longer-term roadmap.)
  • No tax recalculation based on shipping address. Tax is computed per-product from the assigned tax class, full stop. EU-style reverse-charge / cross-border VAT isn't modelled.