jackiedo/cart
jackiedo/cart is a flexible shopping cart package for Laravel with multiple cart instances, item attributes, conditions, taxes, fees, weight handling, and session persistence. It simplifies adding, updating, and removing items while calculating subtotals, totals, and other cart values.
Install with Composer and publish the config:
composer require jackiedo/cart
php artisan vendor:publish --provider="JackieDo\Cart\CartServiceProvider"
On Laravel 5.5+ the service provider and Cart facade alias are auto-discovered. On older skeletons, register JackieDo\Cart\CartServiceProvider in config/app.php and alias Cart to JackieDo\Cart\Facades\Cart.
First place to look is config/cart.php (session key, default format/precision, any instance defaults), then src/Cart.php and src/CartItem.php — the package is small enough that reading both classes takes ten minutes and removes all guesswork about argument order (add() has changed ordering across cart packages, so verify whether it's (id, name, price, qty) or (id, name, qty, price) in your version; the array form is unambiguous and safer).
Minimal first use case in a controller:
use JackieDo\Cart\Facades\Cart;
Cart::add([
'id' => $product->id,
'name' => $product->name,
'price' => $product->price,
'quantity' => 2,
'attributes' => ['size' => 'L', 'color' => 'black'],
]);
$items = Cart::content(); // Collection of CartItem
$count = Cart::count();
$total = Cart::total();
Every line item gets a rowId (a hash derived from the item id plus its attributes). That rowId — not the product id — is the handle you use for update(), remove(), and anything else you do to a single row. Build your "add to cart" route first, return the new count()/total() as JSON, and wire up the UI from there.
Wrap the facade in your own service. The facade is a convenient global; your controllers and Blade views should not depend on it directly. Put an App\Services\CartService (or a repository) around it so you can swap storage, add logging, and unit-test order assembly without touching session state in every test. This also gives you one place to normalize rounding and money formatting.
Store a snapshot, validate on checkout. Persist only what you need in attributes (name, unit price at time of add, options, image). Treat the price in the cart as a display value: re-query products and recompute totals inside a DB transaction at checkout, compare against the session total, and abort if they diverge. Sessions are user-editable and prices go stale; never build an order from cart data alone.
Use named instances for parallel carts. The package is built around multiple cart buckets, so a storefront can run cart, wishlist, and compare side by side via Cart::instance('wishlist'). Keep the instance names in a config constant and write a thin helper (cart('wishlist')) so you don't sprinkle string literals. Confirm the exact instance accessor in your installed version before committing to it.
Model cart-level adjustments as conditions. Tax, shipping, and coupon discounts belong in the condition API rather than baked into line prices. Each condition generally carries a name, a type (tax / shipping / discount), a target (subtotal or total), and a value. That makes coupons removable (Cart::condition(...) in, condition out by name) and keeps subtotal() and total() meaningful. When a discount applies to the whole cart, apply it against the subtotal before tax if that's your accounting rule — decide once and document it, because the order in which conditions are applied is the single biggest source of "the total is off by a cent" bugs.
Hook the item events for stock and rules. The package fires add/update/remove events around each mutation (CartItemAdding, CartItemAdded, CartItemUpdating, …). Register a listener that checks stock and per-customer limits and cancels the operation, which keeps the rule out of every controller. This is also the right place to log price changes or fire analytics.
Merge guest carts on login. On the Login event, read the guest session's content and replay it into the authenticated user's instance, then destroy the guest cart. Because rowIds differ by session, replay through add() rather than copying rows.
Livewire/Inertia integration. Expose a small JSON contract from your CartService: add, updateQty, remove, summary (items, subtotal, tax, shipping, total). Have the front end post to it and re-render from the response instead of mirroring cart math in JavaScript.
It is session-backed, so it only works where sessions work. API routes registered outside the web middleware group, stateless Sanctum clients, and queued jobs will all see an empty cart or fail. Attach web middleware (or explicitly start a session) to any endpoint that touches the cart. For a real API, wrap the cart in your own persistence layer keyed by user id.
Session driver matters at scale. Attributes are serialized into the session, so a cookie driver will blow past the 4 KB limit with a handful of items and silently truncate. Use the database or Redis session driver for any non-trivial store, and keep attribute payloads small (IDs and scalars, not full model snapshots).
RowId churn. Changing an item's attributes produces a different rowId; adding the same id with identical attributes merges quantities. Never key UI state (checkbox selections, "remove" buttons) on the product id — use the rowId, and re-read content() after every mutation.
Floating-point money. subtotal()/total() come back as floats. Round once, at the boundary (per line, then sum), and format for display only. Do not repeatedly add/subtract formatted strings. For anything beyond a hobby cart, cast to integer cents on the way in and out of your service.
Quantity edge cases. Decide explicitly what update($rowId, 0) does (remove vs. keep a zero row) and validate negatives at the controller. Also guard against adding a quantity that exceeds stock — the package won't do it for you unless you add a listener.
Cancelling via events. Listeners must return false to abort a mutation; returning nothing lets it through. If your listener returns the result of a method that returns null, the cancellation silently doesn't happen. Test the "out of stock" path explicitly.
destroy() vs remove() vs clear. remove($rowId) drops one row, destroy() wipes the instance (including conditions). Clear the cart after the order row is committed, not before — a failed payment shouldn't lose the basket.
Conditions in the wrong place. A condition attached to a line item and a condition attached to the cart both affect total(); debugging a wrong total means dumping both. Add a dd(Cart::content()->first()->conditions, Cart::conditions()) step before you start guessing at arithmetic.
Testing. In setUp(), call Cart::destroy() on every instance you use, and use the array form of add() in tests so you're not sensitive to positional-argument changes. If you use the database session driver in tests, swap to the array driver.
Maintenance risk. Last release was February 2023 with no dependents, so treat it as vendored code you own. Pin the version, check it against your Laravel major (Laravel 11 is untested), and be prepared to fork — the surface area is small enough that replacing Cart with your own session repository is a day's work if you ever need to. Run php artisan config:clear after editing config/cart.php, and remember that published config is cached in production.
How can I help you explore Laravel packages today?