Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Shoppingcart Laravel Package

hardevine/shoppingcart

hardevine/shoppingcart is a Laravel shopping cart package (a maintained fork of LaravelShoppingcart) offering simple cart management: add, update and remove items, track quantities and prices, apply conditions, and persist carts via sessions for newer Laravel versions.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up

composer require hardevine/shoppingcart
php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="config"

Pin the constraint deliberately: ^3.4 is the first release line that declares Laravel 12 support, so on an L12 app an older ^3.x (or an unpinned *) may resolve to a version that won't install against illuminate/* ^12. Check composer show hardevine/shoppingcart after install and confirm you landed on 3.4+ before writing code.

Laravel 5.5+ auto-discovers the provider and registers the Cart facade alias. On older apps, add manually in config/app.php:

Gloudemans\Shoppingcart\ShoppingcartServiceProvider::class,
'Cart' => Gloudemans\Shoppingcart\Facades\Cart::class,

Note the namespace is still Gloudemans\Shoppingcart — the package was forked from the abandoned crinsane/LaravelShoppingcart and kept the namespace for drop-in compatibility. First place to look is the published config/shoppingcart.php: it controls the default tax rate, number format, and whether carts live in the session or the database.

First use case — a session-backed cart

use Gloudemans\Shoppingcart\Facades\Cart;

// add (array form is the safe, forward-compatible one)
Cart::add([
    'id'    => $product->id,
    'name'  => $product->name,
    'qty'   => 1,
    'price' => $product->price,
    'weight' => 0,
    'options' => ['size' => 'L', 'color' => 'black'],
]);

Cart::content();   // Collection of rowId => CartItem
Cart::count();     // total qty
Cart::subtotal();  // float
Cart::total();     // float, includes tax
Cart::update($rowId, 2);
Cart::remove($rowId);
Cart::destroy();   // nuke the whole cart

The minimum viable integration is three routes: add, update, remove, plus a view that loops Cart::content() and renders $item->name, $item->qty, $item->price, $item->options->size. Everything else (tax, discounts, persistence) is layered on top of that.

Implementation Patterns

Prefer the array signature. The legacy positional form (Cart::add($id, $name, $qty, $price, $options)) still works but is easy to break when you add weight or tax. Array form is self-documenting and matches what most tutorials and StackOverflow answers use.

Buyable models for real apps. Instead of hand-mapping attributes every time, implement Gloudemans\Shoppingcart\Contracts\Buyable on your Product (getBuyableIdentifier, getBuyableDescription, getBuyablePrice, getBuyableWeight) and pass the model directly:

Cart::add($product, 2, ['size' => 'M']);
// or associate an already-added row so $item->model is hydrated later
Cart::associate($rowId, Product::class);

Association is the cleanest way to render a cart page without N+1 queries — $item->model returns the related model on demand. For a full page, loop content, collect $item->id, and Product::findMany() yourself, or eager-load via $item->model.

Options are your variant key. Row identity is (id + options + price). Same product, same options, same price → quantities merge. Anything different → a new rowId. Use this deliberately: ['size' => 'L'] vs ['size' => 'M'] are separate rows, which is almost always what you want, but it also means a price change creates a duplicate row instead of updating the existing one.

Tax: set it globally, once, before reading totals.

Cart::setGlobalTax(21);            // applies to all existing items
Cart::add([... 'tax' => 21]);      // or per item
Cart::subtotal(); Cart::tax(); Cart::total();

Global tax recalculates existing rows, so call it at the start of the request (middleware or a base controller) rather than mid-flow.

Persisting carts across login. With 'storage' => 'database' in config, publish and run the migration, then:

php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="migrations"
php artisan migrate
// on logout / session expiry
Cart::store($user->id);
// on login
Cart::restore($user->id);
// on checkout completion or account deletion
Cart::erase($user->id);

The identifier is yours to choose — user ID is the common pick. Store on Login/Logout events, not in a controller, or you will miss the mobile-app and remember-me paths.

Checkout snapshot pattern. Never write order lines by reading Cart::content() after payment confirmation — the cart may have been mutated in another tab. At order creation:

$items = Cart::content()->map(fn ($i) => [
    'product_id' => $i->id,
    'name'       => $i->name,
    'qty'        => $i->qty,
    'price'      => $i->price,
    'options'    => $i->options->toArray(),
    'tax'        => $i->tax,
])->values()->all();

DB::transaction(function () use ($items, $total) {
    $order = Order::create([...]);
    $order->items()->createMany($items);
    Cart::destroy();
});

Wrap it in your own service. The facade is convenient and easy to sprinkle through controllers. Put CartService (or a Cart repository) in front of it with methods named after your domain (addVariant, applyCoupon, snapshotForOrder). This is the single highest-value refactor: the underlying package is a 2015-era design, and if you ever migrate to a first-party solution or a DB-backed cart of your own, the call sites are already isolated.

Mini-cart in layouts. Use a view composer so the header badge doesn't force every controller to pass cart data:

View::composer('layouts.app', fn ($view) =>
    $view->with('cartCount', Cart::count())
       ->with('cartTotal', Cart::total())
);

Note Cart::count() is total quantity; Cart::content()->count() is distinct rows. They differ when someone buys three of the same thing.

Validate before mutating. update() and add() do no stock checking. Wrap in a FormRequest that checks qty <= $product->stock and re-reads the price server-side — never trust a price posted from the client, and never let the client pass the price at all if you can avoid it.

Gotchas and Tips

rowId is not id. update() and remove() take the hashed rowId, not your product ID. This is the #1 source of "why didn't it update" bugs, especially when a product appears twice with different options. Return rowId to your frontend and post that back.

Reserved option keys. You cannot use id, name, qty, price, weight, or options as keys inside the options array — the package throws. If you need a field called name in options, namespace it (variant_name).

Money is stored as floats. Totals are PHP floats, so Cart::total() can produce 19.989999999. Do not do equality checks or accumulate across rows with + and expect cents-accurate results. Format for display and round once at the boundary (number_format($total, 2)), or convert to integer cents in your own checkout code. The formatter signature is Cart::subtotal($decimals, $decimalPoint, $thousandSeparator) — note the misspelled parameter name thousandSeperator in the signature; use positional arguments, not named ones.

Session middleware is mandatory for session storage. If you expose cart routes under api.php (which is stateless by default) or any route without StartSession, the cart silently resets on every request. Either move cart routes into web.php or add the session middleware explicitly. For token-authenticated SPAs, switch to 'storage' => 'database' and key carts by user rather than session.

store()/restore() only work with database storage. Calling them with session storage will not do what you expect. Also, restore() merges into the current cart in some versions — if you're recovering after a guest added items and then logged in, verify the merge behaviour with a test rather than assuming overwrite.

Cart::content() returns a Collection keyed by rowId, not a zero-indexed list. $cart[0] will fail. Use foreach, ->values(), ->first(), or ->firstWhere('id', $id) (which will grab the first matching row, not all of them — use ->where('id', $id) for variants).

Global tax mutates existing rows. Setting tax after items are added recalculates them; setting it before means new items inherit it. Pick one convention and enforce it, or you'll get carts where two of the same product have different tax because they were added in different requests.

Database table name and connection. The migration creates a shoppingcart table with a polymorphic-ish identifier + instance pair. If you run multi-tenant or multi-currency, the default single-table setup needs the instance column used deliberately — separate instances per tenant, or you risk cross-tenant cart leakage.

Testing. Add Cart::destroy() to setUp() (or a RefreshDatabase-backed trait) or state leaks between tests via the session. Cart::content() in a test after a request needs $this->session([...]) or withSession() to be meaningful.

Facade alias collisions. Cart is a very common alias. If you also use a payment SDK or another commerce package that registers Cart, the last one registered wins. Rename yours in config/app.php if you're combining packages.

Version compatibility. 3.4 is the L12-capable release; if you're upgrading an app to Laravel 12, bump the package first and re-run the suite before touching your cart code. This is still a maintained fork, not an official Laravel package, and its illuminate/* constraints are always going to lag the first-party packages — so treat each Laravel major as a required package bump, pin an explicit range in composer.json (don't let * float), and read the diff of the 3.x tags when the constraint moves. The API itself has been frozen for years: breaking changes come from Laravel, not from the package, and an L12 bump is unlikely to need code changes in your integration — but it will need a lockfile update and a green test run.

Extension points are thin. There's no policy layer, no event bus worth relying on, and no pluggable price resolver. If you need promotions, tiered pricing, or per-line discounts beyond the built-in globals, compute them yourself and store the result via Cart::update($rowId, ['price' => $effectivePrice]), or keep a parallel promotions service that transforms the cart on read. Do not monkey-patch the Cart class — wrap the facade instead.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo