wearepixel/laravel-cart
A Laravel shopping cart package for managing cart items, quantities, prices, and totals with flexible storage options. It provides a simple API for adding, updating, and removing items, making it easy to integrate into ecommerce checkout flows.
v3.0.0 is additive for most apps — if you already have a working cart, nothing breaks unless you instantiate Cart directly or reference CartSession (see Gotchas). New installs should start here:
php artisan cart:install. This publishes the config and creates the app/Cart/ directory structure for your first-class coupon/tax/shipping classes.config/cart.php first — in particular the driver setting. The default SessionDriver behaves like the old package; DatabaseDriver/RedisDriver are the options to reach for when carts must survive session expiry or be shared across devices.php artisan cart:make:coupon FixedAmountOff, extend the stub, and apply it with Cart::coupon(new FixedAmountOff(...)).Cart::fake() and assertTotal() — this is the single biggest day-to-day win of the release.If you're upgrading, run php artisan cart:debug in a local environment to dump the current cart and confirm the driver/config wiring is intact before touching feature code.
Choose your driver deliberately, not by default.
Session is fine for guest carts, but the moment you need "cart survives logout" or "cart visible in an admin panel", switch to DatabaseDriver or RedisDriver. MultiDriver is the escape hatch for hybrid setups (e.g. read-through from Redis, fall back to session). Your app code should not care — resolve Cart from the container and inject CartDriver only in tests or health checks.
Wrap domain concepts in first-class objects.
Coupon, TaxRule, and ShippingRate are abstract classes that convert to CartCondition. Do the conversion inside the subclass, keep the constructor taking plain scalars/IDs, and never let a subclass hold an Eloquent model you don't intend to serialise. This is the pattern to standardise on — a app/Cart/Coupons/ directory of small typed classes is far easier to test and audit than condition arrays scattered through controllers.
Use applies_to for capped discounts.
This is easy to miss and immediately useful: a "20% off your first 3 units" promo is now a condition attribute instead of a manual quantity calculation in your controller. Prefer it over pre-computing totals.
Keep cart mutations in one service layer.
Controllers, Livewire components, and Inertia share endpoints should all call the same CartService methods. The new HasCart trait and ShareCartWithInertia helper are presentation adapters — let them read state and call proxies, not own business rules. This keeps your Livewire and Inertia surfaces consistent and makes Cart::fake() tests cover the real path.
Testing pattern (adopt this everywhere).
Cart::fake()
->withItems($product, 2)
->withCoupon(new TenPercentOff())
->withCondition($shipping);
Cart::assertTotal(1980);
Cart::fake() swaps the active driver for an in-memory NullDriver and returns a CartFactory, so tests no longer need a session or database. Seed via the factory, assert via the assertion methods, and prefer assertTotal()/assertConditionApplied() over poking at the cart internals — those assertions are the contract that survives future releases.
Inertia sharing.
Wire ShareCartWithInertia once in HandleInertiaRequests::share() so $page.props.cart is available to every page component. Don't duplicate the logic per-controller — otherwise you get drift between pages that show the cart badge and pages that don't.
CartSession is gone. Any type hint, new Cart(...) call, or container binding referencing it will fail. Constructor now takes a CartDriver; if you resolved from the container you're fine, if you hand-rolled new Cart you are not.config to cart-config. Update deploy scripts and vendor:publish --tag=config invocations in your docs/CI, or the publish silently does nothing and you wonder why config changes aren't taking effect.CartCondition (or your Coupon subclass) holding a model instance, closure, or resource will not round-trip cleanly. Keep condition state to scalars and IDs, and re-resolve models at display time.Cart::fake() replaces the active driver for the duration of the test. It's in-memory, so nothing persists between tests — but if you assert on database rows or session contents in the same test you'll be looking at an empty store. Split those concerns.cart:debug only runs outside production by design. Don't build internal support tooling on top of it; it's a local dev convenience.cart:make:* commands validate class names and check write failures — good, but they generate stubs, not finished classes. Read the generated Coupon/TaxRule/ShippingRate before assuming the maths is right; the abstract class only handles condition conversion.@mixin/@method restored means you can now rely on static analysis against the facade. Add the package to your own PHPStan runs (level 5 is a reasonable floor) rather than excluding it as you may have done previously.CartDriver and register it with CartManager — that's the supported seam. Avoid subclassing a built-in driver; the serialisation contract is the thing most likely to shift in future majors.HasCart reactivity assumes the cart is mutated through the trait's proxies. Direct mutations elsewhere (e.g. a queued job, another component) won't trigger a re-render — call $this->refresh() (or your equivalent) after external changes instead of debugging phantom stale totals.How can I help you explore Laravel packages today?