Banco de dados com PDO: consultas seguras
1. Introdução ao PDO e conexão segura
PDO (PHP Data Objects) é uma extensão do PHP que fornece uma camada de abstração para acesso a bancos de dados. Sua principal vantagem é permitir que você trabalhe com diferentes sistemas de banco de dados (MySQL, PostgreSQL, SQLite, etc.) usando a mesma interface, além de oferecer recursos essenciais de segurança como prepared statements.
Para estabelecer uma conexão segura, utilizamos uma string DSN (Data Source Name) que define o driver, host, nome do banco e charset:
<?php
$host = 'localhost';
$dbname = 'meu_banco';
$username = 'usuario';
$password = 'senha_segura';
try {
$dsn = "mysql:host=$host;dbname=$dbname;charset=utf8mb4";
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
echo "Conexão estabelecida com sucesso!";
} catch (PDOException $e) {
echo "Erro na conexão: " . $e->getMessage();
exit;
}
A configuração PDO::ATTR_EMULATE_PREPARES => false desativa a emulação de prepared statements, garantindo que o banco de dados processe os parâmetros de forma nativa e segura.
2. Prepared statements: o pilar da segurança
Prepared statements separam a estrutura da query dos dados fornecidos pelo usuário. Isso impede que dados maliciosos sejam interpretados como parte da query, eliminando o risco de SQL injection.
Existem dois tipos de placeholders:
Placeholders nomeados (:nome):
$sql = "SELECT * FROM usuarios WHERE email = :email AND status = :status";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(':email', $email);
$stmt->bindParam(':status', $status);
Placeholders posicionais (?):
$sql = "SELECT * FROM usuarios WHERE email = ? AND status = ?";
$stmt = $pdo->prepare($sql);
$stmt->bindParam(1, $email);
$stmt->bindParam(2, $status);
Diferença entre bindParam() e bindValue():
- bindParam() vincula uma referência à variável — se a variável mudar antes de execute(), o novo valor será usado
- bindValue() vincula o valor atual da variável no momento da chamada
$nome = "João";
$stmt->bindParam(':nome', $nome); // Referência
$nome = "Maria";
$stmt->execute(); // Insere "Maria"
$nome = "João";
$stmt->bindValue(':nome', $nome); // Valor atual
$nome = "Maria";
$stmt->execute(); // Insere "João"
3. Executando consultas SELECT com segurança
Para consultas SELECT com parâmetros dinâmicos, sempre use prepare() e execute():
$busca = $_GET['busca'] ?? '';
$sql = "SELECT id, nome, email FROM usuarios WHERE nome LIKE :termo";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':termo', "%$busca%");
$stmt->execute();
// fetch() para um único resultado
$usuario = $stmt->fetch(PDO::FETCH_ASSOC);
// fetchAll() para múltiplos resultados
$usuarios = $stmt->fetchAll(PDO::FETCH_OBJ);
foreach ($usuarios as $user) {
echo $user->nome . " - " . $user->email . "<br>";
}
Modos de fetch comuns:
- PDO::FETCH_ASSOC: retorna array associativo
- PDO::FETCH_OBJ: retorna objeto anônimo
- PDO::FETCH_NUM: retorna array numérico
- PDO::FETCH_BOTH: retorna array com ambos índices
Prevenção de SQL injection em cláusulas WHERE e LIKE:
// ❌ INSEGURO - concatenação direta
$stmt = $pdo->query("SELECT * FROM usuarios WHERE id = $id");
// ✅ SEGURO - prepared statement
$stmt = $pdo->prepare("SELECT * FROM usuarios WHERE id = :id");
$stmt->execute([':id' => $id]);
4. Inserção, atualização e exclusão seguras
INSERT com prepared statements e retorno do último ID:
$dados = [
'nome' => 'Ana Silva',
'email' => 'ana@exemplo.com',
'senha' => password_hash('minha_senha', PASSWORD_DEFAULT)
];
$sql = "INSERT INTO usuarios (nome, email, senha) VALUES (:nome, :email, :senha)";
$stmt = $pdo->prepare($sql);
$stmt->execute($dados);
$novoId = $pdo->lastInsertId();
echo "Usuário cadastrado com ID: $novoId";
UPDATE com parâmetros dinâmicos:
$sql = "UPDATE usuarios SET nome = :nome, email = :email WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':nome' => 'Maria Souza',
':email' => 'maria@exemplo.com',
':id' => 5
]);
$linhasAfetadas = $stmt->rowCount();
echo "$linhasAfetadas registro(s) atualizado(s)";
DELETE com parâmetros:
$sql = "DELETE FROM usuarios WHERE id = :id AND ativo = 0";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => 10]);
if ($stmt->rowCount() > 0) {
echo "Usuário excluído com sucesso";
} else {
echo "Nenhum registro encontrado para exclusão";
}
5. Transações com PDO: atomicidade e consistência
Transações garantem que um conjunto de operações seja executado completamente ou nenhuma delas seja aplicada. Exemplo de transferência bancária:
try {
$pdo->beginTransaction();
// Remover valor da conta de origem
$sql = "UPDATE contas SET saldo = saldo - :valor WHERE id = :origem AND saldo >= :valor";
$stmt = $pdo->prepare($sql);
$stmt->execute([':valor' => 500, ':origem' => 1]);
if ($stmt->rowCount() === 0) {
throw new Exception("Saldo insuficiente");
}
// Adicionar valor na conta de destino
$sql = "UPDATE contas SET saldo = saldo + :valor WHERE id = :destino";
$stmt = $pdo->prepare($sql);
$stmt->execute([':valor' => 500, ':destino' => 2]);
// Registrar transação
$sql = "INSERT INTO transacoes (conta_origem, conta_destino, valor, data)
VALUES (:origem, :destino, :valor, NOW())";
$stmt = $pdo->prepare($sql);
$stmt->execute([':origem' => 1, ':destino' => 2, ':valor' => 500]);
$pdo->commit();
echo "Transferência realizada com sucesso!";
} catch (Exception $e) {
$pdo->rollBack();
echo "Erro na transferência: " . $e->getMessage();
}
6. Tratamento de erros e boas práticas
Modos de erro no PDO:
// ERRMODE_SILENT - não exibe erros (padrão)
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT);
// ERRMODE_WARNING - exibe warnings
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING);
// ERRMODE_EXCEPTION - lança exceções (recomendado)
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Boas práticas para logging e mensagens amigáveis:
try {
// Operações com banco
} catch (PDOException $e) {
// Log detalhado para desenvolvedores
error_log("Erro PDO: " . $e->getMessage() . " em " . $e->getFile() . ":" . $e->getLine());
// Mensagem amigável para usuário
echo "Ocorreu um erro ao processar sua solicitação. Tente novamente mais tarde.";
}
Fechamento de conexão e gerenciamento de recursos:
// O PDO fecha automaticamente ao final do script
// Mas você pode forçar o fechamento:
$pdo = null;
// Liberar resultados antes de nova query
$stmt->closeCursor();
7. Trabalhando com tipos de dados e conversão
Bind de tipos específicos:
$sql = "INSERT INTO produtos (nome, preco, quantidade, ativo, descricao)
VALUES (:nome, :preco, :quantidade, :ativo, :descricao)";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':nome', 'Teclado Mecânico', PDO::PARAM_STR);
$stmt->bindValue(':preco', 299.90, PDO::PARAM_STR);
$stmt->bindValue(':quantidade', 50, PDO::PARAM_INT);
$stmt->bindValue(':ativo', true, PDO::PARAM_BOOL);
$stmt->bindValue(':descricao', null, PDO::PARAM_NULL);
$stmt->execute();
Cuidados com datas e booleanos:
// Datas: sempre usar formato ISO (Y-m-d)
$data = '2024-01-15';
$stmt->bindValue(':data', $data, PDO::PARAM_STR);
// Booleanos: MySQL converte true/false para 1/0
$ativo = true;
$stmt->bindValue(':ativo', $ativo, PDO::PARAM_BOOL);
// Arrays: não são suportados diretamente, precisam de tratamento especial
$ids = [1, 2, 3];
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$sql = "SELECT * FROM usuarios WHERE id IN ($placeholders)";
$stmt = $pdo->prepare($sql);
$stmt->execute($ids);
8. Exemplo completo: CRUD seguro com PDO
<?php
class UsuarioDAO {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function create(string $nome, string $email, string $senha): int {
$sql = "INSERT INTO usuarios (nome, email, senha) VALUES (:nome, :email, :senha)";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':nome' => $nome,
':email' => $email,
':senha' => password_hash($senha, PASSWORD_DEFAULT)
]);
return (int) $this->pdo->lastInsertId();
}
public function read(int $id): ?array {
$sql = "SELECT id, nome, email, criado_em FROM usuarios WHERE id = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id]);
$resultado = $stmt->fetch(PDO::FETCH_ASSOC);
return $resultado ?: null;
}
public function update(int $id, string $nome, string $email): bool {
$sql = "UPDATE usuarios SET nome = :nome, email = :email WHERE id = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([
':nome' => $nome,
':email' => $email,
':id' => $id
]);
return $stmt->rowCount() > 0;
}
public function delete(int $id): bool {
$sql = "DELETE FROM usuarios WHERE id = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([':id' => $id]);
return $stmt->rowCount() > 0;
}
}
// Teste de segurança: tentativa de SQL injection
$pdo = new PDO('mysql:host=localhost;dbname=teste;charset=utf8mb4', 'root', '');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dao = new UsuarioDAO($pdo);
// Tentativa de SQL injection - será neutralizada pelo prepared statement
$tentativaInjection = "1; DROP TABLE usuarios; --";
$usuario = $dao->read($tentativaInjection);
// Resultado: retorna null (não encontra ID) ou erro de tipo (se ID não for numérico)
// A tabela NÃO será dropada
O PDO com prepared statements transforma qualquer tentativa de SQL injection em um parâmetro inofensivo, garantindo que os dados fornecidos pelo usuário nunca sejam interpretados como parte da estrutura SQL.
Referências
- PHP Manual: PDO — Documentação oficial completa sobre a extensão PDO do PHP, incluindo todos os métodos, constantes e exemplos.
- PHP Manual: Prepared Statements — Explicação detalhada sobre prepared statements e como eles previnem SQL injection.
- OWASP: SQL Injection Prevention Cheat Sheet — Guia de referência da OWASP sobre prevenção de SQL injection, com ênfase em prepared statements.
- PHP The Right Way: Database Interactions — Seção sobre interação com bancos de dados do guia "PHP do Jeito Certo", com boas práticas e exemplos com PDO.
- DevMedia: Trabalhando com PDO no PHP — Tutorial completo sobre PDO no PHP, abordando conexão, prepared statements, transações e tratamento de erros.
- TablePlus: PDO Error Modes Explained — Artigo técnico explicando os diferentes modos de erro do PDO e quando utilizar cada um.