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

Laravelshoppingcart Laravel Package

mindscms/laravelshoppingcart

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register. The package is a drop-in continuation of the (now abandoned) gloudemans/shoppingcart / Crinsane package, so any tutorial you find for that one applies almost verbatim.

composer require mindscms/laravelshoppingcart

On Laravel 5.5+ the Minds\ShoppingCart\ShoppingCartServiceProvider and the Cart alias are auto-discovered. On older versions add them to config/app.php:

'providers' => [
    Minds\ShoppingCart\ShoppingCartServiceProvider::class,
],
'aliases' => [
    'Cart' => Minds\ShoppingCart\Facades\Cart::class,
],

Publish what you need. You only need the migration if you plan to persist carts to the database (see store()/restore() below). Publishing the config is worth doing on day one so the currency formatting and DB connection are explicit rather than implicit.

php artisan vendor:publish --provider="Minds\ShoppingCart\ShoppingCartServiceProvider"
php artisan migrate

Where to look first. Read src/Cart.php — it is a single, readable class and is effectively the whole API surface. Then src/CartItem.php (and its options bag), the published config/shoppingcart.php, and src/DatabaseStorage.php if you're using persistence. The README is thin compared to the source; don't fight it.

First use case — an add-to-cart endpoint. The array form of add() is the one to learn, because it accepts tax, weight, options and an Eloquent model:

use Minds\ShoppingCart\Facades\Cart;

public function store(Product $product)
{
    Cart::add([
        'id'         => $product->id,
        'name'       => $product->name,
        'qty'        => request('qty', 1),
        'price'      => $product->price,
        'tax'        => 21,                 // percentage for this line
        'options'    => ['size' => 'L'],
        'associatedModel' => $product,      // exposed later as $item->model
    ]);

    return redirect()->route('cart.index');
}

Then render it — content() is a Collection keyed by rowId:

@foreach (Cart::content() as $rowId => $item)
    <tr>
        <td>{{ $item->name }} ({{ $item->options->size }})</td>
        <td>{{ $item->qty }}</td>
        <td>{{ $item->price }}</td>
        <td>{{ $item->subtotal }}</td>
        <td>
            <form method="POST" action="{{ route('cart.update', $rowId) }}">
                @csrf @method('PATCH')
                <input name="qty" value="{{ $item->qty }}">
            </form>
        </td>
    </tr>
@endforeach
<p>Items: {{ Cart::count() }} — Total: {{ Cart::total() }}</p>

That's enough to ship a working cart in an afternoon.

Implementation Patterns

Keep controllers thin, wrap the cart in a service. Cart is a facade over a singleton, so it's easy to spray calls across views. Put the mutations in one CartService (add, update, remove, clear) and let controllers validate and delegate. Views should only ever read (content(), count(), total()).

Merge-on-add is automatic — rely on it, then verify it. Adding the same id with identical options increments the existing row; different options create a new row. This is why the options array matters: it's part of the row identity, not just metadata.

Cart::add(['id' => 1, 'name' => 'Tee', 'qty' => 1, 'price' => 25, 'options' => ['size' => 'L']]);
Cart::add(['id' => 1, 'name' => 'Tee', 'qty' => 1, 'price' => 25, 'options' => ['size' => 'L']]);
// one row, qty 2 — same options hash

Quantity updates. update() accepts either a scalar (quantity) or an array of attributes, and is the standard "change quantity" endpoint:

Cart::update($rowId, request('qty'));                  // qty only
Cart::update($rowId, ['qty' => 3, 'price' => 19.99]);  // qty + reprice
Cart::remove($rowId);
Cart::destroy();                                       // empty the whole cart

Because this is user input, validate it (integer|min:1|max:99) in the request class, not in the package.

Attach Eloquent models for the read path. associatedModel is the single most useful integration point: it lets you render images, slugs and stock without a second lookup per row.

$item->model;          // the Product you passed in
$item->model->image;   // still lazy-loaded per row — eager-load where it hurts

If you're rendering a cart page for 20 lines, hydrate instead of relying on N+1: Product::whereIn('id', Cart::content()->pluck('id'))->get()->keyBy('id').

Money: use the *Float() accessors for logic, the string accessors for display. Cart::total(), subtotal(), tax() return formatted strings according to your config. Use Cart::totalFloat(), Cart::subtotalFloat(), Cart::taxFloat() when you need a number (shipping thresholds, free-shipping checks, passing to a payment gateway).

$shipping = Cart::subtotalFloat() >= 50 ? 0 : 4.95;

Tax and discounts. Set a default rate globally once (Cart::setGlobalTax(21) in a service provider or middleware) and override per line with the tax key at add time. The package also ships a condition API (Cart::addCondition(...), Cart::removeCondition(...), Cart::getCondition(...)) that can target the whole cart or a row — it's genuinely useful for promotions, but the docs are sparse, so read src/Cart.php and unit-test your condition before trusting the numbers.

Persistence across sessions — store() / restore() is manual, not automatic. Session storage is the default; the DB layer is an explicit save/restore of a serialized cart. The standard pattern:

// on login or before logout
Cart::store(auth()->id());

// on next visit (middleware or login controller)
Cart::restore(auth()->id());

// on logout
Cart::destroy();

Wrap the login-side restore in a try/catch — restoring into a non-empty cart, or restoring with a stale rowId, is a common source of "duplicate line item" bugs. The cleanest version is: destroy() the merge target first, then restore().

Checkout: snapshot, don't trust. The cart is a session blob. At checkout, re-fetch every product by id, re-read the current price and stock, compare with $item->price, and only then write order_items. Then Cart::destroy() inside the same DB transaction that creates the order.

DB::transaction(function () {
    $order = Order::create([...]);
    foreach (Cart::content() as $item) {
        $product = Product::lockForUpdate()->findOrFail($item->id);
        abort_if($product->stock < $item->qty, 422, 'Out of stock');
        $order->items()->create([... 'unit_price' => $product->price ...]);
    }
    Cart::destroy();
});

Events for side effects. When event dispatching is enabled in the config, the cart fires add / update / remove / destroy (and store/restore) events carrying the CartItem. Use them for analytics, abandoned-cart timestamps, or "recently viewed" — never for anything the checkout depends on.

Multiple carts. Recent versions expose Cart::instance('wishlist') alongside the default instance, each with its own session key and DB rows. Confirm it exists in the Cart class of the version you've installed before designing around it; it's a convenient way to separate a wishlist from a live cart.

Gotchas and Tips

  • The session is the storage engine by default. With SESSION_DRIVER=array the cart vanishes between requests (and in some test setups). With the cookie driver you're capped around 4KB — a dozen line items with chunky options and the cart silently truncates. Use file/redis/database for anything real.
  • total() is a string, not a number. Cart::total() with the default config returns "1,234.56". Doing * 1.1 on it gives you nonsense. Reach for totalFloat() / subtotalFloat() / taxFloat().
  • Everything is PHP floats. Prices are stored and summed as floats with per-line rounding. That's fine for a storefront display, not fine as an accounting record. Persist integer cents (or recompute from decimal columns) when you write the order.
  • update($rowId, 0) deletes the row. It's documented behaviour, not a bug, but it surprises people writing "decrement" buttons. Guard against it, or embrace it for a "remove" affordance.
  • rowId is a hash of id + options, not a stable identifier. It changes if the option array changes shape or key order. Never store a rowId in the DB or in a URL you expect to survive a cart edit — resolve it from content() at request time and 404 on misses.
  • search() returns items; get() returns one. Cart::search(['id' => 5]) gives you a Collection of matching CartItems (handy for "is this product already in the cart?"). Cart::get($rowId) returns a single item or null.
  • No stock, price, or quantity validation lives in the package. It will happily let you add -3 of a discontinued product. All of that belongs in your FormRequests and your checkout transaction.
  • store()/restore() fail quietly-ish. Restoring an identifier that doesn't exist throws; restoring into a non-empty cart merges. Always destroy() or store() deliberately, and test the logout→login→cart round trip.
  • Database storage needs the migration and the right connection. The config's database section points at a connection, not a table name you can guess — publish the config, check it, then migrate. The stored payload is a serialized blob keyed by identifier, so don't expect to query it with SQL.
  • Events are opt-in. If your listener in EventServiceProvider never fires, check the events key in the published config before you start debugging your listener registration.
  • No session in CLI/queue context. Anything that touches the cart from an artisan command, a queued job or a Nova action will throw or silently operate on an empty cart. Pass an explicit identifier and use restore()/store() if you must, or better, keep cart logic in HTTP-land.
  • Testing. Call Cart::destroy() in setUp() — the singleton and session persist between test methods in the same class and produce order-dependent failures that look like package bugs.
  • Maintenance risk. ~72 stars, a fork of a package whose upstream is dormant, and a Laravel-5/6-era API. Pin the version in composer.json, be prepared to fork, and treat it as a session cart, not as an order engine. The abstraction is small enough (Cart.php is one file) that forking or vendor-patching is a realistic option if you hit a wall.
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.
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console