sumonwd / laravel-cart
Session and database backed shopping cart and wishlist for Laravel, with multiple instances, vouchers, tax, shipping and guest-to-user merge on login.
Requires
- php: ^8.2
- illuminate/auth: ^11.0|^12.0|^13.0
- illuminate/database: ^11.0|^12.0|^13.0
- illuminate/events: ^11.0|^12.0|^13.0
- illuminate/session: ^11.0|^12.0|^13.0
- illuminate/support: ^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.5|^11.0|^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
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-Identifierheader CartUpdatedandWishlistUpdatedevents
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-Identifierheader 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.