PDO(PHP Data Objects)は,PHPからデータベースを操作するための標準的なクラスライブラリです。MySQL専用のmysqliとは異なり,PDOはMySQL,PostgreSQL,SQLiteなど複数のデータベースに対して,ほぼ同じ書き方で接続・問い合わせを行えるように設計されています。
本教材では第4章でmysqliを使ってPHPとMySQLの接続を説明してきましたが,第5章のWebアプリケーション制作では,SQLインジェクション対策を組み込みやすいPDOのprepare,execute,プレースホルダを使う形に統一します。
PDOでは,接続先データベースの種類,ホスト名,データベース名,文字コードをDSN(Data Source Name)として指定します。MySQLのtest_dbに接続する最小例は次の通りです。
<?php
$dsn = 'mysql:host=localhost;dbname=test_db;charset=utf8mb4';
$user = 'root';
$password = '';
try {
$dbh = new PDO($dsn, $user, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException $e) {
exit('データベース接続に失敗しました。');
}
?>
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTIONを指定しておくと,SQL文や接続に問題があった場合に例外として検出できます。PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOCは,検索結果を連想配列として取り出すための指定です。
PDOを使う大きな利点の一つは,SQL文に外部から入力された値を直接連結せず,プレースホルダを使って安全に渡せる点です。例えば,フォームから受け取った$idを使って検索する場合は,次のように書きます。
<?php
$sql = 'SELECT * FROM animal WHERE id = :id';
$stmt = $dbh->prepare($sql);
$stmt->execute([':id' => $id]);
$animal = $stmt->fetch();
?>
:idがプレースホルダです。SQL文の中に変数を直接埋め込まないため,SQLインジェクションを避けやすくなります。
データを追加するときも同じく,prepareとexecuteを使います。
<?php
$sql = 'INSERT INTO animal (id, name, size)
VALUES (:id, :name, :size)';
$stmt = $dbh->prepare($sql);
$stmt->execute([
':id' => 4,
':name' => 'クジラ',
':size' => 2000,
]);
?>
複数行の検索結果を取り出す場合は,fetchAllまたはwhile文とfetchを使います。
<?php
$sql = 'SELECT * FROM animal ORDER BY id ASC';
$stmt = $dbh->query($sql);
$animals = $stmt->fetchAll();
foreach ($animals as $animal) {
echo htmlspecialchars($animal['name'], ENT_QUOTES, 'UTF-8');
echo '<br>';
}
?>
SQL文に外部入力が含まれない単純な検索ではqueryも使えます。一方,フォーム入力やURLパラメータなど外部から受け取った値を使う場合は,原則としてprepareとexecuteを使います。
更新や削除では,対象レコードを指定するidなどに外部入力が入ることが多いため,必ずプレースホルダを使うようにします。
<?php
// 更新
$sql = 'UPDATE animal SET name = :name, size = :size WHERE id = :id';
$stmt = $dbh->prepare($sql);
$stmt->execute([
':name' => 'シロナガスクジラ',
':size' => 2500,
':id' => 4,
]);
// 削除
$sql = 'DELETE FROM animal WHERE id = :id';
$stmt = $dbh->prepare($sql);
$stmt->execute([':id' => 4]);
?>
ここまでに作成したCRUDシステムは,従来の教材の流れではmysqliを使っています。PDO版では,SQL文にフォームの値を直接連結せず,prepare()とexecute()で値を渡します。以下に,PDO版CRUDシステムの完成例を示します。
minicrud_pdo/
index.html
dbconnect.php
select.php
insert.php
select_delete.php
delete.php
select_update.php
update.php
create_animal.sql
README.txt
dbconnect.phpに集約します。:nameや:idのようなプレースホルダで渡します。h()でHTMLエスケープします。filter_input()で整数として検証します。<?php
// Chapter4 CRUDシステム PDO版 共通接続ファイル
// 環境に合わせて,データベース名・ユーザー名・パスワードを変更してください。
$dsn = 'mysql:host=localhost;dbname=sample;charset=utf8mb4';
$user = 'root';
$password = '';
try {
$dbh = new PDO($dsn, $user, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
]);
} catch (PDOException $e) {
exit('データベース接続に失敗しました。');
}
function h(?string $value): string
{
return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8');
}
<?php
require_once __DIR__ . '/dbconnect.php';
$message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$name = trim($_POST['name'] ?? '');
$size = filter_input(INPUT_POST, 'size', FILTER_VALIDATE_INT);
$memo = trim($_POST['memo'] ?? '');
if ($name === '' || $size === false || $size === null) {
$message = '名前とサイズを正しく入力してください。';
} else {
$sql = 'INSERT INTO animal (name, size, memo) VALUES (:name, :size, :memo)';
$stmt = $dbh->prepare($sql);
$stmt->execute([
':name' => $name,
':size' => $size,
':memo' => $memo,
]);
$message = 'データを追加しました。';
}
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>データ追加 PDO版</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container my-4">
<h1>データ追加</h1>
<p><a href="index.html">トップページへ戻る</a></p>
<?php if ($message !== ''): ?>
<div class="alert alert-info"><?= h($message) ?></div>
<?php endif; ?>
<form method="post" action="insert.php">
<div class="mb-3">
<label for="name" class="form-label">name</label>
<input type="text" name="name" id="name" class="form-control" required>
</div>
<div class="mb-3">
<label for="size" class="form-label">size</label>
<input type="number" name="size" id="size" class="form-control" required>
</div>
<div class="mb-3">
<label for="memo" class="form-label">memo</label>
<textarea name="memo" id="memo" class="form-control" rows="3"></textarea>
</div>
<button type="submit" class="btn btn-primary">追加</button>
</form>
</div>
</body>
</html>
<?php
require_once __DIR__ . '/dbconnect.php';
$sql = 'SELECT id, name, size, memo FROM animal ORDER BY id';
$stmt = $dbh->query($sql);
$animals = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>削除データ選択 PDO版</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container my-4">
<h1>削除データ選択</h1>
<p><a href="index.html">トップページへ戻る</a></p>
<table class="table table-bordered table-striped">
<thead>
<tr><th>操作</th><th>id</th><th>name</th><th>size</th><th>memo</th></tr>
</thead>
<tbody>
<?php foreach ($animals as $animal): ?>
<tr>
<td>
<form method="post" action="delete.php" onsubmit="return confirm('本当に削除しますか?');">
<input type="hidden" name="id" value="<?= h((string)$animal['id']) ?>">
<button type="submit" class="btn btn-danger btn-sm">削除</button>
</form>
</td>
<td><?= h((string)$animal['id']) ?></td>
<td><?= h($animal['name']) ?></td>
<td><?= h((string)$animal['size']) ?></td>
<td><?= h($animal['memo']) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</body>
</html>
<?php
require_once __DIR__ . '/dbconnect.php';
$id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);
$message = '削除対象のidが正しくありません。';
if ($id !== false && $id !== null) {
$sql = 'DELETE FROM animal WHERE id = :id';
$stmt = $dbh->prepare($sql);
$stmt->execute([':id' => $id]);
$message = 'データを削除しました。';
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>削除結果 PDO版</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container my-4">
<h1>削除結果</h1>
<div class="alert alert-info"><?= h($message) ?></div>
<p><a href="select_delete.php">削除画面へ戻る</a></p>
<p><a href="index.html">トップページへ戻る</a></p>
</div>
</body>
</html>
<?php
require_once __DIR__ . '/dbconnect.php';
$sql = 'SELECT id, name, size, memo FROM animal ORDER BY id';
$stmt = $dbh->query($sql);
$animals = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>更新データ選択 PDO版</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container my-4">
<h1>更新データ選択</h1>
<p><a href="index.html">トップページへ戻る</a></p>
<table class="table table-bordered table-striped align-middle">
<thead>
<tr><th>id</th><th>name</th><th>size</th><th>memo</th><th>操作</th></tr>
</thead>
<tbody>
<?php foreach ($animals as $animal): ?>
<tr>
<form method="post" action="update.php">
<td>
<?= h((string)$animal['id']) ?>
<input type="hidden" name="id" value="<?= h((string)$animal['id']) ?>">
</td>
<td><input type="text" name="name" class="form-control" value="<?= h($animal['name']) ?>" required></td>
<td><input type="number" name="size" class="form-control" value="<?= h((string)$animal['size']) ?>" required></td>
<td><input type="text" name="memo" class="form-control" value="<?= h($animal['memo']) ?>"></td>
<td><button type="submit" class="btn btn-primary btn-sm">更新</button></td>
</form>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</body>
</html>
<?php
require_once __DIR__ . '/dbconnect.php';
$id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT);
$name = trim($_POST['name'] ?? '');
$size = filter_input(INPUT_POST, 'size', FILTER_VALIDATE_INT);
$memo = trim($_POST['memo'] ?? '');
$message = '入力内容が正しくありません。';
if ($id !== false && $id !== null && $name !== '' && $size !== false && $size !== null) {
$sql = 'UPDATE animal SET name = :name, size = :size, memo = :memo WHERE id = :id';
$stmt = $dbh->prepare($sql);
$stmt->execute([
':name' => $name,
':size' => $size,
':memo' => $memo,
':id' => $id,
]);
$message = 'データを更新しました。';
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>更新結果 PDO版</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container my-4">
<h1>更新結果</h1>
<div class="alert alert-info"><?= h($message) ?></div>
<p><a href="select_update.php">更新画面へ戻る</a></p>
<p><a href="index.html">トップページへ戻る</a></p>
</div>
</body>
</html>
第5章の講義支援システムでは,最初は第4章と同様にdbconnect.phpでPDO接続オブジェクト$dbhを作成し,各PHPスクリプトから読み込む形で作成します。その後,「共通する機能をまとめる」ところでcommon/common.phpを作成し,データベース接続,ログイン確認,HTMLエスケープなどの共通処理をまとめます。完成版では各PHPスクリプトからrequire_once __DIR__ . '/common/common.php';で共通処理を読み込み,$dbh->prepare(),$stmt->execute(),$stmt->fetch()を使ってデータベース操作を行います。