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.
/cartroutes 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.sessiondefaults — 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/freeShippingkeys in thecms_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.