PDO Transactions
debt(d9/e3/b3/t7)
Closest to 'silent in production until users hit it' (d9). The detection_hints field explicitly states 'automated: no' and the code pattern (multiple execute() calls without transaction wrapping) produces no compiler error, no default linter warning, and no runtime exception — the database simply ends up in a partially corrupted state only visible when a mid-operation crash occurs in production.
Closest to 'simple parameterised fix (replace pattern with safer alternative)' (e3). The quick_fix states 'Wrap multi-step operations in beginTransaction() / commit() with a catch block that calls rollBack()' — this is a small, localised refactor within the affected function or component, not a one-liner but not cross-cutting either.
Closest to 'localised tax' (b3). The choice applies to web and CLI contexts but is scoped to database-interaction code. Once correctly wrapped, transactions don't impose ongoing weight on unrelated parts of the codebase; the burden is felt only in DB-touching components.
Closest to 'serious trap (contradicts how a similar concept works elsewhere)' (t7). The misconception field directly identifies the trap: calling beginTransaction() twice appears to nest transactions (as supported in other databases or ORMs), but MySQL silently commits the first transaction instead — contradicting reasonable developer expectations about nesting behaviour and requiring SAVEPOINTs as a non-obvious workaround.
Also Known As
TL;DR
Explanation
beginTransaction() disables auto-commit. commit() writes all changes atomically. rollBack() undoes everything since beginTransaction(). PDO throws a PDOException if beginTransaction() is called when already in a transaction. Savepoints (SAVEPOINT name / ROLLBACK TO name) allow partial rollbacks within a transaction. MySQL requires InnoDB tables — MyISAM does not support transactions.
Diagram
sequenceDiagram
participant PHP
participant MySQL
PHP->>MySQL: beginTransaction()
PHP->>MySQL: UPDATE accounts SET balance=balance-100
PHP->>MySQL: UPDATE accounts SET balance=balance+100
alt success
PHP->>MySQL: commit()
MySQL-->>PHP: OK
else exception
PHP->>MySQL: rollBack()
MySQL-->>PHP: reverted
end
Common Misconception
Why It Matters
Common Mistakes
- Forgetting rollBack() in the catch block — failed transactions hold locks until the connection closes.
- Using MyISAM tables which silently ignore transaction boundaries.
- Opening long-running transactions that hold row locks and cause deadlocks under concurrency.
Avoid When
- Avoid long-running transactions — they hold row locks and cause deadlocks under concurrency.
- Do not open a transaction and then make external HTTP calls before committing — network failures leave transactions open.
When To Use
- Use transactions for any operation that modifies multiple rows or tables that must succeed or fail together.
- Use transactions for financial operations, inventory management, and any multi-step state change.
Code Examples
// No transaction — partial failure leaves DB inconsistent
$pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
->execute([$amount, $fromId]);
// Exception here? $fromId debited, $toId never credited.
$pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
->execute([$amount, $toId]);
$pdo->beginTransaction();
try {
$pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')
->execute([$amount, $fromId]);
$pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')
->execute([$amount, $toId]);
$pdo->commit();
} catch (\Throwable $e) {
$pdo->rollBack();
throw $e;
}