Médio PHP

Transação com rollback

Enunciado

Crie um script que gerencie uma conta bancária simples. Crie uma tabela contas com id e saldo. Insira uma conta com saldo 100. Tente transferir 150 para outra conta (que não existe), mas dentro de uma transação que deve ser revertida (rollback) se o saldo for insuficiente. Exiba o saldo final da conta original.

Requisitos

  • Usar beginTransaction, commit e rollBack.
  • Verificar saldo antes de debitar; se insuficiente, lançar exceção e fazer rollback.
  • Exibir o saldo final da conta após a tentativa.
  • Usar prepared statements para consultas e atualizações.

Código inicial

<?php
// Seu código aqui

Saída esperada

Saldo final: 100
Ver dica

Dentro da transação, consulte o saldo atual. Se for menor que o valor a transferir, lance new Exception('Saldo insuficiente') e capture para dar rollback.

Mostrar solução
<?php
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

// Cria tabela e insere conta inicial
$pdo->exec('CREATE TABLE contas (id INTEGER PRIMARY KEY, saldo REAL)');
$pdo->exec('INSERT INTO contas (id, saldo) VALUES (1, 100)');

// Tenta transferir 150 da conta 1 para conta 2 (inexistente)
try {
    $pdo->beginTransaction();

    // Consulta saldo atual
    $stmt = $pdo->prepare('SELECT saldo FROM contas WHERE id = :id');
    $stmt->execute([':id' => 1]);
    $saldo = $stmt->fetchColumn();

    if ($saldo < 150) {
        throw new Exception('Saldo insuficiente');
    }

    // Debita da conta 1
    $stmt = $pdo->prepare('UPDATE contas SET saldo = saldo - :valor WHERE id = :id');
    $stmt->execute([':valor' => 150, ':id' => 1]);

    // Credita na conta 2 (não existe, então o UPDATE não afeta linhas)
    $stmt = $pdo->prepare('UPDATE contas SET saldo = saldo + :valor WHERE id = :id');
    $stmt->execute([':valor' => 150, ':id' => 2]);

    $pdo->commit();
    echo "Transferência realizada.\n";
} catch (Exception $e) {
    $pdo->rollBack();
    echo "Transferência falhou: " . $e->getMessage() . "\n";
}

// Exibe saldo final
$stmt = $pdo->query('SELECT saldo FROM contas WHERE id = 1');
$saldoFinal = $stmt->fetchColumn();
echo "Saldo final: $saldoFinal\n";

Passo a passo

  1. Conectamos ao SQLite em memória e configuramos exceções.
  2. Criamos a tabela contas e inserimos uma conta com saldo 100.
  3. Iniciamos uma transação com beginTransaction().
  4. Consultamos o saldo atual da conta 1 com prepared statement e fetchColumn().
  5. Comparamos: se saldo < 150, lançamos uma exceção para forçar o rollback.
  6. Caso contrário, debitaríamos e creditaríamos, mas como o saldo é insuficiente, a exceção é lançada antes.
  7. No catch, chamamos rollBack() e exibimos a mensagem de falha.
  8. Por fim, consultamos o saldo final e mostramos que permaneceu 100.

Por que funciona

A transação garante que todas as operações sejam atômicas: ou todas são efetivadas, ou nenhuma. O rollback desfaz qualquer alteração feita desde o beginTransaction. O prepared statement evita injeção e o fetchColumn retorna um único valor.

Erros comuns

  • Esquecer de chamar rollBack() no catch: a transação fica aberta e pode causar bloqueios.
  • Não verificar o saldo antes de debitar: poderia deixar saldo negativo.
  • Usar query() em vez de prepare() para valores dinâmicos: risco de SQL injection.

Outra forma de resolver

Poderia usar $pdo->exec('BEGIN') e $pdo->exec('ROLLBACK'), mas a API orientada a objetos é mais clara e portável.

Saída esperada

Transferência falhou: Saldo insuficiente
Saldo final: 100