Атомарный перевод средств между двумя счетами через PDO
Реализуйте Ledger::transfer. Метод должен списать с одного счёта и зачислить на другой так, чтобы изменились обе строки либо ни одной, и не должен прятать сбой — вызывающий обязан узнать, что перевод не состоялся.
Требования: используйте внедрённый PDO, каждое значение передавайте параметром и ни на одном пути не оставляйте открытую транзакцию.
<?php
class Ledger {
public function __construct(private PDO $pdo) {}
public function transfer(int $fromId, int $toId, int $amountCents): void {
// ваш код здесь
}
}
Допишите реализацию.
Оберните оба UPDATE в beginTransaction() … commit(), а в catch вызовите rollBack() и пробросьте исключение дальше, чтобы вызывающий узнал о сбое. При PDO::ERRMODE_EXCEPTION упавший запрос бросает исключение сам. Значения $fromId, $toId и сумму передавайте параметрами, не интерполируйте их.
- ✗Обойтись без транзакции и «чинить» неудавшееся списание компенсирующей записью
- ✗Поймать исключение и выйти, из-за чего вызывающий считает перевод успешным
- ✗Интерполировать id и сумму в SQL вместо связывания параметрами
- →От чего страхует
PDO::inTransaction(), еслиtransfer()могут вызвать из внешней транзакции? - →Как заставить перевод отказать при недостатке средств, не выходя из той же транзакции?
Транзакция обязана быть замкнутой: beginTransaction() открывает её, commit() закрывает на успешном пути, а catch откатывает и пробрасывает исключение дальше — иначе вызывающий решит, что перевод прошёл.
<?php
class Ledger {
public function __construct(private PDO $pdo) {}
public function transfer(int $fromId, int $toId, int $amountCents): void {
$this->pdo->beginTransaction();
try {
$debit = $this->pdo->prepare(
'UPDATE accounts SET balance = balance - :amt WHERE id = :id AND balance >= :amt'
);
$debit->execute(['amt' => $amountCents, 'id' => $fromId]);
if ($debit->rowCount() !== 1) {
throw new RuntimeException('Insufficient funds'); // ❌ откат
}
$credit = $this->pdo->prepare(
'UPDATE accounts SET balance = balance + :amt WHERE id = :id'
);
$credit->execute(['amt' => $amountCents, 'id' => $toId]);
$this->pdo->commit(); // ✅ обе строки
} catch (Throwable $e) {
$this->pdo->rollBack();
throw $e; // ⚠️ не глотать
}
}
}
Три детали, которые проверяют на собеседовании:
бросает исключение сам, и ручные проверки возвращаемого значения не нужны.
проверку баланса частью самой записи, а 0 затронутых строк означает «средств нет».
инъекция, а PDO ничего не экранирует «сам по себе».
PDO::ERRMODE_EXCEPTION(задаётся при созданииPDO) — упавший запросrowCount()после условногоUPDATE—balance >= :amtвWHEREделает- Только связанные параметры. Интерполяция
$fromIdв строку запроса — прямая