本番と同じようにPDOを設定し、SQLインジェクションを構造的に排除し、トランザクションで書き込みを信頼できるものにしましょう。
このレッスンを Kodokon で開くPDOは、データベースへのアクセスを統一するインターフェースです。MySQLでも、PostgreSQLでも、SQLiteでも、同じAPIで操作します。三つの設定が、素朴な接続と本番の接続を分けます。ERRMODE_EXCEPTIONはすべてのSQLエラーを、黙って失敗させるのではなく例外として投げさせます。FETCH_ASSOCをデフォルトにすればきれいな結果が得られます。そしてEMULATE_PREPARESをfalseにすれば、サーバーが実際に実行するプリペアドステートメントになります。charset=utf8mb4はDSNに直接加えましょう。それを宣言できる唯一の信頼できる場所です。
<?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()し、そのあと例外を再スローする