Search by

sumonwd / laravel-cart

sumonwd

Session and database backed shopping cart and wishlist for Laravel, with multiple instances, vouchers, tax, shipping and guest-to-user merge on login.

Package info

github.com/sumonwd/laravel-cart

pkg:composer/sumonwd/laravel-cart

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-09-25 09:19 UTC

This package is auto-updated.

Last update: 2026-09-25 09:31:54 UTC


README

tests Latest Version License

A shopping cart and wishlist for Laravel 11, 12 and 13.

  • Multiple named cart instances (default, wishlist, saved, ...)
  • Persistent storage: database, cache/redis, session or file
  • Guest cart and wishlist merge into the user's cart on login
  • Vouchers (percentage, fixed amount, max discount cap) through a pluggable resolver
  • Tax and shipping totals
  • Headless/mobile guest carts through an X-Cart-Identifier header
  • CartUpdated and WishlistUpdated events

Installation

composer require sumonwd/laravel-cart

The service provider and the Cart / Wishlist facades are auto-discovered.

Publish the config and, for the database driver, the migration:

php artisan vendor:publish --tag=cart-config
php artisan vendor:publish --tag=cart-migrations
php artisan migrate

Cart usage

use Sumonwd\Cart\Facades\Cart;

// add(id, name, price, quantity = 1, attributes = [], asNewItem = false)
$rowId = Cart::add(1, 'Solitaire Ring', 1500.00, 2, [
    'size'  => '7',
    'image' => '/img/ring.png',
    'slug'  => 'solitaire-ring',
]);

// Adding the same id + attributes again increases the quantity.
// Pass asNewItem: true to always create a separate row.

Cart::update($rowId, 3);                 // new quantity (0 removes the row)
Cart::update($rowId, ['name' => 'New']); // or update fields
Cart::remove($rowId);
Cart::clear();

Cart::get($rowId);       // array|null
Cart::has($rowId);
Cart::content();         // Collection keyed by rowId
Cart::count();           // total quantity
Cart::countDistinct();   // number of rows
Cart::isEmpty();

Cart::setShipping(15);
Cart::subtotal();
Cart::discount();
Cart::tax();             // uses config('cart.tax_rate') on subtotal - discount
Cart::shipping();
Cart::total();

Each item is an array:

[
    'rowId' => '...', 'id' => 1, 'name' => 'Solitaire Ring',
    'price' => 1500.0, 'quantity' => 2, 'subtotal' => 3000.0,
    'attributes' => [...], 'image' => '/img/ring.png', 'slug' => 'solitaire-ring',
    'added_at' => '2026-09-25 10:00:00',
]

Instances

Cart::instance('saved')->add(2, 'Bracelet', 50.00);
Cart::instance('saved')->count();
Cart::instance()->count(); // back to "default"

Vouchers

$result = Cart::applyVoucher('WELCOME10');
// ['success' => true, 'message' => "Voucher 'WELCOME10' applied successfully!", 'voucher' => [...]]

Cart::getVouchers();
Cart::removeVoucher('WELCOME10'); // or removeVoucher() to remove all

By default codes come from config('cart.vouchers.codes'):

'vouchers' => [
    'multiple' => false, // true allows stacking vouchers
    'codes' => [
        'WELCOME10' => ['percentage' => 10, 'max_discount' => 50],
        'FLAT20'    => ['discount' => 20, 'is_fixed' => true],
    ],
],

To validate codes from your database, implement VoucherResolverInterface:

namespace App\Cart;

use App\Models\Voucher;
use Sumonwd\Cart\Contracts\VoucherResolverInterface;
use Sumonwd\Cart\Services\CartService;

class DatabaseVoucherResolver implements VoucherResolverInterface
{
    public function resolve(string $code, CartService $cart): ?array
    {
        $voucher = Voucher::active()->where('code', $code)->first();

        if (! $voucher || ! $voucher->isValid()) {
            return null;
        }

        return [
            'code' => $voucher->code,
            'percentage' => (float) $voucher->discount_percentage,
            'is_fixed' => false,
        ];
    }
}

Then set 'vouchers.resolver' => App\Cart\DatabaseVoucherResolver::class in config/cart.php.

Wishlist usage

use Sumonwd\Cart\Facades\Wishlist;

Wishlist::add($productId);                  // returns rowId
Wishlist::add($productId, $variantId, ['name' => 'Ring', 'price' => 99]);
Wishlist::toggle($productId);               // true = added, false = removed
Wishlist::has($productId);
Wishlist::remove($productIdOrRowId);
Wishlist::content();
Wishlist::count();
Wishlist::moveToCart($productIdOrRowId, 1);
Wishlist::clear();

The wishlist looks up the product's name, price, slug and image through a product resolver. Point it at your model:

'wishlist' => [
    'product_model' => App\Models\Product::class,
    'name_attribute' => 'name',
    'price_attribute' => 'price',
    'slug_attribute' => 'slug',
    'image_collection' => 'images', // Spatie Media Library models
    'image_attribute' => null,      // or a plain column, e.g. 'thumbnail'
],

For anything more complex, implement Sumonwd\Cart\Contracts\ProductResolverInterface and set wishlist.product_resolver.

Guest carts and login merge

  • Logged-in users (web or Sanctum) are stored as user_{id}.
  • Web guests get a random id kept in the session.
  • API clients can send an X-Cart-Identifier header to keep a guest cart.

When Illuminate\Auth\Events\Login fires, the guest's cart and wishlist are merged into the user's stored cart. Quantities of matching rows are added together, and guest vouchers are kept. Turn this off with 'merge_on_login' => false.

Events

Event Properties
Sumonwd\Cart\Events\CartUpdated items, instance, identifier
Sumonwd\Cart\Events\WishlistUpdated items, instance, identifier

Customising

Every part is bound to an interface, so you can swap it in your AppServiceProvider:

Interface Default
CartStorageInterface CartStorageService
CartCalculatorInterface CartCalculator
SessionManagerInterface CartSessionManager
VoucherResolverInterface ConfigVoucherResolver
ProductResolverInterface ModelProductResolver
$this->app->bind(
    \Sumonwd\Cart\Contracts\CartCalculatorInterface::class,
    \App\Cart\MyCalculator::class,
);

Messages can be translated after php artisan vendor:publish --tag=cart-lang.

Laravel Octane

The cart services are singletons that remember the current user. Flush them on every request in config/octane.php:

'flush' => [
    \Sumonwd\Cart\Services\CartService::class,
    \Sumonwd\Cart\Services\WishlistService::class,
],

Testing

composer test

License

MIT. See LICENSE.md.