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

Cart Laravel Package

vanilo/cart

Vanilo Cart provides a robust shopping cart for Laravel e-commerce apps. It manages cart items, quantities, pricing, taxes, and multiple cart instances, with session or database storage and a clean API for building checkout flows.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal path from composer require to a working cart.

1. Install and publish.

composer require vanilo/cart
php artisan vendor:publish --tag=vanilo.cart   # config + migration
php artisan migrate

The package ships a cart_items table migration and a config/cart.php. If you skip publishing, the package defaults still work but you lose the ability to swap the item model or table name.

2. Where to look first, in order:

  • config/cart.php — the only real config surface. Key knobs are the cart-item model class and the table name (items.model, items.table).
  • Vanilo\Cart\Facades\Cart — the whole day-to-day API lives here.
  • Vanilo\Cart\Contracts\CartContract and CartItemContract — the behavioral contract; read this before extending.
  • Vanilo\Cart\Cart / CartManager — the default implementation and its lifecycle (session-bound).

3. The first use case: add a product, read the total.

The cart is session-backed, so the only prerequisite is that your routes have a session (web middleware group). There is no cart middleware to register.

use Vanilo\Cart\Facades\Cart;

Route::post('/cart', function (Request $request) {
    $product = Product::findOrFail($request->product_id);

    Cart::addItem($product, $request->integer('quantity', 1));

    return redirect('/cart');
});

Route::get('/cart', fn () => view('cart', [
    'items' => Cart::getItems(),
    'total' => Cart::total(),
    'count' => Cart::itemCount(),
]));

The argument to addItem() must implement Vanilo\Contracts\Buyable — a plain Eloquent model that doesn't implement it will throw InvalidCartItemException. If you're using vanilo/products, the Product model already satisfies this.

Implementation Patterns

Line-item identity is (product, extra attributes). Calling addItem twice with the same product and the same $extra array merges quantities; different extras create separate lines. This is how you model variants without a variant table:

Cart::addItem($product, 1, ['size' => 'L', 'color' => 'black']);
Cart::addItem($product, 1, ['size' => 'L', 'color' => 'black']); // qty 2 on one line
Cart::addItem($product, 1, ['size' => 'M', 'color' => 'black']); // second line

Mutating existing lines. Look items up rather than iterating and rebuilding:

$item = Cart::getItemById($itemId);
Cart::setQuantity($item, 3);      // to a value
Cart::removeItem($item);
Cart::destroy();                  // nuke everything (post-checkout, logout)

Guard state in views with Cart::exists() / Cart::isEmpty() rather than counting getItems() yourself.

Checkout hand-off. The cart package deliberately stops at "here is a basket." It does not create orders. The pattern is: read getItems() → map each CartItemContract into your order-line DTO (copying getPrice() and quantity) → only call Cart::destroy() after the order row is committed. Wrapping that in a DB transaction but destroying the cart before commit is the classic way to lose baskets on failure.

Custom item model. To persist extra fields (gift note, variant FK, promo attribution), subclass the default and repoint config:

// app/Models/CartItem.php
class CartItem extends \Vanilo\Cart\Models\CartItem
{
    protected $guarded = [];
}
// config/cart.php
'items' => ['model' => App\Models\CartItem::class],

Add columns via your own migration against the same table. Do this before you have production carts — changing the model class later orphans rows unless the schema matches.

Event hooks. The package dispatches cart events on add/remove/clear. Rather than editing package code, register listeners in a service provider (Event::listen(...)) to do cross-cutting work: recompute shipping estimates, invalidate a cached mini-cart widget, log abandonments. This is the intended extension point for "cart changed" reactions.

UI integration. Keep quantity mutations as discrete POST/DELETE routes with redirects, not as a single AJAX blob — the cart is session state, and concurrent tab mutations will race. If you do use AJAX, send a CSRF token and re-render the totals from the response rather than optimistically updating the DOM.

Gotchas and Tips

It is session-only by default. There is no carts table and no cart ID; the contents live in the session under a package-managed key. Consequences: no multi-device sync, no admin view of live carts, and a cart is lost when the session expires or the user is logged out (unless you migrate it). If you need persistence you must supply your own storage — the package is not going to do it for you.

Merging surprises. Users editing an $extra value (e.g. changing size from L to M) will silently produce a new line rather than updating the old one, because identity includes the extras. Your "update cart" endpoint must explicitly removeItem the old line when the attributes change.

N+1 on getItems(). Each CartItemContract resolves its buyable lazily. A cart page showing 20 lines can fire 20 queries. Eager-load by resolving the products in one query and joining in PHP, or cache the resolved models on the item collection for the request.

Money and rounding. total() is derived from per-item price × quantity. Do not re-round each line and then sum — accumulate at full precision and round once at display time, or totals will drift by cents against your order total. Also assert currency consistency: swapping a Buyable that reports a different currency mid-cart gives you a nonsense total with no exception.

destroy() is not idempotent-safe in the wrong place. It's fine to call on an empty cart, but calling it in a finally block around checkout will wipe the basket when payment fails and the user hits back. Destroy only on confirmed order.

Config caching. After editing config/cart.php (especially the item model or table), run php artisan config:clear. A cached config pointing at a model you've since renamed produces confusing Class not found errors from deep inside the item factory.

Migration ordering. If you publish later than your first deploy, Cart::addItem() fails with a missing-table query exception, not a friendly error. Ensure the published migration is committed and run in every environment, including CI where RefreshDatabase needs the table before any cart test runs.

Testing. The cart is bound to the session, so tests need $this->withSession([]) or a session started per test. Flush between tests — Cart::destroy() in setUp — otherwise state leaks across test methods and makes quantity-merge assertions flaky. For unit tests of pricing logic, prefer constructing CartItem directly over driving the facade.

Extension points, ranked by safety: (1) config → custom item model; (2) events/listeners for side effects; (3) binding a custom implementation of CartContract in a service provider for storage changes — this is the sanctioned way to move off sessions, but it is the most invasive and worth only doing if you genuinely need server-side carts.

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.
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle