TL;DR: I built a Laravel cart package that never stores prices. Instead, it resolves them at runtime - eliminating price manipulation attacks and stale pricing bugs forever.
The Problem with Traditional Cart Packages
Every Laravel cart package I've used has the same fundamental flaw: they store prices at the moment an item is added to the cart.
This creates real problems:
// User adds item at $49.99
Cart::add($product); // Price stored: $49.99
// Admin updates price to $59.99
$product->update(['price' => 5999]);
// User checks out hours later... still pays $49.99
// Or worse: they inspect the DOM, find the stored price, manipulate it
Stale prices. Price manipulation vulnerabilities. Race conditions during flash sales.
I decided to fix this properly.
Introducing Cartly: Real-Time Price Resolution for Laravel
After months of development, I'm releasing Cartly - a shopping cart library that takes a fundamentally different approach.
composer require vanthao03596/cartly
Core principle: Prices are never stored. They're resolved at checkout time, every time.
use Cart\Cart;
// Add to cart - no price stored
$item = Cart::add($product, quantity: 2);
// Price resolved fresh when accessed
$subtotal = Cart::subtotal(); // Always current
$total = Cart::total(); // Always accurate
Why Real-Time Resolution Matters
1. Security by Design
Price manipulation attacks become impossible. There's no stored price to tamper with.
// Traditional package - vulnerable
$_SESSION['cart']['item_1']['price'] = 100; // Attacker changes price
// Cartly - no price to manipulate
// Price is resolved from your trusted source at runtime
2. Flash Sales Actually Work
// Flash sale starts
$product->update(['price' => 1999]); // 50% off!
// All carts immediately reflect new price
// No "sorry, that sale ended" errors at checkout
3. Context-Aware Pricing
Prices can vary by user, currency, locale, or any business logic:
class TieredPriceResolver implements PriceResolver
{
public function resolve(CartItem $item, CartContext $context): ResolvedPrice
{
$user = $context->user();
return match(true) {
$user?->isPremium() => new ResolvedPrice(cents: 1999),
$user?->isWholesale() => new ResolvedPrice(cents: 1499),
default => new ResolvedPrice(cents: 2499),
};
}
}
Performance: Batch Resolution Eliminates N+1
"But resolving prices at runtime must be slow!"
Not with batch resolution:
// Traditional approach - N+1 queries
foreach ($cart->items() as $item) {
$price = $item->product->price; // Query per item
}
// Cartly - Single batch query
public function resolveMany(CartItemCollection $items, CartContext $context): array
{
$ids = $items->pluck('id')->all();
$prices = Price::whereIn('product_id', $ids)->get()->keyBy('product_id');
return $items->mapWithKeys(fn($item) => [
$item->rowId => new ResolvedPrice(cents: $prices[$item->id]->amount)
])->all();
}
Cartly automatically batch-loads models and prices. One query for models, one for prices - regardless of cart size.
Features That Actually Matter
Multiple Cart Instances
Separate cart, wishlist, and compare lists with full isolation:
// Main cart
Cart::add($product);
// Wishlist
Cart::wishlist()->add($product);
// Compare (limited to 4 items)
Cart::compare()->add($product);
// Move between instances
Cart::moveToWishlist($rowId);
Cart::moveToCart($rowId);
Flexible Conditions System
Tax, discounts, shipping - with explicit ordering control:
use Cart\Conditions\TaxCondition;
use Cart\Conditions\DiscountCondition;
// 20% discount applied first
Cart::condition(new DiscountCondition(
name: 'Summer Sale',
value: 20,
mode: 'percentage',
order: 1
));
// Tax calculated on discounted amount
Cart::condition(new TaxCondition(
name: 'VAT',
rate: 10,
order: 2
));
// Get transparent breakdown
$breakdown = Cart::getCalculationBreakdown();
// [
// 'subtotal' => 10000,
// 'conditions' => [
// 'Summer Sale' => -2000,
// 'VAT' => 800,
// ],
// 'total' => 8800
// ]
Multiple Storage Drivers
Choose what fits your architecture:
| Driver | Best For |
|---|---|
| Session | Stateless apps, quick setup |
| Database | Persistent carts, user accounts |
| Cache | High-performance, Redis/Memcached |
| Array | Testing |
// config/cart.php
'driver' => 'database',
// Or per-instance
'instances' => [
'default' => ['driver' => 'database'],
'compare' => ['driver' => 'session'],
]
Guest-to-User Cart Merging
Automatic cart merging when users log in:
'associate' => [
'auto_associate' => true,
'merge_on_login' => true,
'merge_strategy' => 'combine', // combine | keep_guest | keep_user
]
Developer Experience
All Prices in Cents
No more floating-point precision bugs:
// Internal: everything in cents
$subtotal = Cart::subtotal(); // 4999 (not 49.99)
// Display: formatted for humans
$formatted = Cart::subtotal(formatted: true); // "$49.99"
// Helpers included
format_price(4999); // "$49.99"
cents_to_dollars(4999); // 49.99
dollars_to_cents(49.99); // 4999
First-Class Testing Support
use Cart\Cart;
public function test_checkout_calculates_correct_total()
{
Cart::fake();
Cart::factory()
->withItems([
['id' => 1, 'quantity' => 2, 'price' => 1000],
['id' => 2, 'quantity' => 1, 'price' => 2500],
])
->withCondition(new TaxCondition('VAT', 10))
->create();
Cart::assertSubtotal(4500);
Cart::assertTaxTotal(450);
Cart::assertTotal(4950);
}
Event-Driven Architecture
Hook into every cart operation:
// 12 events for complete lifecycle coverage
CartItemAdding / CartItemAdded
CartItemUpdating / CartItemUpdated
CartItemRemoving / CartItemRemoved
CartClearing / CartCleared
CartMerging / CartMerged
CartConditionAdded / CartConditionRemoved
Quick Start
composer require vanthao03596/cartly
php artisan vendor:publish --provider="Cart\CartServiceProvider" --tag="config"
Make your model buyable:
use Cart\Contracts\Buyable;
use Cart\Contracts\Priceable;
use Cart\Traits\CanBeBought;
class Product extends Model implements Buyable, Priceable
{
use CanBeBought;
// That's it - price auto-detected from common attributes
// (price, sale_price, current_price, etc.)
}
Start using it:
use Cart\Cart;
// Add items
Cart::add($product, quantity: 2, options: ['size' => 'L']);
// Get totals
$total = Cart::total(formatted: true); // "$99.98"
// Clear cart
Cart::clear();
Why I Built This
I've built e-commerce systems for years. Every time, I'd either:
- Use an existing package and fight its limitations
- Build a custom cart from scratch
Neither was satisfying. Existing packages stored prices (security/freshness issues). Custom solutions meant reinventing the wheel every project.
Cartly is the cart library I wish I had 5 years ago:
- Secure by default - no price storage means no manipulation
- Always fresh - real-time resolution, not stale snapshots
- Extensible - swap any component without forking
- Well-tested - comprehensive test suite with testing utilities
- Properly typed - full PHP 8.1+ type safety
Links
- GitHub: github.com/vanthao03596/cartly
- Documentation: Full docs in the repository
- License: MIT
What's Next?
I'm actively maintaining this package and have plans for:
- More built-in condition types
- Livewire/Inertia component examples
Star the repo if this approach resonates with you. Feedback and contributions welcome!
Top comments (0)