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

Laravel Shopping Cart Laravel Package

overtrue/laravel-shopping-cart

A lightweight shopping cart package for Laravel with multiple cart instances, item quantities, price calculations, conditions, and flexible storage. Easy to integrate into simple e-commerce workflows and designed for clean, straightforward cart management.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up

composer require overtrue/laravel-shopping-cart

The package is archived (last release 2019), so pin the version explicitly and check the composer.json constraints before pulling it into a modern Laravel app. On Laravel 5.5+ the service provider is auto-discovered; on older versions register it manually:

// config/app.php
'providers' => [
    Overtrue\LaravelShoppingCart\ShoppingCartServiceProvider::class,
],
'aliases' => [
    'Cart' => Overtrue\LaravelShoppingCart\Facade::class,
],

Then publish the config (session key, formatting):

php artisan vendor:publish --provider="Overtrue\LaravelShoppingCart\ShoppingCartServiceProvider"

Two things matter more than anything else in this package:

  1. The cart lives in the session. Any route that touches Cart must be inside the web middleware group. An API route without StartSession will appear to "work" and then silently drop everything.
  2. Look first at config/shopping_cart.php. The session_key is the single value you'll inspect when debugging "where is my cart?" — session(config('shopping_cart.session_key')) is your window into the entire cart.

First use case: add from a product page

// routes/web.php
Route::post('/cart', [CartController::class, 'store'])->name('cart.store');
Route::get('/cart', [CartController::class, 'index'])->name('cart.index');
public function store(Request $request)
{
    $product = Product::findOrFail($request->input('product_id'));

    Cart::add(
        $product->id,
        $product->name,
        $product->price,
        $request->integer('quantity', 1),
        ['size' => $request->input('size')]   // attributes make the row unique
    );

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

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

content() returns a collection keyed by row id; each item exposes id, name, price, qty, attributes, and conditions. Confirm the exact accessor names on your pinned version before building views around them.

Implementation Patterns

Thin controller, row-id round trip. Every mutating action is addressed by the row id, not the product id, because the same product with different attributes is a different row. Put the row id in a hidden input and validate it:

public function update(Request $request, string $rowId)
{
    abort_unless(Cart::get($rowId), 404);

    $data = $request->validate(['quantity' => 'required|integer|min:1|max:99']);
    Cart::update($rowId, $data['quantity']);

    return back();
}

public function destroy(string $rowId)
{
    Cart::remove($rowId);
    return back();
}

Never trust the price from the request. Fetch the model, take $product->price server-side, and pass that to Cart::add(). The cart is a session blob — anyone can forge it through a cookie session driver.

Conditions are the right extension point for tax, shipping, and discounts. Implement the package's CartCondition contract and register conditions centrally rather than in each controller:

class VatCondition implements CartCondition
{
    public function __construct(protected float $rate) {}

    public function getName(): string        { return 'vat'; }
    public function getType(): string        { return 'tax'; }
    public function getTarget(): string      { return 'total'; }   // or 'subtotal'
    public function getValue(): string|float { return $this->rate . '%'; }
    public function getRules(): array        { return []; }

    public function apply(CartItem $item): float
    {
        return round($item->price * $item->qty * ($this->rate / 100), 2);
    }
}

Register in a middleware or service provider so the cart's conditions stay in sync with current config:

Cart::removeCondition('vat');
Cart::condition(new VatCondition(config('shop.vat')));

Remove-then-add keeps it idempotent — otherwise repeated requests can stack duplicate conditions into the session.

Per-item conditions (e.g. a bulk discount) go in the fifth argument of add(); global conditions apply to the whole cart. Use target subtotal for discounts and total for tax/shipping, since total conditions are applied after subtotal ones.

Checkout into real tables. The cart is a staging area only; the moment money moves, write durable rows:

DB::transaction(function () use ($request) {
    $order = Order::create([...]);
    foreach (Cart::content() as $rowId => $item) {
        $order->items()->create([
            'product_id' => $item->id,
            'qty'        => $item->qty,
            'unit_price' => $item->price,
            'options'    => $item->attributes,
        ]);
    }
    Cart::destroy();
});

Testing. Because state lives in the session, feature tests are straightforward but need a clean slate:

protected function setUp(): void
{
    parent::setUp();
    Cart::destroy();
}

Use $this->withSession([...]) to seed a pre-populated cart for update/remove tests instead of driving the whole add flow.

Gotchas and Tips

  • Archived, not maintained. Expect zero fixes for new Laravel versions. If the composer.json blocks your PHP/Laravel, don't fight it — vendor a fork or switch to a maintained alternative. Budget for an exit plan.
  • Cookie session driver will betray you. A cart with many items can exceed the ~4KB cookie limit, and the failure mode is a silently truncated or vanished cart. Use file, redis, or database sessions.
  • Logout destroys the cart. $request->session()->invalidate() (standard in Auth\LoginController) flushes the cart with everything else. If you want carts to survive logout, stash and restore the cart array around invalidation.
  • No guest→user merge. There's no built-in behavior to migrate a guest cart into a logged-in user's cart. Hook Illuminate\Auth\Events\Login and merge manually (sum quantities on matching id+attributes) if you need it.
  • Changing attributes means remove + re-add. The row id is derived from product id plus attributes; there is no "update options" call. Also never persist a row id beyond the request expecting it to be stable.
  • Floats all the way down. Prices are stored and summed as floats, and format/number_format only masks the drift. If you need correctness, store integer minor units in the cart and divide at render time, and round explicitly inside each condition's apply().
  • update($rowId, 0) is ambiguous. Some versions leave a zero-quantity row, some drop it. Be explicit: validate min:1 and call remove() when the user means "delete".
  • Serialized objects in the session. The session holds instantiated CartItem and condition objects. Renaming properties on those classes, or changing the session_key, orphans every live cart on deploy. Bump the session key on breaking changes, or clear it in your deploy script.
  • Debugging without a database. dd(Cart::content()) is the fastest look, followed by session(config('shopping_cart.session_key')) to see the raw payload. If the cart empties between requests, check middleware order and the session driver before suspecting the package.
  • Quantity is not inventory. Nothing here reserves stock or validates against on-hand counts. Do that check in the controller at add time and re-check inside the checkout transaction — the session can be arbitrarily stale.
  • Two tabs don't merge. Same session, last write wins; a shopper with the cart page open in two tabs will overwrite quantities. Re-read Cart::content() on every request rather than caching it in a service singleton.
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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast