像生产环境那样配置 PDO,从结构上消除 SQL 注入,并用事务让你的写入操作变得可靠。
在 Kodokon 中打开本课PDO 是统一的数据库访问接口:MySQL、PostgreSQL 或 SQLite 都用同一套 API 来驱动。三项设置把一个幼稚的连接和一个生产级的连接区分开来:ERRMODE_EXCEPTION 让每一个 SQL 错误都抛出异常,而不是悄无声息地失败;默认使用 FETCH_ASSOC 以得到干净的结果;以及把 EMULATE_PREPARES 设为 false,让预处理语句真正由服务器执行。直接在 DSN 中加上 charset=utf8mb4 - 这是唯一可靠的声明它的地方。
<?php
declare(strict_types=1);
$dsn = 'mysql:host=localhost;dbname=shop'
. ';charset=utf8mb4';
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, 'app_user', 'secret', $options);预处理语句是你抵御 SQL 注入唯一从结构上有效的防线。原理如下:查询的结构,连同它的 :email 或 ? 占位符,会先被发送到服务器;数据则随后单独跟上,永远不会被当作 SQL 来解释。把一个变量拼接进查询里,哪怕它是经过"转义"的,仍然是一个专业级别的错误。命名占位符让代码更易读;对于简短的查询,位置占位符就足够了。
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION,
);
$pdo->exec(
'CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL
)'
);
$insert = $pdo->prepare(
'INSERT INTO users (email) VALUES (:email)'
);
$insert->execute(['email' => 'lea@example.com']);
$query = $pdo->prepare(
'SELECT id, email FROM users
WHERE email = :email'
);
$query->execute(['email' => 'lea@example.com']);
var_dump($query->fetch(PDO::FETCH_ASSOC));事务保证原子性:要么所有写入都成功,要么一个都不生效。经典的写法:beginTransaction(),把工作放在 try 里,在代码块末尾 commit(),在 catch 中 rollBack() - 然后你要重新抛出这个异常,因为隐藏失败比失败本身更糟糕。让事务保持简短:在一次网络调用或一段漫长处理过程中持有的每一把锁,都会拖累整个应用的并发能力。
<?php
declare(strict_types=1);
$pdo = new PDO('sqlite::memory:');
$pdo->setAttribute(
PDO::ATTR_ERRMODE,
PDO::ERRMODE_EXCEPTION,
);
$pdo->exec(
'CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
balance INTEGER NOT NULL
)'
);
$pdo->exec(
'INSERT INTO accounts (balance)
VALUES (100), (20)'
);
$pdo->beginTransaction();
try {
$debit = $pdo->prepare(
'UPDATE accounts
SET balance = balance - :n
WHERE id = :id'
);
$debit->execute(['n' => 50, 'id' => 1]);
$credit = $pdo->prepare(
'UPDATE accounts
SET balance = balance + :n
WHERE id = :id'
);
$credit->execute(['n' => 50, 'id' => 2]);
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
throw $e;
}
echo 'Transfer done';:param 占位符传入什么?catch 块里要做什么?commit() 保存已经成功的部分rollBack(),然后重新抛出异常