Menu

PHP PDO: conectar ao MySQL e usar prepared statements

PDO é a camada de banco de dados do PHP: conecte com new PDO('mysql:host=localhost;dbname=shop;charset=utf8mb4', $user, $pass) e depois execute toda consulta com valores por prepare() e execute([$value]). Veja conexão, prepared statements, leitura de linhas, inserts, transações e WHERE IN.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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=utf8mb4 vai 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 $sort no SQL.
  • LIMIT ? com prepares emulados. Com o ATTR_EMULATE_PREPARES padrão no MySQL, execute(['10']) envia LIMIT '10', um erro de sintaxe. Faça o bind com PDO::PARAM_INT ou desligue a emulação.
  • O mesmo marcador nomeado duas vezes. WHERE first = :q OR last = :q só funciona com prepares emulados. Com prepares reais você recebe SQLSTATE[HY093]: Invalid parameter number; use :q1 e :q2.
  • Esquecer que o fetch() retorna false. $user['name'] em false é um aviso e null; 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.

Ilustração das linguagens de programação do Coddy

Aprenda a programar com o Coddy

COMEÇAR