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

gloudemans/shoppingcart

Laravel Shoppingcart is a simple, flexible cart package for Laravel. It supports multiple cart instances, item attributes, conditions like discounts and VAT, and pluggable storage drivers including sessions and databases. Easily add, update, and remove items and calculate totals.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up (5 minutes)

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

Check which major line you actually installed before writing a line of code. composer show gloudemans/shoppingcart — 3.x is the current API described below; 2.6.x (which closed out with Laravel 5.7 support) uses the older add() argument order and different tax/format defaults. Mixing docs from the two is the #1 source of "my quantities are 9.99" bug reports. See Gotchas.

On Laravel 5.5+ the ShoppingcartServiceProvider and the Cart facade alias are auto-discovered. On older versions, add to config/app.php:

'providers' => [Gloudemans\Shoppingcart\ShoppingcartServiceProvider::class],
'aliases'   => ['Cart' => Gloudemans\Shoppingcart\Facades\Cart::class],

Look here first, in this order:

  1. config/shoppingcart.php (just published) — it controls tax rate, number formatting, and the DB persistence connection/table. Read it before writing any code; the defaults will silently affect your totals.
  2. Gloudemans\Shoppingcart\Cart — the facade resolves to this class. It is the whole API surface; skim every public method once.
  3. Gloudemans\Shoppingcart\CartItem — what Cart::content() returns. Note the magic $item->options, $item->subtotal, $item->tax, $item->model.
  4. Gloudemans\Shoppingcart\Contracts\Buyable — three methods (getBuyableIdentifier, getBuyableDescription, getBuyablePrice) that let you pass Eloquent models straight into the cart.
  5. The README on Crinsane/LaravelShoppingcart — the examples are the de-facto documentation.

First working use case: an add-to-cart route + a cart page.

// routes/web.php
Route::post('/cart/{product}', [CartController::class, 'add'])->name('cart.add');
Route::get('/cart', [CartController::class, 'index'])->name('cart.index');
class CartController extends Controller
{
    public function index()
    {
        return view('cart.index', ['items' => Cart::content()]);
    }

    public function add(Request $request, Product $product)
    {
        Cart::add(
            $product->id,
            $product->name,
            $product->price,                          // v3: price comes BEFORE quantity
            $request->integer('qty', 1),
            ['size' => $request->input('size')]        // options array (optional)
        )->associate(Product::class);                  // so $item->model works later

        return redirect()->route('cart.index');
    }
}
@foreach ($items as $item)
    <tr>
        <td>{{ $item->name }}</td>
        <td>{{ $item->qty }}</td>
        <td>{{ $item->price }}</td>
        <td>{{ $item->subtotal }}</td>
        <td>
            <form method="POST" action="{{ route('cart.remove', $item->rowId) }}">
                @csrf @method('DELETE')
                <button>Remove</button>
            </form>
        </td>
    </tr>
@endforeach
<p>Subtotal: {{ Cart::subtotal() }}</p>
<p>Tax: {{ Cart::tax() }}</p>
<p>Total: {{ Cart::total() }}</p>

That single flow — add() → content() → total()/destroy() — covers ~90% of what this package is used for. Everything else is the update/remove/persist cycle.

Implementation Patterns

Keep the controller thin; the cart is already a repository. A CartController with add, update, remove, destroy actions that each mutate and redirect back is the idiomatic shape. Avoid poking at session('cart') directly — always go through the facade so the internal CartItem objects stay hydrated.

Route model binding + a Buyable model is the cleanest integration. Implement the contract on Product:

class Product extends Model implements Buyable
{
    public function getBuyableIdentifier($options = null) { return $this->id; }
    public function getBuyableDescription($options = null) { return $this->name; }
    public function getBuyablePrice($options = null) { return $this->price; }
}

Then the controller becomes Cart::add($product, $request->integer('qty')) and you never hand-copy columns. The trade-off: the cart stores a snapshot at add-time, so if the price changes later the cart still holds the old price — which is usually what you want for UX, but see the checkout re-pricing note below.

Update quantities from a single form, not N forms. Loop the row IDs and call Cart::update($rowId, $qty) inside one POST; wrap the loop in a DB transaction only if you're also persisting to a table.

public function update(Request $request)
{
    foreach ($request->input('qty', []) as $rowId => $qty) {
        $qty = max(0, (int) $qty);
        $qty === 0 ? Cart::remove($rowId) : Cart::update($rowId, $qty);
    }
    return back();
}

Multiple carts via instances. Cart::instance('wishlist') gives you a completely separate cart stored under its own session key. Remember that instance() switches the active cart and the switch persists for the rest of the request — always switch back with Cart::instance('default') if you interleave operations, or scope the wishlist work into its own action.

Cart::instance('wishlist')->add($product->id, $product->name, $product->price, 1);
$wishlist = Cart::instance('wishlist')->content();
Cart::instance('default'); // switch back

Cross-device / cross-session persistence. The cart lives in the session, so it dies with the session or is invisible on another browser. For logged-in users, persist on login/logout:

// on login
Cart::restore($user->id);        // merges the DB copy into the session cart
// on logout (after the session cart is still populated)
Cart::store($user->id);

This requires the shoppingcart table (see the README's migration for the exact schema — identifier, instance, content columns) and the database block in config/shoppingcart.php to point at the right connection. If you use instances, store/restore operate on the current instance only.

Checkout: snapshot, re-price, then empty. Treat the cart as an untrusted draft. In the order action:

DB::transaction(function () use ($user) {
    $order = $user->orders()->create(['total' => 0]);
    $total = 0;

    foreach (Cart::content() as $item) {
        $product = Product::findOrFail($item->id);   // re-fetch, don't trust $item->price
        $total += $product->price * $item->qty;
        $order->lines()->create([
            'product_id' => $product->id,
            'qty'        => $item->qty,
            'unit_price' => $product->price,
            'options'    => $item->options->toArray(),
        ]);
    }

    $order->update(['total' => $total]);
    Cart::destroy();
});

Regional tax. config('shoppingcart.tax') is a single global percentage (default 21 — Dutch VAT). If you need per-region rates, either call Cart::setGlobalTax($rate) at the start of each request in a middleware, or set it to 0 and compute tax yourself from Cart::subtotal(). Don't mix both approaches or you'll double-tax.

Testing. Give every test a clean cart: call Cart::destroy() in setUp(), or use a distinct Cart::instance('test-'.uniqid()) per test. If your test suite uses the array session driver the cart behaves correctly within a request but is wiped between requests — assert via Cart::content(), not via a follow-up HTTP call unless you're persisting with store().

Gotchas and Tips

The v2 → v3 argument order flipped. v2 was add($id, $name, $qty, $price, $options); v3 is add($id, $name, $price, $qty, $options). Copy-pasted blog snippets from 2015 will compile fine and produce a cart with qty = 9.99 and price = 2. If quantities look insane, check this first. Pin ^3.0 in composer.json.

Know which line you're on: 2.6.x vs 3.x. The 2.x branch finished with 2.6.0, a compatibility-only release that added Laravel 5.7 support — no API changes. That matters practically in two ways: (a) if you're maintaining a Laravel 5.7-era app, gloudemans/shoppingcart:^2.6 is a legitimate pin and the old add() argument order applies, so the v3 snippets above will silently misbehave; (b) a version bump like this one is not an invitation to upgrade the API surface — treat 2.6.0 as "framework compatibility," not "features," and don't rewrite a working 2.x integration around it. New projects should still start on ^3.0.

Cart::total(), subtotal() and tax() return formatted strings, not floats. They use the format block in the config. Do (float) Cart::total() before any arithmetic, or pass explicit overrides: Cart::total(2, '.', ''). Related quirk: the config key is spelled thousand_seperator (sic) — a typo baked into the package. Use the misspelled key inside format, or pass the separator as the third argument.

rowId is a hash of the item id plus its options, and it changes. Adding the same product with the same options merges quantities into one row; adding it with different options creates a second row. Because the ID is derived, it is not stable across restore() or after update() changes options — never persist rowId in your own tables or URLs that outlive the session. Store id + options and look the row up with Cart::search():

$row = Cart::search(fn ($item) => $item->id === $productId && $item->options->get('size') === 'XL')->first();

update($rowId, 0) semantics differ across minor versions. Don't rely on it deleting the row; branch explicitly to Cart::remove($rowId) and you'll be version-proof. Same for negative or non-numeric quantities — clamp before you call.

Cart::count() is total quantity, Cart::content()->count() is number of lines. A badge showing "3 items" from Cart::count() is right; a "3 products" label built from it is wrong when quantities are >1.

Always validate against the DB before checkout. The cart is a session blob the user can influence through any route that calls add/update. Validate quantity against stock and re-read price from the Product row at order time. If you implement Buyable and pass $product->price at add-time, a price change between add and checkout won't be reflected — which is fine for UX, wrong for accounting. Decide deliberately.

Session dependency bites in APIs and multi-tab usage. The cart requires the session middleware; token-authenticated JSON endpoints won't have it unless you add StartSession. And two open tabs share one session cart, so "add" in tab A can clobber "update" in tab B — the last write wins. For anything more complex, persist each change with Cart::store($userId).

Default tax is 21%, not 0. If you compute tax in your order model, set 'tax' => 0 in the config or every Cart::total() will be ~21% too high and you will chase it for an hour.

Maintenance status. The 2.x line ended at 2.6.0 (Laravel 5.7 compatibility), and the v3 line has had no release since February 2019. There are no composer.json constraints for modern PHP/Laravel. On Laravel 9+ expect to either loosen constraints, install from dev-master, or use a maintained fork — hardevine/shoppingcart is the usual drop-in, and darryldecode/cart is a viable rewrite. Audit the dependency before committing to it on a new project; the API above is stable, but the framework integration is not.

Extension points are thin on purpose. Cart is a plain class (not Macroable), so you can't bolt on methods. The practical approach is a thin CartService wrapper injected into controllers that composes Cart:: calls, or rebind the cart singleton in a service provider if you truly need to subclass. There are also database.events in the config for hooking store/restore — useful for logging, not much else.

Debugging. dd(Cart::content()) gives you everything: each CartItem shows rowId, id, name, qty, price, options, subtotal, tax, total. If the cart looks empty when it shouldn't, check (a) which instance is active, (b) the session driver, (c) whether a middleware is regenerating the session ID, and (d) session('cart') directly — the raw array is keyed by instance name.

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.
skyraptor/chatter
edlugz/sasapay
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource