anayarojo/shoppingcart
A flexible shopping cart package for Laravel with a simple API for adding, updating, and removing items, applying conditions and attributes, and persisting carts to session or database storage. Well suited for building e-commerce checkout flows.
This package is a maintained fork of gloudemans/shoppingcart (Crinsane), so the upstream README and every tutorial you find for Cart::add() / Cart::content() apply almost verbatim — just swap the namespace to Anayarojo\ShoppingCart.
As of 4.2.5 the package's dependency constraints allow Laravel 13 (a laravel-shift dependency bump). That release is dependency-only — no src/ changes, no API changes — so all usage below is unchanged. On Laravel 13 you need ^4.2.5; older 4.2.x pins will refuse to install.
composer require anayarojo/shoppingcart
php artisan vendor:publish --provider="Anayarojo\ShoppingCart\ShoppingCartServiceProvider"
(Publish creates the config file — config/cart.php in upstream forks, sometimes config/shoppingcart.php. The publish tag is listed in the provider's boot() method; check there if --tag fails.)
Verify the Cart facade alias exists in config/app.php. Older forks don't auto-alias it; add 'Cart' => Anayarojo\ShoppingCart\Facades\Cart::class if Cart:: throws "class not found."
Where to look first, in order:
src/ root class (Cart.php / ShoppingCart.php) — the entire public API is 300–500 lines and is the real documentation.src/CartItem.php and src/CartItemOptions.php — tells you exactly what a row looks like.tests/ — the fastest source of truth for method signatures, especially update() and store()/restore().config/cart.php — tax rate, number formatting, session key.First use case — add to cart from a controller and render it:
use Anayarojo\ShoppingCart\Facades\Cart;
public function add(Product $product)
{
Cart::add(
$product->id,
$product->name,
1,
$product->price, // float, see Gotchas
['size' => 'L', 'image' => $product->image_url]
);
return back()->with('status', 'Added to cart');
}
@foreach (Cart::content() as $row)
<tr>
<td>{{ $row->name }}</td>
<td>{{ $row->qty }}</td>
<td>{{ $row->price }}</td>
<td>{{ $row->subtotal }}</td>
<td>
<form method="POST" action="{{ route('cart.remove', $row->rowId) }}">
@csrf @method('DELETE')
<button>Remove</button>
</form>
</td>
</tr>
@endforeach
<p>Subtotal: {{ Cart::subtotal() }} • Tax: {{ Cart::tax() }} • Total: {{ Cart::total() }}</p>
You now have a working session cart. Everything else is controller glue plus a checkout routine.
Wrap the facade in one service class. Don't sprinkle Cart:: across twenty controllers — you'll want to swap it, fake it in tests, and add cross-cutting logic (price revalidation, logging, discount rules) in one place.
class CartService
{
public function add(Product $p, int $qty = 1, array $options = []): string
{
return Cart::add($p->id, $p->name, $qty, $p->price, $options);
}
public function update(string $rowId, int $qty): void
{
Cart::update($rowId, ['qty' => $qty]);
}
public function items(): Collection { return Cart::content(); }
public function count(): int { return Cart::count(); }
public function clear(): void { Cart::destroy(); }
}
Row identity is id + options, not id alone. The rowId is a hash of the product id and the serialized options. This is the single most useful fact about the package:
size → two rows, both survive.add().update($rowId, ['options' => ...]) if the fork supports it).Always address lines by rowId from content(), never by product id.
Standard CRUD controller shape. Add / update / remove / clear, each redirecting back with a flash message so the cart page and the mini-cart in the header both refresh. For a quantity input, loop the POSTed items[rowId => qty] array and skip zero-quantity rows (or remove() them).
Multi-cart via instances. Use Cart::instance('wishlist') for a wishlist alongside the main cart, or for admin-side order entry. Instances have independent contents and independent store()/restore() keys — a common trick is one instance per product bundle or per vendor in a marketplace.
Persist across sessions for logged-in users. Cart::store($user->id) writes the cart to the session under that key; Cart::restore($user->id) brings it back. Call store() after each mutation (or in a middleware/terminating callback) and restore() on login. This is how you survive logout and switching devices within the same session cookie — it is not a database cart, so it does not survive a different browser.
Checkout is where the package stops. A reliable order-creation flow:
DB::transaction(function () {
$order = Order::create(['user_id' => auth()->id(), 'total' => 0]);
foreach (Cart::content() as $row) {
$product = Product::findOrFail($row->id); // re-verify, don't trust session
abort_if($product->price != $row->price, 409, 'Price changed');
$order->items()->create([
'product_id' => $product->id,
'name' => $row->name,
'price' => $product->price,
'qty' => $row->qty,
'options' => $row->options->toArray(),
]);
}
$order->update(['total' => Cart::total(2, '.', '')]);
Cart::destroy();
});
Snapshot name, price, and options into order_items — product rows change, order history must not.
Header cart count without touching every controller. A view composer or a Blade component that calls Cart::count()/Cart::total() keeps the "3 items — $59.90" badge consistent and centralizes the formatting.
Discounts and shipping are yours to compute. The package only knows subtotal + a flat config tax + total. Apply coupons by iterating content(), computing a discount, and storing it in the session alongside the cart — then subtract at display and checkout time. Don't try to mutate price on rows; you lose the original for receipt rendering.
Events. Newer forks dispatch add/update/remove events. If yours does, hooking a listener for abandoned-cart tracking or inventory reservation is cleaner than post-controller code; grep event( in src/ to confirm before relying on it.
Upgrade path for the 4.2.x line. 4.2.5 is a dependency-only release, so upgrading is low-risk. If your app is on Laravel 13, bump the constraint and update with composer update anayarojo/shoppingcart -W; if you're on Laravel 10/11/12, there's nothing to gain and a widened illuminate/support range can pull in transitive versions your app hasn't locked — stay where you are until you plan the framework upgrade.
SESSION_DRIVER=array (the default in phpunit.xml) means the cart vanishes between test requests. Either use file/database in tests, or assert within a single request after $this->withSession([...]).add() accepts a float price and totals are float math; 19.99 * 3 will bite you eventually. For money-critical apps pass integer cents (or strings) and format only at render. If the fork has no minor-unit support, round explicitly with round($x, 2) before persisting an order total.subtotal(), tax(), total() return formatted strings by default, driven by config (format, decimals, thousandsSeparator). total(2, '.', '') gives you a raw numeric string. Never feed Cart::total() straight into a payment gateway — that's why the checkout snippet above strips formatting.tax at 0.options. The options array is serialized into the session. A model serializes to an id and rehydrates badly (or explodes on schema changes). Store scalars: ids, slugs, URLs, labels.file, redis, or database in any real deployment.config:cache after publishing. The config is read at runtime, but a stale cached config after you edit tax is a classic "my tax change did nothing" bug. php artisan config:clear.destroy() vs remove(). remove($rowId) kills one line; destroy()/clear() wipes the instance. Easy to fat-finger in a route:list-less codebase — name your routes cart.remove and cart.clear distinctly.update() and remove() with a stale rowId typically fail quietly or throw a CartItemNotFoundException depending on the fork version. Wrap in try/catch and redirect with an error rather than a 500 on a double-submitted remove form.dd(Cart::content()) first; then look at the raw session key (session()->get('cart') or config('cart.session_key')) to confirm what's actually persisted. If content() is empty right after add(), you almost always have a session-driver or middleware-ordering problem (StartSession not applied to the API route group).extend() the package class if you need custom totals; publish and override the config; the package ships no views, so cart UI is 100% yours. There is no repository/DB layer to hook into, which is the main architectural limitation — if you need a DB-backed cart for multi-device persistence, plan to write it yourself and keep this package only as a session scratchpad.illuminate/support for Laravel 13, pin ^4.2.5 on L13 apps and check composer why-not anayarojo/shoppingcart 4.2.5 on older frameworks before assuming a conflict is the package's fault. When the next Laravel major lands, watch for the same laravel-shift dependency-bump PR — a package that only ever gets dependency bumps has an untested API surface against new framework versions, so run your own cart tests after any framework upgrade rather than trusting the release note.How can I help you explore Laravel packages today?