PDO (PHP Data Objects) é o jeito nativo de falar com um banco de dados. Conecte com new PDO($dsn, $user, $password) e depois execute toda consulta que contém um valor com prepare() e execute(), para que o valor seja enviado separado do SQL:
<?php
$pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'shop_user', 'secret', [
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare('SELECT id, name FROM users WHERE email = ?');
$stmt->execute([$email]);
$user = $stmt->fetch(); // an array, or false if no row matched
Os exemplos desta página precisam de um servidor de banco de dados, então aparecem como código simples. O exemplo de SQL injection mais abaixo roda.
Conectar ao MySQL com PDO
O primeiro argumento é um DSN: o nome do driver e depois os detalhes da conexão. Coloque a conexão num único lugar e inclua-a onde precisar.
<?php
// db.php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=shop;charset=utf8mb4';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // the default since PHP 8.0
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // rows as ['id' => 1, ...]
PDO::ATTR_EMULATE_PREPARES => false, // real prepared statements, real int types
];
try {
$pdo = new PDO($dsn, getenv('DB_USER'), getenv('DB_PASS'), $options);
} catch (PDOException $e) {
error_log($e->getMessage());
http_response_code(500);
exit('Database unavailable.');
}
return $pdo;
charset=utf8mb4vai no DSN. Sem ele, acentos e emoji podem chegar como?ou bytes embaralhados.- Nunca imprima
$e->getMessage()para os visitantes: a mensagem pode conter o host e o nome de usuário. Registre em log. - Mantenha a senha fora do código, numa variável de ambiente ou num arquivo de configuração fora da raiz web.
Outros bancos só mudam o DSN: pgsql:host=localhost;dbname=shop para PostgreSQL, sqlite:/var/data/app.db para um arquivo SQLite. Cada um precisa do driver PDO instalado (pdo_mysql, pdo_pgsql, pdo_sqlite); php -m lista os que você tem.
Prepared statements com ? e :name
Um marcador indica onde vai um valor. Use marcadores posicionais ? com uma lista, ou nomeados com um array associativo:
<?php
// Positional
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = ? AND price < ?');
$stmt->execute(['books', 20]);
// Named: easier to read when there are many values
$stmt = $pdo->prepare('SELECT * FROM products WHERE category = :cat AND price < :max');
$stmt->execute(['cat' => 'books', 'max' => 20]);
// bindValue when you want to set the type explicitly
$stmt = $pdo->prepare('SELECT * FROM products ORDER BY id LIMIT :limit');
$stmt->bindValue('limit', 10, PDO::PARAM_INT);
$stmt->execute();
Não coloque aspas em volta de um marcador: WHERE name = '?' compara com o texto literal ?. Um prepared statement também pode ser executado muitas vezes com valores diferentes, então prepare-o uma vez antes de um loop, não dentro dele.
Por que concatenar strings no SQL é perigoso
Este é o único bloco da página que roda: ele monta a consulta do jeito errado e do jeito certo para a mesma entrada. Mude $email e rode de novo.
Na consulta concatenada, a aspa na entrada fecha a string, e OR '1'='1' torna a condição verdadeira para todas as linhas: a consulta retorna todos os usuários. Num formulário de login, isso significa entrar sem saber a senha, e se o driver aceitar várias instruções numa chamada, uma entrada como '; DROP TABLE users; -- executa uma segunda instrução escolhida pelo atacante. Na versão preparada, o banco recebe o SQL e o valor separados, então a aspa é só um caractere dentro de um e-mail que não bate com ninguém.
Funções de escape como addslashes() não são uma correção: elas deixam passar casos que dependem do conjunto de caracteres da conexão. Use marcadores para todo valor, incluindo números e valores que "vêm do seu próprio código".
Ler linhas: fetch, fetchAll e fetchColumn
<?php
// One row
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([42]);
$user = $stmt->fetch(); // ['id' => 42, 'name' => 'Ada', ...] or false
if ($user === false) {
http_response_code(404);
}
// All rows
$rows = $pdo->query('SELECT id, name FROM users ORDER BY name')->fetchAll();
// Loop row by row without building an array of all rows
$stmt = $pdo->query('SELECT id, name FROM users');
foreach ($stmt as $row) {
echo $row['name'], "\n";
}
// One value
$count = $pdo->query('SELECT COUNT(*) FROM users')->fetchColumn();
// Two columns as key => value
$names = $pdo->query('SELECT id, name FROM users')->fetchAll(PDO::FETCH_KEY_PAIR); // [42 => 'Ada', ...]
O query() serve quando o SQL não contém valores vindos de fora. Assim que uma variável entra, passe para o prepare().
Insert, update e delete
<?php
$stmt = $pdo->prepare('INSERT INTO users (email, name, password_hash) VALUES (?, ?, ?)');
$stmt->execute([$email, $name, password_hash($password, PASSWORD_DEFAULT)]);
$id = (int) $pdo->lastInsertId();
$stmt = $pdo->prepare('UPDATE users SET name = ? WHERE id = ?');
$stmt->execute([$newName, $id]);
echo $stmt->rowCount(); // rows changed
$stmt = $pdo->prepare('DELETE FROM sessions WHERE expires_at < NOW()');
$stmt->execute();
O lastInsertId() retorna uma string, então faça o cast. Com MySQL, o rowCount() depois de um UPDATE conta as linhas que de fato mudaram, então definir um nome com o valor que ele já tem dá 0. Guarde senhas com password_hash, nunca em texto puro.
Transações
Uma transação faz várias instruções darem certo ou falharem juntas. Se qualquer coisa lançar uma exceção, faça rollback e nada é salvo:
<?php
try {
$pdo->beginTransaction();
$pdo->prepare('UPDATE accounts SET balance = balance - ? WHERE id = ?')->execute([100, $from]);
$pdo->prepare('UPDATE accounts SET balance = balance + ? WHERE id = ?')->execute([100, $to]);
$pdo->prepare('INSERT INTO transfers (from_id, to_id, amount) VALUES (?, ?, ?)')->execute([$from, $to, 100]);
$pdo->commit();
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
Isso depende de os erros lançarem exceções (o padrão desde o PHP 8.0), para que uma instrução que falha pule para o catch. Com MySQL, as tabelas precisam usar InnoDB; o MyISAM ignora transações. A página sobre try/catch explica Throwable e como relançar.
WHERE IN com um array
Um único ? guarda um único valor, então IN (?) com um array não funciona. Monte um marcador por valor:
<?php
$ids = [3, 8, 15];
if ($ids) {
$placeholders = implode(',', array_fill(0, count($ids), '?')); // "?,?,?"
$stmt = $pdo->prepare("SELECT * FROM products WHERE id IN ($placeholders)");
$stmt->execute($ids);
$products = $stmt->fetchAll();
} else {
$products = []; // IN () would be a syntax error
}
A string interpolada no SQL tem só pontos de interrogação e vírgulas, então a consulta continua segura.
Erros comuns com marcadores
- Nomes de tabelas e colunas.
ORDER BY ?ordena por uma string constante, não pela coluna. Escolha identificadores de uma lista permitida:$sort = in_array($_GET['sort'] ?? '', ['name', 'price'], true) ? $_GET['sort'] : 'name';, e depois coloque$sortno SQL. LIMIT ?com prepares emulados. Com oATTR_EMULATE_PREPARESpadrão no MySQL,execute(['10'])enviaLIMIT '10', um erro de sintaxe. Faça o bind comPDO::PARAM_INTou desligue a emulação.- O mesmo marcador nomeado duas vezes.
WHERE first = :q OR last = :qsó funciona com prepares emulados. Com prepares reais você recebeSQLSTATE[HY093]: Invalid parameter number; use:q1e:q2. - Esquecer que o
fetch()retornafalse.$user['name']emfalseé um aviso enull; verifique o resultado antes.
Perguntas frequentes
Como conecto ao MySQL com PDO?
Crie um objeto PDO com um DSN, um usuário e uma senha: $pdo = new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', 'user', 'secret', [PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]);. Uma conexão que falha lança uma PDOException; capture-a, registre a mensagem em log e mostre ao visitante um erro genérico.
Prepared statements evitam SQL injection?
Sim, para valores: com prepare('... WHERE id = ?') e execute([$id]) o valor é enviado como dado e nunca consegue alterar a consulta. Marcadores não podem representar nomes de tabelas, nomes de colunas ou ASC/DESC, então compare esses com uma lista de nomes permitidos antes de colocá-los no SQL.
Qual a diferença entre fetch e fetchAll no PDO?
O fetch() retorna a próxima linha (ou false quando não há mais), então serve para uma única linha ou para um loop sobre um resultado grande. O fetchAll() retorna todas as linhas num array de uma vez. O fetchColumn() retorna um valor da próxima linha, prático para SELECT COUNT(*).
Como uso WHERE IN com um array no PDO?
Monte um ? por valor e passe o array para o execute(): $in = implode(',', array_fill(0, count($ids), '?')); $stmt = $pdo->prepare("SELECT * FROM users WHERE id IN ($in)"); $stmt->execute($ids);. Verifique antes se o array não está vazio, porque IN () é erro de sintaxe.
Devo usar PDO ou mysqli?
Use PDO, a menos que precise de um recurso exclusivo do MySQL. A API dele é a mesma para MySQL, PostgreSQL, SQLite e outros, aceita marcadores nomeados e, desde o PHP 8.0, lança exceções em erros por padrão. O mysqli só funciona com MySQL e MariaDB.