php
45 lines · 8 steps
Atomic order placement with PDO transactions
A repository writes an order, its items, and stock decrements as one all-or-nothing database transaction.
Explained by
highlit
1<?php
2
3final class OrderRepository
4{
5 public function __construct(private PDO $pdo)
6 {
7 $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
8 }
9
10 public function placeOrder(int $customerId, array $lineItems): int
11 {
12 $this->pdo->beginTransaction();
13
14 try {
15 $insertOrder = $this->pdo->prepare(
16 'INSERT INTO orders (customer_id, status, created_at) VALUES (?, ?, NOW())'
17 );
18 $insertOrder->execute([$customerId, 'pending']);
19 $orderId = (int) $this->pdo->lastInsertId();
20
21 $insertItem = $this->pdo->prepare(
22 'INSERT INTO order_items (order_id, sku, quantity, unit_price) VALUES (?, ?, ?, ?)'
23 );
24 $decrementStock = $this->pdo->prepare(
25 'UPDATE products SET stock = stock - ? WHERE sku = ? AND stock >= ?'
26 );
27
28 foreach ($lineItems as $item) {
29 $insertItem->execute([$orderId, $item['sku'], $item['qty'], $item['price']]);
30 $decrementStock->execute([$item['qty'], $item['sku'], $item['qty']]);
31
32 if ($decrementStock->rowCount() === 0) {
33 throw new RuntimeException("Insufficient stock for SKU {$item['sku']}");
34 }
35 }
36
37 $this->pdo->commit();
38
39 return $orderId;
40 } catch (Throwable $e) {
41 $this->pdo->rollBack();
42 throw $e;
43 }
44 }
45}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Wrapping related writes in a transaction guarantees they all succeed or all roll back, leaving no half-finished orders.
- 2A conditional UPDATE plus rowCount() enforces business invariants like stock availability at the database level.
- 3Preparing a statement once and executing it per item reuses the query plan and keeps values safely bound.
Related explainers
php
<?php namespace App\Notifications;
How a queued weekly digest email works in Laravel
notifications
queues
email
Intermediate
7 steps
javascript
import { parsePhoneNumberFromString } from 'libphonenumber-js'; export class PhoneValidationError extends Error { constructor(message, code) {
Normalizing phone numbers with typed errors
validation
error-handling
custom-errors
Intermediate
6 steps
typescript
import { Injectable, computed, inject, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs';
Optimistic updates with Angular signals
signals
optimistic-updates
state-management
Intermediate
9 steps
python
import os from datetime import datetime, timezone import jwt
JWT bearer auth as a FastAPI dependency
authentication
jwt
dependency-injection
Intermediate
7 steps
rust
use std::error::Error; use std::path::Path; use serde::Deserialize;
Custom CSV deserialization with serde in Rust
serde
csv
deserialization
Intermediate
7 steps
ruby
class CartMergeService def initialize(guest_cart:, user_cart:) @guest_cart = guest_cart @user_cart = user_cart
Merging a guest cart into a user cart in Rails
service object
transactions
idempotency
Intermediate
7 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/atomic-order-placement-with-pdo-transactions-explained-php-c992/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.