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

bumbummen99/shoppingcart

A Laravel shopping cart package for managing cart items, quantities, prices, taxes, shipping, and coupons. It supports session and database storage, multiple cart instances, item conditions, and events, with a simple facade API for adding, updating, and removing products.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require bumbummen99/shoppingcart:^4.2
php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="config"
php artisan vendor:publish --provider="Gloudemans\Shoppingcart\ShoppingcartServiceProvider" --tag="database"
php artisan migrate

Laravel 5.5+ auto-discovers the service provider. The package is a maintained fork of Crinsane/LaravelShoppingcart, so the namespace is still Gloudemans\Shoppingcart — the facade alias is Cart → Gloudemans\Shoppingcart\Facades\Cart. Only publish/run the database migration if you plan to use the store()/restore() persistence feature; the default cart lives entirely in the session.

Pin ^4.2 rather than *: 4.2.0 changes the on-disk format of persisted carts (see Gotchas), so an unpinned composer update on a live store can break restore() without a code change on your side.

Where to look first

  1. config/cart.php — set tax (defaults to 21, an EU VAT assumption), format, database.table, and destroy_on_logout before writing any cart code. Changing tax after items exist produces totals that don't reconcile.
  2. src/Cart.php — the public API surface. Skim the method list once; it is small (add, update, remove, destroy, content, search, store, restore, instance, setGlobalTax).
  3. src/CartItem.php — the object your Blade views and checkout logic will actually touch (rowId, id, name, qty, price, weight, options, model).

First use case: an add-to-cart endpoint plus a cart page

// app/Http/Controllers/CartController.php
public function add(Request $request, Product $product)
{
    $request->validate(['qty' => 'required|integer|min:1|max:20']);

    Cart::add([
        'id'       => $product->id,
        'name'     => $product->name,
        'qty'      => $request->integer('qty'),
        'price'    => $product->price,
        'weight'   => $product->weight ?? 0,
        'options'  => ['size' => $request->input('size')],
    ])->associate(Product::class);

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

public function index()
{
    return view('cart.index', ['cart' => Cart::content()]);
}
@foreach ($cart as $rowId => $item)
    <tr>
        <td>{{ $item->name }} {{ $item->options->size }}</td>
        <td>{{ $item->qty }}</td>
        <td>{{ $item->price }}</td>
        <td>{{ $item->subtotal }}</td>
    </tr>
@endforeach
<p>Subtotal: {{ Cart::subtotal() }} / Total: {{ Cart::total() }}</p>

The non-suffixed money methods (subtotal(), total(), tax()) return strings formatted via config('cart.format'); the *Float() variants return raw floats. Use the float variants for any arithmetic.

Implementation Patterns

Array-form add() with a model association. Prefer the array signature over the legacy positional one — it's order-independent and self-documenting. Chaining ->associate(Product::class) stores the class name on the item so $item->model lazily resolves the Eloquent record; this saves N+1 lookups when rendering a cart page but will re-query per item, so eager-load separately if you need relations.

Multi-instance carts. Cart::instance('wishlist') gives you a fully separate cart in the same session, which is the cleanest way to build a wishlist, a "save for later" list, or a comparison tray alongside the shopping cart:

Cart::instance('wishlist')->add([...]);
Cart::instance('wishlist')->content();

// Back to the default cart — the instance is sticky, so be explicit.
Cart::instance('cart')->content();

Update/remove via rowId, not product id. The cart row key is a hash of the product id and its options, so the same product in two sizes is two rows. Route-model-bind on the rowId or pass it through a form hidden field, then verify ownership/authorization yourself — the package has none.

Cart::update($rowId, ['qty' => $request->integer('qty')]);
Cart::remove($rowId);

Persisting carts across sessions/devices. Use the shoppingcart table (store/restore) rather than dumping Cart::content() into your own column:

// On login
Cart::restore(auth()->id());

// On logout
Cart::store(auth()->id());

Wire these into EventServiceProvider listeners for Illuminate\Auth\Events\Login and Logout, or into a small middleware. This is the pattern that makes guest → authenticated cart merging work, and it's the main reason to run the migration at all.

As of 4.2.0 the serialised cart is base64-encoded before it is written to the shoppingcart table, which fixes truncation of the payload on PostgreSQL. Treat the content column as an opaque blob owned by the package: don't read it, don't json_decode it, don't write to it from your own code.

Checkout as a re-validation step, not a cart read. Treat the cart as a UI cache, never as a source of truth. At checkout, loop Cart::content(), re-fetch each product from the database, re-check price and stock, then build the order inside a transaction and call Cart::destroy() last:

DB::transaction(function () use ($cart) {
    $order = Order::create([...]);

    foreach ($cart as $item) {
        $product = Product::lockForUpdate()->findOrFail($item->id);

        abort_if($product->stock < $item->qty, 422, 'Out of stock');

        $order->items()->create([
            'product_id' => $product->id,
            'qty'        => $item->qty,
            'unit_price' => $product->price, // recalculated, not $item->price
        ]);
    }
});

Cart::destroy();

Discounts and coupons live outside the package. There is no conditions/coupons engine here (that older Crinsane tutorial feature is gone). Keep a coupon service that reads Cart::subtotalFloat() and returns a discount amount, then apply it at order creation and in the totals partial. Do not mutate $item->price on cart items to fake a discount — it desynchronises the row from the product and breaks the rowId assumptions downstream.

Totals partial. Centralise money formatting so the cart page, mini-cart and checkout all agree:

// app/Support/Money.php
public static function format(float $value): string
{
    return number_format($value, 2, config('cart.format.decimal_point'), config('cart.format.thousand_seperator'));
}

Testing. The cart is session-backed, so every test starts dirty. Reset in setUp() with Cart::destroy(), or namespace tests to Cart::instance('test'). Use $this->withSession([])/assertSessionHas to inspect state rather than asserting on formatted strings. If you assert on store()/restore() round-trips, assert that restore() yields the same content() — never assert on the raw column value, which is now base64 and will change again.

Gotchas and Tips

  • total() returns a string. This is the single most common bug: Cart::total() * 100 or passing it to a payment gateway silently produces "1,234.50". Use Cart::totalFloat(), subtotalFloat(), taxFloat(), weightFloat(). The same split applies to CartItem ($item->price string vs. $item->priceFloat).
  • 4.2.0 breaks previously persisted carts. The store() payload is now base64-encoded (fix for PostgreSQL truncation, PR #167). Any rows written by an earlier version are in the old raw-serialised format and restore() will not read them correctly. Before deploying 4.2.0, either flush the shoppingcart table (DB::table(config('cart.database.table'))->truncate()) or write a one-off migration that re-encodes each row. Silently keeping the old rows means returning users get an empty cart.
  • PostgreSQL and the persistence table. If you are on Postgres and see store() appear to succeed while restore() returns nothing — or see truncation errors in the logs — you are on <4.2.0. Upgrade rather than widening the column; the encoding, not the length, was the problem.
  • Config format shape changed across versions. Newer releases use an associative array (decimals, decimal_point, thousand_seperator); older 0.x-style configs used a positional array. If totals render as 0.00 or throw, open the published config/cart.php rather than copying a snippet from a blog post.
  • add() increments when id + options match. Adding the same product with the same options merges quantities rather than creating a second row. If you need two distinct rows, give them a distinguishing option key (or an intentional one, like 'line' => $uuid).
  • update() with 0. Set quantity to zero via remove($rowId) explicitly. Relying on update($rowId, 0) has changed behaviour across versions of this fork family and leaves zero-qty rows behind in some of them.
  • instance() is sticky state on the singleton. After Cart::instance('wishlist'), every subsequent Cart:: call in that request hits the wishlist until you call Cart::instance('cart') again. In a controller that touches two carts, always qualify both.
  • Session driver matters. A full cart serialised into a cookie-backed session will blow the 4 KB limit and silently drop the cart on the next request. Use file, redis or database sessions for anything real.
  • destroy_on_logout in config plus your own store()/restore() listeners will fight each other. Pick one: either the config flag or explicit event listeners.
  • Global tax is one number. Cart::setGlobalTax(21) applies to the whole instance; per-item overrides go in the add() payload as a tax key. Mixed-rate baskets (food vs. standard VAT) need the per-item key on every add, not just the special ones — items added without it inherit the global rate.
  • Floats don't add up. Prices are stored as PHP floats, so subtotalFloat() can drift by fractions of a cent over many rows. Round once at display and once at order creation; never compare two independently-computed totals with ===.
  • Nothing is validated. No stock checks, no price re-checks, no CSRF-safe quantity clamping. qty is whatever you pass in — clamp and max: it in the FormRequest.
  • Maintenance risk. 4.2.0 is a single targeted community fix (a base64 encoding change for Postgres) rather than a sign of active feature development; the API surface and the 2022-era deprecation picture are otherwise unchanged. Pin the version in composer.json, and if you're on PHP 8.2+ watch for dynamic-property deprecations on CartItem; a #[AllowDynamicProperties] shim or a small compat release is the fast fix. Budget for a fork or in-house replacement if you stay long-term — and note that any such fork must keep the base64 encoding, or you'll re-introduce the Postgres bug.
  • Debugging tip: Cart::content() is a Collection keyed by rowId. dump(Cart::content()->toArray()) gives you the full serialised state, including options, in one shot — faster than stepping through totals methods. When persistence misbehaves, dump content() after restore(), not the DB row: the column is now opaque base64 and will tell you nothing by eye.
  • Extension point: the package exposes no events. If you need "item added" hooks, wrap Cart in your own CartService and fire your own events there; that's also the right seam for coupon logic and re-pricing — and the right place to hide the store()/restore() upgrade concern if you ever swap the package out.
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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core