ตั้งค่า PDO แบบเดียวกับที่ใช้งานจริง กำจัด SQL injection ในเชิงโครงสร้าง และทำให้การเขียนข้อมูลของคุณเชื่อถือได้ด้วยทรานแซกชัน
เปิดบทเรียนนี้ใน KodokonPDO คืออินเทอร์เฟซการเข้าถึงฐานข้อมูลแบบรวมศูนย์: ไม่ว่าจะเป็น MySQL, PostgreSQL หรือ SQLite ล้วนขับเคลื่อนด้วย API เดียวกัน มีการตั้งค่าสามอย่างที่แยกการเชื่อมต่อแบบมือใหม่ออกจากการเชื่อมต่อแบบใช้งานจริง ได้แก่ ERRMODE_EXCEPTION เพื่อให้ข้อผิดพลาด SQL ทุกครั้งโยน exception ออกมาแทนที่จะล้มเหลวอย่างเงียบ ๆ, FETCH_ASSOC เป็นค่าเริ่มต้นเพื่อผลลัพธ์ที่สะอาด และ EMULATE_PREPARES ที่ตั้งเป็น false เพื่อให้เซิร์ฟเวอร์ประมวลผล prepared statement จริง ๆ จงเพิ่ม 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);prepared statement คือการป้องกัน SQL injection เชิงโครงสร้างเพียงอย่างเดียวของคุณ หลักการคือ โครงสร้าง ของคำสั่ง พร้อมกับตัวยึดตำแหน่ง :email หรือ ? จะถูกส่งไปยังเซิร์ฟเวอร์ก่อน ส่วน ข้อมูล จะตามมาแยกต่างหากและจะไม่ถูกตีความว่าเป็น SQL เลย การนำตัวแปรมาต่อสายอักขระเข้ากับคำสั่ง แม้จะเป็นตัวที่ "หลีกอักขระ (escape)" แล้ว ก็ยังคงเป็นความผิดพลาดระดับมืออาชีพ ตัวยึดตำแหน่งแบบมีชื่อทำให้โค้ดอ่านง่าย ส่วนแบบระบุตำแหน่งก็เพียงพอสำหรับคำสั่งสั้น ๆ
<?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));ทรานแซกชันรับประกันความเป็นอะตอม (atomicity): การเขียนทั้งหมดสำเร็จ หรือไม่มีอะไรสำเร็จเลย แพตเทิร์นมาตรฐานคือ beginTransaction(), ทำงานภายใน try, commit() ที่ท้ายบล็อก, rollBack() ใน catch จากนั้นคุณ โยน exception ซ้ำ (rethrow) เพราะการซ่อนความล้มเหลวไว้ย่อมเลวร้ายกว่าตัวความล้มเหลวเสียเอง จงทำให้ทรานแซกชันสั้นเข้าไว้ ทุกล็อกที่ถือครองไว้ระหว่างการเรียกผ่านเครือข่ายหรือกระบวนการที่ยาวนานจะบั่นทอนการทำงานพร้อมกัน (concurrency) ทั่วทั้งแอปพลิเคชัน
<?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 ใน prepared statement ได้บ้าง?catch?commit() เพื่อบันทึกส่วนที่สำเร็จrollBack() แล้วโยน exception ซ้ำ