webx-ui / module-cart
A guest cart for WebX UI sites: lines of anything a module sells through the Purchasable contract, priced live by their owner, totals as a chain other modules plug into, and cart() for Blade.
Requires
- php: ^8.4
- illuminate/console: ^13.0
- illuminate/contracts: ^13.0
- illuminate/cookie: ^13.0
- illuminate/database: ^13.0
- illuminate/http: ^13.0
- illuminate/routing: ^13.0
- illuminate/support: ^13.0
- illuminate/validation: ^13.0
- illuminate/view: ^13.0
- webx-ui/localization: ^0.91.0
- webx-ui/widgets: ^0.91.0
Requires (Dev)
- orchestra/testbench: ^11.0
- phpunit/phpunit: ^12.0 || ^13.0
- webx-ui/mcp: ^0.91.0
- webx-ui/module-admin: ^0.91.0
- webx-ui/module-auth: ^0.91.0
- webx-ui/routing: ^0.91.0
Suggests
- webx-ui/mcp: Agents read the carts: cart_list, cart_get, cart_summary, cart_quote and cart://catalog, behind the cart.view permission.
- webx-ui/module-admin: webx:doctor names the setting when the site's currency is not configured.
- webx-ui/module-catalog: Its products go into the cart, and its currency is the cart's when the cart has none of its own.
Provides
None
Conflicts
None
Replaces
None
README
A guest cart for WebX UI sites. Lines of anything a module sells go in through the Purchasable
contract and are priced live by their owner; totals are a chain other modules plug into; cart()
gives a template the counter and the full state. With a visitors module, a signed-in visitor's
cart belongs to the account, and the guest's cart joins it at sign-in.
composer require webx-ui/module-cart php artisan migrate
Set the currency, unless the catalogue already has one:
WEBX_CART_CURRENCY=EUR
What lies in a line
The cart stores a type, an id, the options and the quantity. Everything else — the name, the price, the picture, whether it can be bought — the owner of the record answers when asked, a batch of a type at a time:
use WebxUi\Cart\Contracts\Purchasable; use WebxUi\Cart\PurchasableLine; final class ProductPurchasable implements Purchasable { public function resolve(array $refs, string $locale): array { return Product::query()->whereIn('id', $refs)->get()->mapWithKeys(fn (Product $product) => [ $product->id => new PurchasableLine( name: $product->name, sku: $product->sku, price: $product->price_minor, // integer, minor units; 0 — price on request currency: 'EUR', url: $product->url, ), ])->all(); } }
The owner registers it from its own provider, only when the cart is installed:
if (class_exists(\WebxUi\Cart\Purchasables::class)) { $this->callAfterResolving(\WebxUi\Cart\Purchasables::class, static fn ($cart) => $cart->register('shop.product', ProductPurchasable::class)); }
A record that is gone (null) stays in the cart as «no longer available» with the name it had when
it was added; it counts for nothing and can be removed.
Bundles
A batch is written whole or not at all. A line points at its parent by the parent's key in the
same batch; a child's quantity is per unit of the parent. Removing the parent keeps the children,
their quantity multiplied.
app(\WebxUi\Cart\CartService::class)->add($cart, [ ['type' => 'shop.product', 'ref' => 42, 'key' => 'set'], ['type' => 'shop.product', 'ref' => 57, 'qty' => 2, 'parent' => 'set'], ], 'en');
Totals
Calculator counts lines → generated lines → subtotal → adjustments → total → tenders → payable,
with benefits and hints beside them. The five registries (Totals\LineSources,
AdjustmentSources, TenderSources, BenefitSources, HintSources) are empty here: discount
rules, coupons and wallets add their sources. Amounts are integers; every one has a
*_formatted string from Money::format(), written by the currency's rules, never the
language's: "2 500,00 €" for every visitor.
In a template
<span data-webx-cart-count>{{ cart()->count() }}</span>
count(), isEmpty() and has($type, $ref) cost no query without the cookie and one with it.
cart()->state() is the full count.
A buy button — a form with @csrf, so it works without JavaScript:
<x-webx-cart::add type="shop.product" :ref="$product->id">Buy</x-webx-cart::add>
The widget — the icon with the counter, the main view where lines change (a drawer or a dialog) and the mini cart under the icon, all drawn by the server:
<x-webx-cart /> {{-- drawer, or WEBX_CART_WIDGET=dialog --}} <x-webx-cart preview="none" /> {{-- no mini cart, or WEBX_CART_PREVIEW=none --}}
A buy button and webx.cart.open() always open the main view; «Edit cart» in the mini cart does
too. An item already at its maximum is not an error under the button: the cart opens with a
notice on that line.
Its script (dist/cart.js, committed) comes with the first widget or button on the page and
gives the page window.webx.cart — add(items) takes a bundle as one batch — and the
webx:cart:change event.
The API
GET /api/cart, POST /api/cart/items (a batch), PATCH and DELETE /api/cart/items/{id},
DELETE /api/cart; every answer is the whole state. The web group, CSRF and a throttle on
writes; a line of another cart is 404. The format and the guide: apps/docs/guide/cart.md in the
monorepo.
Housekeeping
php artisan webx:cart:prune deletes carts untouched for webx-cart.prune_after_days (60); it is
on the schedule daily. webx:doctor says when the site has no currency.
Specification: docs/architecture/WEBX_UI_MODULE_CART.md in the monorepo.