Dia 13 — PDO: PHP com MySQL: PHP e MySQL
O SQL dos dias anteriores era escrito para ser executado à mão no phpMyAdmin ou no terminal. A partir daqui ele passa a ser executado pelo próprio PHP, o que exige um caminho diferente para chegar no banco. Este dia introduz o `PDO`, a conexão com `DSN` e variável de ambiente, e o prepared statement, que é o que separa o SQL que o aluno digita do SQL que o banco executa.
PDO: conexão
A conexão com o banco pelo PHP passa por uma string que se chama DSN e por um objeto PDO. A DSN descreve onde está o banco; o construtor recebe a DSN, o usuário e a senha, e um quarto argumento com as opções de comportamento. A senha vem de getenv(), nunca escrita no arquivo.
Montando a conexão
new PDO(string $dsn, ?string $usuario, ?string $senha, array $opcoes = []): PDO
A DSN do MySQL segue o formato mysql:host=HOST;port=PORTA;dbname=BANCO;charset=utf8mb4. A porta só precisa aparecer quando não for a 3306.
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=escola;charset=utf8mb4'; try { $pdo = new PDO($dsn, 'app_user', getenv('DB_PASS') ?: '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); } catch (PDOException $e) { exit('Nao foi possivel conectar ao banco.'); }
As três opções que valem a pena
| Opção | Valor | O que muda sem ela |
|---|---|---|
PDO::ATTR_ERRMODE | PDO::ERRMODE_EXCEPTION | O erro vira false silencioso e o script continua com um resultado quebrado |
PDO::ATTR_DEFAULT_FETCH_MODE | PDO::FETCH_ASSOC | fetch() devolve array com chave numérica e coluna, em vez de ['nome' => 'Ana'] |
PDO::ATTR_EMULATE_PREPARES | false | O PHP monta o SQL por conta própria, e a consulta real vai para o banco como texto |
Onde a mensagem de erro pode vazar
A conexão que falha é o caso em que a exceção carrega mais do que deveria: a mensagem original costuma conter o host e o usuário do banco. error_log() grava o detalhe no log do servidor; a tela recebe uma frase genérica.
PDOException é a exceção que o PDO lança. Sem ERRMODE_EXCEPTION, ela nunca chega, e o catch não existe.
Para descobrir se a extensão do driver está instalada, sem precisar de servidor: PDO::getAvailableDrivers(): array. Se mysql não estiver na lista, o problema é a extensão pdo_mysql, não o código.
Exemplo
<?php declare(strict_types=1); // Esta aula monta a configuracao e a DSN de verdade (PHP puro, roda sempre). // A conexao em si aparece como texto: o gerador roda este arquivo numa imagem // php:8.3-cli que nao tem o driver pdo_mysql nem servidor MySQL. $config = [ 'host' => getenv('DB_HOST') ?: '127.0.0.1', 'port' => (int) (getenv('DB_PORT') ?: 3306), 'database' => getenv('DB_NAME') ?: 'escola', 'username' => getenv('DB_USER') ?: 'app_user', 'password' => getenv('DB_PASS') ?: '', 'charset' => 'utf8mb4', ]; echo "=== 1. Configuracao vinda do ambiente ===\n"; foreach ($config as $chave => $valor) { printf(" %-9s %s\n", $chave, $chave === 'password' ? '(de getenv, nunca no codigo)' : $valor); } // A DSN e uma string: describe o banco para o driver. Nao e segredo e pode // aparecer em log de conexao. $dsn = sprintf( 'mysql:host=%s;port=%d;dbname=%s;charset=%s', $config['host'], $config['port'], $config['database'], $config['charset'], ); echo "\n=== 2. A DSN montada ===\n{$dsn}\n"; // --- 3. A conexao real, exibida como texto --- $codigo = <<<'PHP' <?php declare(strict_types=1); $senha = getenv('DB_PASS'); $dsn = 'mysql:host=127.0.0.1;port=3306;dbname=escola;charset=utf8mb4'; try { $pdo = new PDO($dsn, 'app_user', $senha ?: '', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, ]); echo "conectado\n"; } catch (PDOException $e) { // A mensagem original carrega host e usuario do banco. Ela vai para o log // do servidor, nunca para a tela de quem esta usando o site. error_log('Falha de conexao: ' . $e->getMessage()); exit('Nao foi possivel conectar ao banco.' . PHP_EOL); } PHP; echo "\n=== 3. A conexao real ===\n{$codigo}\n"; // --- 4. As opcoes do quarto argumento --- $opcoes = [ 'PDO::ATTR_ERRMODE' => 'PDO::ERRMODE_EXCEPTION', 'PDO::ATTR_DEFAULT_FETCH_MODE' => 'PDO::FETCH_ASSOC', 'PDO::ATTR_EMULATE_PREPARES' => 'false', ]; echo "\n=== 4. Opcoes passadas na construcao ===\n"; foreach ($opcoes as $constante => $valor) { printf(" %-30s => %s\n", $constante, $valor); } // --- 5. O que esta build do PHP tem compilado --- // getAvailableDrivers() responde sem precisar de servidor: e o jeito de // descobrir se a extensao pdo_mysql esta instalada. echo "\n=== 5. Drivers desta build do PHP ===\n"; printf(" %s\n", implode(', ', PDO::getAvailableDrivers())); echo " (numa instalacao com MySQL, 'mysql' aparece na lista)\n";
Saída real
=== 1. Configuracao vinda do ambiente ===
host 127.0.0.1
port 3306
database escola
username app_user
password (de getenv, nunca no codigo)
charset utf8mb4
=== 2. A DSN montada ===
mysql:host=127.0.0.1;port=3306;dbname=escola;charset=utf8mb4
=== 3. A conexao real ===
<?php
declare(strict_types=1);
$senha = getenv('DB_PASS');
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=escola;charset=utf8mb4';
try {
$pdo = new PDO($dsn, 'app_user', $senha ?: '', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
echo "conectado\n";
} catch (PDOException $e) {
// A mensagem original carrega host e usuario do banco. Ela vai para o log
// do servidor, nunca para a tela de quem esta usando o site.
error_log('Falha de conexao: ' . $e->getMessage());
exit('Nao foi possivel conectar ao banco.' . PHP_EOL);
}
=== 4. Opcoes passadas na construcao ===
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
PDO::ATTR_EMULATE_PREPARES => false
=== 5. Drivers desta build do PHP ===
sqlite
(numa instalacao com MySQL, 'mysql' aparece na lista)
Prepared statement
Comando preparado é o SQL com a variável separada do valor. O PHP monta o comando uma vez e manda o valor por fora, então o banco nunca interpreta o que o usuário digitou como sintaxe de SQL. Todo acesso a dado que venha de formulário passa por prepare().
As três formas de enviar o valor
prepare(string $sql): PDOStatement cria o comando. execute(array $pares = []): bool envia os valores. bindValue(string|int $parametro, mixed $valor, int $tipo = PDO::PARAM_STR): bool copia o valor no momento da chamada. bindParam(string|int $parametro, mixed &$variavel, int $tipo = PDO::PARAM_STR): bool guarda a referência e lê a variável no momento do execute().
// placeholder nomeado: o mesmo SQL para qualquer valor $stmt = $pdo->prepare('SELECT id, nome FROM alunos WHERE nome = :nome'); $stmt->execute([':nome' => $nome]); // placeholder posicional: cada ? na ordem dos valores $stmt = $pdo->prepare('INSERT INTO alunos (nome, email) VALUES (?, ?)'); $stmt->execute([$nome, $email]); // bindParam, para lote: a variavel e lida a cada execute $stmt = $pdo->prepare('INSERT INTO alunos (nome) VALUES (:nome)'); foreach ($nomes as $nome) { $stmt->bindParam(':nome', $nome); $stmt->execute(); }
Por que concatenar valor é SQL Injection
Concatenar monta o SQL com texto e número misturados, e o valor do usuário entra no mesmo lugar onde o PHP esperava escrever código. Quando esse valor contém uma aspa simples, a aspa fecha o trecho de texto do SQL e o que vem depois é lido como comando.
O que a pessoa digita no campo de busca: Ana'; DROP TABLE alunos; --
O SQL que chega ao banco, montado por concatenação:
SELECT id, nome FROM alunos WHERE nome = 'Ana'; DROP TABLE alunos; --'
O mesmo valor, com placeholder, vira só o texto Ana'; DROP TABLE alunos; -- dentro da coluna nome. O DROP nunca é interpretado, porque o driver enviou comando e valor separados.
Não existe sanitização manual que feche esse caminho: addslashes() quebra com barra invertida, e trocar a aspa por nada altera o dado. Não existe caso em que concatenar valor de usuário no SQL seja aceitável.
O que EMULATE_PREPARES => false protege de verdade
A emulação faz o PHP montar o SQL inteiro e o mandar como texto comum. Nesse caminho, o driver do lado do PHP continua sendo quem escapa o valor, e é aí que moram bugs de escape e de codificação.
Com false, o comando vai ao servidor com os valores fora dele, e quem interpreta é o MySQL. O ponto não é o prepare() sozinho: é o prepare() com o valor indo por fora.
Uma consequência prática: com emulação desligada, LIMIT e OFFSET-placeholder precisam de tipo declarado, senão o MySQL recusa.
$stmt = $pdo->prepare('SELECT id FROM alunos ORDER BY nome LIMIT :limite OFFSET :offset'); $stmt->bindValue(':limite', 10, PDO::PARAM_INT); $stmt->bindValue(':offset', 20, PDO::PARAM_INT); $stmt->execute();
Exemplo
<?php declare(strict_types=1); // Ataque e defesa de SQL Injection. As consultas aparecem como texto porque a // imagem php:8.3-cli do gerador nao tem driver pdo_mysql. O que roda de // verdade aqui e a comparacao entre as duas formas de montar o comando. $busca = "Ana'; DROP TABLE alunos; --"; echo "=== 1. Jeito que abre brecha: o valor entra no texto do SQL ===\n"; echo "valor digitado pelo usuario: {$busca}\n\n"; echo 'SQL que chega ao banco:' . "\n"; echo " SELECT id, nome FROM alunos WHERE nome = '{$busca}'\n\n"; echo "O ponto e o apostrofo: ele fecha a string, o resto vira comando.\n"; // Uma string com aspa simples e barra invertida e o que a pessoa consegue // escrever. Isso mostra que validar na mao e o caminho para a falha. $comBarra = "\\"; echo "\nCom uma barra invertida, o proprio escape fica com um caracter a menos:\n"; echo " nome = '{$comBarra}' -> a aspa seguinte fecha a string\n"; // --- 2. O mesmo SQL com placeholder --- $sqlSeguro = 'SELECT id, nome FROM alunos WHERE nome = :nome'; echo "\n=== 2. Jeito correto: o valor nao entra no texto do SQL ===\n"; echo "SQL, sempre identico: {$sqlSeguro}\n"; echo "valor enviado a parte: {$busca}\n\n"; echo "O driver manda o comando e o valor separados. O MySQL ve\n"; echo "'{$busca}' como um texto so, nunca como sintaxe.\n"; // --- 3. As duas formas de enviar o valor --- echo "\n=== 3. execute com array (a mais comum) ===\n"; $parametros = [':nome' => $busca]; foreach ($parametros as $chave => $valor) { printf(" %-6s <= %s\n", $chave, $valor); } echo "\n=== 4. bindValue e bindParam ===\n"; // bindValue copia o valor no momento da chamada. // bindParam guarda a referencia: se a variavel mudar depois, o valor enviado // e o novo. E o que se usa com loop de INSERT em lote. $valor = 'Ana Souza'; printf(" bindValue copia: %s\n", $valor); $valor = 'Bruno Lima'; printf(" bindParam le junto: %s\n", $valor); // --- 5. Um trecho real de prepared statement --- $codigo = <<<'PHP' <?php declare(strict_types=1); // Com placeholder nomeado: o mesmo SQL para qualquer valor. $stmt = $pdo->prepare('SELECT id, nome FROM alunos WHERE nome = :nome'); $stmt->execute([':nome' => $busca]); $alunos = $stmt->fetchAll(); // Com placeholder posicional: cada ? na ordem em que os valores aparecem. $stmt = $pdo->prepare('INSERT INTO alunos (nome, email) VALUES (?, ?)'); $stmt->execute([$nome, $email]); // bindParam, util em lote: a variavel e lida no momento do execute. $stmt = $pdo->prepare('INSERT INTO alunos (nome) VALUES (:nome)'); foreach ($nomes as $nome) { $stmt->bindParam(':nome', $nome); $stmt->execute(); } // Limite e deslocamento sao inteiros: declarados como PARAM_INT, nunca string. $stmt = $pdo->prepare('SELECT id, nome FROM alunos ORDER BY nome LIMIT :limite OFFSET :offset'); $stmt->bindValue(':limite', 10, PDO::PARAM_INT); $stmt->bindValue(':offset', 20, PDO::PARAM_INT); $stmt->execute(); PHP; echo "\n=== 5. Prepared statement na pratica ===\n{$codigo}\n";
Saída real
=== 1. Jeito que abre brecha: o valor entra no texto do SQL ===
valor digitado pelo usuario: Ana'; DROP TABLE alunos; --
SQL que chega ao banco:
SELECT id, nome FROM alunos WHERE nome = 'Ana'; DROP TABLE alunos; --'
O ponto e o apostrofo: ele fecha a string, o resto vira comando.
Com uma barra invertida, o proprio escape fica com um caracter a menos:
nome = '\' -> a aspa seguinte fecha a string
=== 2. Jeito correto: o valor nao entra no texto do SQL ===
SQL, sempre identico: SELECT id, nome FROM alunos WHERE nome = :nome
valor enviado a parte: Ana'; DROP TABLE alunos; --
O driver manda o comando e o valor separados. O MySQL ve
'Ana'; DROP TABLE alunos; --' como um texto so, nunca como sintaxe.
=== 3. execute com array (a mais comum) ===
:nome <= Ana'; DROP TABLE alunos; --
=== 4. bindValue e bindParam ===
bindValue copia: Ana Souza
bindParam le junto: Bruno Lima
=== 5. Prepared statement na pratica ===
<?php
declare(strict_types=1);
// Com placeholder nomeado: o mesmo SQL para qualquer valor.
$stmt = $pdo->prepare('SELECT id, nome FROM alunos WHERE nome = :nome');
$stmt->execute([':nome' => $busca]);
$alunos = $stmt->fetchAll();
// Com placeholder posicional: cada ? na ordem em que os valores aparecem.
$stmt = $pdo->prepare('INSERT INTO alunos (nome, email) VALUES (?, ?)');
$stmt->execute([$nome, $email]);
// bindParam, util em lote: a variavel e lida no momento do execute.
$stmt = $pdo->prepare('INSERT INTO alunos (nome) VALUES (:nome)');
foreach ($nomes as $nome) {
$stmt->bindParam(':nome', $nome);
$stmt->execute();
}
// Limite e deslocamento sao inteiros: declarados como PARAM_INT, nunca string.
$stmt = $pdo->prepare('SELECT id, nome FROM alunos ORDER BY nome LIMIT :limite OFFSET :offset');
$stmt->bindValue(':limite', 10, PDO::PARAM_INT);
$stmt->bindValue(':offset', 20, PDO::PARAM_INT);
$stmt->execute();
CRUD completo
CRUD são as quatro operações de um cadastro: criar, ler, atualizar e apagar. Em SQL são INSERT, SELECT, UPDATE e DELETE, e no PDO as quatro seguem o mesmo par: prepare() seguido de execute().
As quatro operações
// CREATE $stmt = $pdo->prepare('INSERT INTO alunos (nome, email, nota) VALUES (:nome, :email, :nota)'); $stmt->execute([':nome' => $nome, ':email' => $email, ':nota' => $nota]); $novoId = (int) $pdo->lastInsertId(); // READ $stmt = $pdo->prepare('SELECT id, nome, nota FROM alunos WHERE nota >= :minima ORDER BY nota DESC'); $stmt->execute([':minima' => 7.0]); $alunos = $stmt->fetchAll(); // UPDATE $stmt = $pdo->prepare('UPDATE alunos SET nota = :nota WHERE id = :id'); $stmt->execute([':nota' => 7.75, ':id' => $novoId]); // DELETE $stmt = $pdo->prepare('DELETE FROM alunos WHERE id = :id'); $stmt->execute([':id' => $novoId]);
lastInsertId() e rowCount()
lastInsertId(): string devolve o AUTO_INCREMENT da última inserção, como texto. Serve para redirecionar para o registro que acabou de ser criado. Depois de um UPDATE ou DELETE, devolve 0.
rowCount(): int devolve as linhas afetadas por INSERT, UPDATE e DELETE. Em SELECT não serve: quem conta resultado de leitura é o fetch ou o fetchAll.
| Verificação | Como fica |
|---|---|
| Inseriu mesmo? | $stmt->rowCount() === 1 |
| Qual o id novo? | (int) $pdo->lastInsertId() |
| Alterou alguma linha? | $stmt->rowCount() > 0 |
| Apagou algo? | $stmt->rowCount() === 1 |
O caso que quebra: DELETE sem WHERE
UPDATE e DELETE sem WHERE valem para a tabela inteira. Não há aviso e não há erro: o banco executa e apaga tudo, e o rowCount() confirma com um número grande demais. Em produção, a primeira defesa é sempre o WHERE com placeholder, e a segunda é conferir o rowCount().
O mesmo vale para UPDATE sem WHERE: a coluna nova é gravada em todas as linhas. Manter o WHERE sempre visível na tela do editor evita a maior parte dos acidentes desses dois.
Para operações que mexem em mais de uma tabela, cada operação vai dentro de uma transação, senão o cadastro fica pela metade quando a terceira etapa falha.
Exemplo
<?php declare(strict_types=1); // CRUD completo: as quatro operacoes com prepared statement. // // O gerador roda este arquivo numa imagem php:8.3-cli que nao tem pdo_mysql // nem servidor MySQL, entao o exemplo roda em SQLite (sqlite: fica na // memoria). A API do PDO e identica nos dois: prepare, execute, rowCount e // lastInsertId se comportam do mesmo jeito. O que muda entre os dois e apenas // o CREATE TABLE, que aparece abaixo no SQL de MySQL. echo "=== 1. Estrutura da tabela (SQL de MySQL) ===\n"; $sql = <<<'SQL' CREATE TABLE alunos ( id INT AUTO_INCREMENT PRIMARY KEY, nome VARCHAR(120) NOT NULL, email VARCHAR(160) NOT NULL UNIQUE, nota DECIMAL(4,2) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 SQL; echo $sql . "\n"; $pdo = new PDO('sqlite::memory:', null, null, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); $pdo->exec('CREATE TABLE alunos (id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT NOT NULL, email TEXT NOT NULL UNIQUE, nota REAL)'); // --- CREATE --- echo "\n=== 2. CREATE: INSERT ===\n"; $novos = [ ['Ana Souza', '[email protected]', 8.50], ['Bruno Lima', '[email protected]', 6.00], ['Carla Dias', '[email protected]', 9.25], ]; $sql = 'INSERT INTO alunos (nome, email, nota) VALUES (:nome, :email, :nota)'; $stmt = $pdo->prepare($sql); $inseridas = 0; foreach ($novos as [$nome, $email, $nota]) { $stmt->execute([':nome' => $nome, ':email' => $email, ':nota' => $nota]); printf(" inserido %-14s id %d (esta insercao alterou %d linha)\n", $nome, (int) $pdo->lastInsertId(), $stmt->rowCount()); $inseridas += $stmt->rowCount(); } printf(" %d linhas inseridas no total\n", $inseridas); // --- READ --- echo "\n=== 3. READ: SELECT com filtro ===\n"; $sql = 'SELECT id, nome, nota FROM alunos WHERE nota >= :minima ORDER BY nota DESC'; $stmt = $pdo->prepare($sql); $stmt->execute([':minima' => 7.0]); foreach ($stmt->fetchAll() as $aluno) { printf(" #%d %-14s %5.2f\n", $aluno['id'], $aluno['nome'], $aluno['nota']); } // --- UPDATE --- echo "\n=== 4. UPDATE: SET com WHERE ===\n"; $sql = 'UPDATE alunos SET nota = :nota WHERE id = :id'; $stmt = $pdo->prepare($sql); $stmt->execute([':nota' => 7.75, ':id' => 2]); printf(" linhas alteradas: %d\n", $stmt->rowCount()); // --- DELETE --- echo "\n=== 5. DELETE com WHERE ===\n"; $sql = 'DELETE FROM alunos WHERE id = :id'; $stmt = $pdo->prepare($sql); $stmt->execute([':id' => 3]); printf(" linhas removidas: %d\n", $stmt->rowCount()); // --- O DELETE sem WHERE apaga a tabela inteira --- echo "\n=== 6. Por que o WHERE e obrigatorio ===\n"; $pdo->exec('CREATE TABLE backup_alunos AS SELECT * FROM alunos'); $antes = (int) $pdo->query('SELECT COUNT(*) FROM backup_alunos')->fetchColumn(); $stmt = $pdo->prepare('DELETE FROM backup_alunos'); // esqueceu o WHERE $stmt->execute(); $depois = (int) $pdo->query('SELECT COUNT(*) FROM backup_alunos')->fetchColumn(); printf(" linhas antes do DELETE sem WHERE: %d\n", $antes); printf(" linhas depois: %d\n", $depois); // --- Chave que nao existe --- echo "\n=== 7. Efeito colateral de nao conferir o rowCount ===\n"; $stmt = $pdo->prepare('DELETE FROM alunos WHERE id = :id'); $stmt->execute([':id' => 999]); printf(" DELETE do id 999: %d linhas (nenhum aluno tem esse id)\n", $stmt->rowCount()); echo "\nAlunos que sobraram:\n"; foreach ($pdo->query('SELECT id, nome, nota FROM alunos ORDER BY id') as $aluno) { printf(" #%d %-14s %5.2f\n", $aluno['id'], $aluno['nome'], $aluno['nota']); }
Saída real
=== 1. Estrutura da tabela (SQL de MySQL) ===
CREATE TABLE alunos (
id INT AUTO_INCREMENT PRIMARY KEY,
nome VARCHAR(120) NOT NULL,
email VARCHAR(160) NOT NULL UNIQUE,
nota DECIMAL(4,2) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
=== 2. CREATE: INSERT ===
inserido Ana Souza id 1 (esta insercao alterou 1 linha)
inserido Bruno Lima id 2 (esta insercao alterou 1 linha)
inserido Carla Dias id 3 (esta insercao alterou 1 linha)
3 linhas inseridas no total
=== 3. READ: SELECT com filtro ===
#3 Carla Dias 9.25
#1 Ana Souza 8.50
=== 4. UPDATE: SET com WHERE ===
linhas alteradas: 1
=== 5. DELETE com WHERE ===
linhas removidas: 1
=== 6. Por que o WHERE e obrigatorio ===
linhas antes do DELETE sem WHERE: 2
linhas depois: 0
=== 7. Efeito colateral de nao conferir o rowCount ===
DELETE do id 999: 0 linhas (nenhum aluno tem esse id)
Alunos que sobraram:
#1 Ana Souza 8.50
#2 Bruno Lima 7.75
Lendo resultados
Depois do execute(), o PDOStatement vira um cursor sobre o resultado. A escolha é entre puxar uma linha por vez ou trazer o array inteiro, e entre receber array ou objeto.
Os métodos de leitura
| Método | Devolve | Uso |
|---|---|---|
fetch(?int $modo = null): mixed | a próxima linha, ou false no fim | while (($linha = $stmt->fetch()) !== false) |
fetchAll(?int $modo = null): array | todas as linhas, de uma vez | listagem de tamanho conhecido |
fetchColumn(int $indice = 0): mixed | um valor só da próxima linha | COUNT(*), checagem de existência |
fetchObject(string $classe = 'stdClass', array $construtor = []): object | a próxima linha como objeto | resultado lido por atributo |
$stmt = $pdo->prepare('SELECT id, nome FROM alunos ORDER BY nome'); $stmt->execute(); // uma linha por vez while (($linha = $stmt->fetch()) !== false) { echo $linha['nome'], "\n"; } // o array inteiro $alunos = $stmt->fetchAll(); // um valor só $stmt = $pdo->prepare('SELECT COUNT(*) FROM alunos WHERE turno = :turno'); $stmt->execute([':turno' => 'noturno']); $total = (int) $stmt->fetchColumn();
O fetchColumn() anda com o cursor: chamado em sequência, devolve o valor da coluna pedida de cada linha. Com COUNT(*) numa consulta, devolve o total em uma chamada.
fetchAll() com dois argumentos
fetchAll(int $modo): array aceita um modo que muda o formato do array, sem passar por atributo.
| Modo | Formato do array |
|---|---|
PDO::FETCH_ASSOC | ['id' => 1, 'nome' => 'Ana'] |
PDO::FETCH_NUM | [0 => 1, 1 => 'Ana'] |
PDO::FETCH_OBJ | objeto com atributo ->id e ->nome |
PDO::FETCH_KEY_PAIR | id => nome, quando o SELECT tem duas colunas |
PDO::FETCH_COLUMN | só a coluna pedida, como lista de valores |
PDO::FETCH_KEY_PAIR resolve o array associativo de duas colunas sem laço: SELECT id, nome vira [1 => 'Ana', 2 => 'Bruno'].
O que o fetch devolve no fim
fetch() devolve false quando não há mais linha. Com while ($linha = $stmt->fetch()), uma linha vazia ou com valor 0 interrompe o laço antes da hora. O while (($linha = $stmt->fetch()) !== false) evita isso.
O fetchAll() carrega o resultado inteiro na memória. Em tabela grande, o laço com fetch() percorre sem nada acumular, e é a diferença entre uma listagem de mil linhas e uma de cem mil.
Exemplo
<?php declare(strict_types=1); // Os jeitos de ler o resultado de uma consulta: fetch, fetchAll, fetchColumn, // fetchObject e fetchAll com chave. // // Roda em SQLite (sqlite:) porque a imagem do gerador nao tem pdo_mysql. A // API de leitura do PDO e a mesma para MySQL, entao o que aparece aqui e // exatamente o que o aluno vera no MySQL. $pdo = new PDO('sqlite::memory:', null, null, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); $pdo->exec('CREATE TABLE cursos (id INTEGER PRIMARY KEY AUTOINCREMENT, nome TEXT, turno TEXT, vagas INTEGER)'); $stmt = $pdo->prepare('INSERT INTO cursos (nome, turno, vagas) VALUES (:nome, :turno, :vagas)'); foreach ([ ['Eletronica', 'matutino', 40], ['Informatica', 'vespertino', 35], ['Mecanica', 'noturno', 30], ['Quimica', 'matutino', 25], ] as [$nome, $turno, $vagas]) { $stmt->execute([':nome' => $nome, ':turno' => $turno, ':vagas' => $vagas]); } $sql = 'SELECT id, nome, turno, vagas FROM cursos ORDER BY nome'; echo "=== 1. fetch(): uma linha por vez, cursor fica no Statement ===\n"; $stmt = $pdo->prepare($sql); $stmt->execute(); while (($curso = $stmt->fetch()) !== false) { printf(" %-12s %-11s %d vagas\n", $curso['nome'], $curso['turno'], $curso['vagas']); } echo "\n=== 2. fetchAll(): o array inteiro, de uma vez ===\n"; $stmt = $pdo->prepare($sql); $stmt->execute(); $cursos = $stmt->fetchAll(); printf(" %d registros, %d colunas em cada um\n", count($cursos), count($cursos[0])); printf(" primeira linha: %s -> id %d\n", $cursos[0]['nome'], $cursos[0]['id']); echo "\n=== 3. fetchColumn(): um unico valor, nao a linha ===\n"; $stmt = $pdo->prepare($sql); $stmt->execute(); printf(" fetchColumn() com indice 1: %s\n", $stmt->fetchColumn(1)); $stmt = $pdo->prepare('SELECT COUNT(*) FROM cursos WHERE turno = :turno'); $stmt->execute([':turno' => 'matutino']); printf(" COUNT(*) do turno matutino: %d\n", (int) $stmt->fetchColumn()); echo "\n=== 4. fetchColumn() andando com o indice do loop ===\n"; $stmt = $pdo->prepare($sql); $stmt->execute(); $nomes = []; while (($nome = $stmt->fetchColumn(1)) !== false) { $nomes[] = $nome; } printf(" %s\n", implode(', ', $nomes)); echo "\n=== 5. fetchObject(): a linha vira um objeto ===\n"; $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); $stmt = $pdo->prepare($sql); $stmt->execute(); $curso = $stmt->fetch(); printf(" %s tem %d vagas no turno %s\n", $curso->nome, $curso->vagas, $curso->turno); echo "\n=== 6. fetchAll(PDO::FETCH_KEY_PAIR): duas colunas viram chave e valor ===\n"; $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $stmt = $pdo->prepare('SELECT id, nome FROM cursos ORDER BY nome'); $stmt->execute(); $porId = $stmt->fetchAll(PDO::FETCH_KEY_PAIR); foreach ($porId as $id => $nome) { printf(" %d => %s\n", $id, $nome); } echo "\n=== 7. rowCount() e o numero de linhas afetadas ===\n"; $stmt = $pdo->prepare('UPDATE cursos SET vagas = vagas - 1 WHERE turno = :turno'); $stmt->execute([':turno' => 'matutino']); printf(" UPDATE: %d linha(s) alterada(s)\n", $stmt->rowCount()); $stmt = $pdo->prepare('SELECT id FROM cursos'); $stmt->execute(); printf(" SELECT devolveu %d linha(s) (aqui o certo e contar o fetch)\n", count($stmt->fetchAll())); echo "\n=== 8. Memoria: fetchAll em tabela grande vs while com fetch ===\n"; $stmt = $pdo->prepare('SELECT nome FROM cursos ORDER BY nome'); $stmt->execute(); $contador = 0; while ($stmt->fetch() !== false) { // uma linha por vez: o array nunca cresce $contador++; } printf(" percorridas %d linhas sem guardar o resultado inteiro\n", $contador);
Saída real
=== 1. fetch(): uma linha por vez, cursor fica no Statement === Eletronica matutino 40 vagas Informatica vespertino 35 vagas Mecanica noturno 30 vagas Quimica matutino 25 vagas === 2. fetchAll(): o array inteiro, de uma vez === 4 registros, 4 colunas em cada um primeira linha: Eletronica -> id 1 === 3. fetchColumn(): um unico valor, nao a linha === fetchColumn() com indice 1: Eletronica COUNT(*) do turno matutino: 2 === 4. fetchColumn() andando com o indice do loop === Eletronica, Informatica, Mecanica, Quimica === 5. fetchObject(): a linha vira um objeto === Eletronica tem 40 vagas no turno matutino === 6. fetchAll(PDO::FETCH_KEY_PAIR): duas colunas viram chave e valor === 1 => Eletronica 2 => Informatica 3 => Mecanica 4 => Quimica === 7. rowCount() e o numero de linhas afetadas === UPDATE: 2 linha(s) alterada(s) SELECT devolveu 4 linha(s) (aqui o certo e contar o fetch) === 8. Memoria: fetchAll em tabela grande vs while com fetch === percorridas 4 linhas sem guardar o resultado inteiro