The simplest shopping cart in Laravel 13 stores a small array in the session: product ID mapped to quantity. On every page you load the products from the database by those IDs, so prices always come from your own data and never from the browser. This tutorial builds the cart class, the routes, the controller and the Blade views for add, update, remove and total.
Targets: Laravel 13 (PHP 8.3 to 8.5), Blade and the session. This cart is for guests and logged-in users alike. A database cart or a package is better when you need carts that persist across devices.
Step 1: Create a Product model
php artisan make:model Product -mfSchema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->unsignedInteger('price_cents');
$table->timestamps();
});// app/Models/Product.php
protected $fillable = ['name', 'price_cents'];Prices are stored as integer cents (for example 1999 for 19.99) to avoid floating point rounding errors in totals. Run php artisan migrate and create a few products with the factory.
Step 2: Write a Cart class
Keep the cart logic in one class so controllers stay small. Put it in app/Services/Cart.php:
<?php
namespace App\Services;
use App\Models\Product;
use Illuminate\Support\Collection;
class Cart
{
private const KEY = 'cart';
/** @return array<int, int> product id => quantity */
public function raw(): array
{
return session(self::KEY, []);
}
public function add(int $productId, int $quantity = 1): void
{
$cart = $this->raw();
$cart[$productId] = min(($cart[$productId] ?? 0) + $quantity, 99);
session([self::KEY => $cart]);
}
public function set(int $productId, int $quantity): void
{
$cart = $this->raw();
if ($quantity <= 0) {
unset($cart[$productId]);
} else {
$cart[$productId] = min($quantity, 99);
}
session([self::KEY => $cart]);
}
public function remove(int $productId): void
{
$this->set($productId, 0);
}
public function clear(): void
{
session()->forget(self::KEY);
}
/** @return Collection<int, array{product: Product, quantity: int, subtotal: int}> */
public function items(): Collection
{
$cart = $this->raw();
return Product::whereIn('id', array_keys($cart))->get()->map(fn (Product $product) => [
'product' => $product,
'quantity' => $cart[$product->id],
'subtotal' => $product->price_cents * $cart[$product->id],
]);
}
public function totalCents(): int
{
return $this->items()->sum('subtotal');
}
}The session stores only IDs and quantities. Names and prices are read from the database each time, so a user cannot change a price, and price changes in your shop appear in existing carts automatically. The quantity is limited to 99 to stop absurd values.
Step 3: Add the controller and routes
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use App\Services\Cart;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class CartController extends Controller
{
public function index(Cart $cart): View
{
return view('cart', ['items' => $cart->items(), 'total' => $cart->totalCents()]);
}
public function add(Request $request, Cart $cart, Product $product): RedirectResponse
{
$cart->add($product->id, $request->integer('quantity', 1));
return redirect()->route('cart.index')->with('success', $product->name.' added to cart.');
}
public function update(Request $request, Cart $cart, Product $product): RedirectResponse
{
$data = $request->validate(['quantity' => ['required', 'integer', 'min:0', 'max:99']]);
$cart->set($product->id, $data['quantity']);
return back();
}
public function destroy(Cart $cart, Product $product): RedirectResponse
{
$cart->remove($product->id);
return back();
}
}use App\Http\Controllers\CartController;
use Illuminate\Support\Facades\Route;
Route::get('/cart', [CartController::class, 'index'])->name('cart.index');
Route::post('/cart/{product}', [CartController::class, 'add'])->name('cart.add');
Route::patch('/cart/{product}', [CartController::class, 'update'])->name('cart.update');
Route::delete('/cart/{product}', [CartController::class, 'destroy'])->name('cart.destroy');Laravel resolves the Cart class from the container automatically, because it has no constructor arguments.
Step 4: Build the cart view
<h1>Your cart</h1>
@forelse ($items as $item)
<div>
{{ $item['product']->name }} -
{{ number_format($item['product']->price_cents / 100, 2) }}
<form method="POST" action="{{ route('cart.update', $item['product']) }}" style="display:inline">
@csrf
@method('PATCH')
<input type="number" name="quantity" value="{{ $item['quantity'] }}" min="0" max="99">
<button type="submit">Update</button>
</form>
<form method="POST" action="{{ route('cart.destroy', $item['product']) }}" style="display:inline">
@csrf
@method('DELETE')
<button type="submit">Remove</button>
</form>
</div>
@empty
<p>Your cart is empty.</p>
@endforelse
<p>Total: {{ number_format($total / 100, 2) }}</p>On the product page add a button that posts to cart.add: a form with @csrf and route('cart.add', $product).
How do I show the item count in the header?
Share it with all views in a service provider's boot() method:
use Illuminate\Support\Facades\View;
View::composer('*', function ($view) {
$view->with('cartCount', array_sum(session('cart', [])));
});How do I check that it works?
- Add two different products and one of them twice. The cart page should show both with the right quantities.
- Change a quantity to 0 and confirm the product is removed.
- Change a product price in the database and reload the cart. The total should update.
Common mistakes
- Storing prices in the session: users or old data can create wrong totals. Store only IDs and quantities.
- Using floats for money: use integer cents or a decimal type.
- Not validating the quantity: negative or huge numbers cause errors.
- Using GET links for add or remove: use POST, PATCH and DELETE forms with CSRF.
- Forgetting to clear the cart after checkout: call
$cart->clear()when the order is saved.
FAQ
When should I use a database cart instead?
When carts must survive logout, work across devices, or you want abandoned cart emails.
Is there a package?
Yes, several cart packages exist. Check that one supports your Laravel version before using it. The class above is small enough to maintain yourself.
How do I handle stock?
Add a stock column to products and compare the quantity with it in add() and set(), then check again when the order is created.



