php
63 lines · 8 steps
Placing an order atomically in Laravel
A service wraps order creation, stock deduction, and coupon redemption in one retryable database transaction.
Explained by
highlit
1<?php
2
3namespace App\Services;
4
5use App\Models\Order;
6use App\Models\Product;
7use App\Exceptions\InsufficientStockException;
8use App\Events\OrderPlaced;
9use Illuminate\Support\Facades\DB;
10
11class OrderCreator
12{
13 public function place(array $items, int $customerId, string $couponCode = null): Order
14 {
15 return DB::transaction(function () use ($items, $customerId, $couponCode) {
16 $order = Order::create([
17 'customer_id' => $customerId,
18 'status' => 'pending',
19 'total' => 0,
20 ]);
21
22 $total = 0;
23
24 foreach ($items as $item) {
25 $product = Product::lockForUpdate()->findOrFail($item['product_id']);
26
27 if ($product->stock < $item['quantity']) {
28 throw new InsufficientStockException($product->id);
29 }
30
31 $product->decrement('stock', $item['quantity']);
32
33 $order->lines()->create([
34 'product_id' => $product->id,
35 'quantity' => $item['quantity'],
36 'unit_price' => $product->price,
37 ]);
38
39 $total += $product->price * $item['quantity'];
40 }
41
42 if ($couponCode) {
43 $coupon = $this->redeemCoupon($couponCode, $order);
44 $total -= $coupon->discountFor($total);
45 }
46
47 $order->update(['total' => $total, 'status' => 'confirmed']);
48
49 event(new OrderPlaced($order));
50
51 return $order;
52 }, 3);
53 }
54
55 protected function redeemCoupon(string $code, Order $order): Coupon
56 {
57 $coupon = Coupon::where('code', $code)->active()->lockForUpdate()->firstOrFail();
58 $coupon->decrement('remaining');
59 $coupon->redemptions()->create(['order_id' => $order->id]);
60
61 return $coupon;
62 }
63}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Wrapping multi-step writes in a transaction ensures the order, stock, and coupon changes all succeed or all roll back together.
- 2Locking rows for update prevents two concurrent orders from overselling the same product or coupon.
- 3Firing a domain event after the commit-worthy work lets side effects like emails stay decoupled from the core logic.
Related explainers
php
<?php class NameParser {
Parsing a full name into components in PHP
string-parsing
arrays
normalization
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
Intermediate
8 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
php
<?php final class RotatingFileLogger {
How a rotating file logger works in PHP
logging
file-rotation
io
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/placing-an-order-atomically-in-laravel-explained-php-1586/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.