概要と目標
レコードの抽出方法と
SQLインジェクションの対策。
SELECT文を使ったレコードの抽出方法と、
SQLインジェクションという脆弱性の対策方法を学習しましょう。
SELECT文を使ったレコードの抽出方法と、
SQLインジェクションという脆弱性の対策方法を学習しましょう。
SELECT文は実行した後、
SELECT文は、以前紹介したquery()メソッドだけでは画面に表示できない。
実行結果を変数に代入し、連想配列として取得する必要がある。
実行結果を連想配列にするには、fetchAll()メソッドを使えば便利。
fetchAll()メソッド ・・・ SQLの実行結果を配列して取得するメソッド$SQL実行結果が入った変数->fetchALL(配列のスタイル)
詳細はPHPマニュアルを参照
| スタイル | 説明 |
|---|---|
PDO::FETCH_ASSOC |
カラム名をキーとした連想配列として取得 |
PDO::FETCH_NUM |
カラムに「0」からの番号を付けて通常の配列として取得 |
PDO::FETCH_BOTH (デフォルト) |
上記の連想配列と配列の両方とも取得 |
SELECT文でINSERT文をSELECT文に変更し、
実行結果を連想配列として取得するコードを記述
<?php
// ファイルの読み込み
require_once('inc/config.php');
require_once('inc/functions.php');
try {
// データベースへ接続
$dbh = new PDO(DSN, DB_USER, DB_PASSWORD);
// エラー発生時に「PDOException」という例外を投げる設定に変更
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SQL文の作成
$sql = 'SELECT * FROM posts ORDER BY created DESC';
// SQLを実行
$stmt = $dbh->query($sql);
// 実行結果を連想配列として取得
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($result);
// データベースとの接続を終了
$dbh = null;
} catch (PDOException $e) {
// 例外発生時の処理
echo 'エラー' . h($e->getMessage());
exit();
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>新着記事一覧</title>
</head>
<body>
<h1>新着情報</h1>
</body>
</html>
連想配列となったSELECT文の抽出結果は、ループを活用して画面に出力すればよい。
print_r($result); の部分をコメントにし、h1要素の下に、ループを使って用語説明リストとして画面に出力するコードを記述dt要素 ・・・ 記事の作成日 (<time> ~ </time>で囲む)dd要素 ・・・ 記事のタイトル<?php
// ファイルの読み込み
require_once('inc/config.php');
require_once('inc/functions.php');
try {
// データベースへ接続
$dbh = new PDO(DSN, DB_USER, DB_PASSWORD);
// エラー発生時に「PDOException」という例外を投げる設定に変更
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SQL文の作成
$sql = 'SELECT * FROM posts ORDER BY created DESC';
// SQLを実行
$stmt = $dbh->query($sql);
// 実行結果を連想配列として取得
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
// print_r($result);
// データベースとの接続を終了
$dbh = null;
} catch (PDOException $e) {
// 例外発生時の処理
echo 'エラー' . h($e->getMessage());
exit();
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>新着記事一覧</title>
</head>
<body>
<h1>新着情報</h1>
<dl>
<?php foreach($result as $row) : ?>
<dt><time><?php echo h($row['created']) ?></time></dt>
<dd><?php echo h($row['title']) ?></dd>
<?php endforeach; ?>
</dl>
</body>
</html>
date関数と、strtotime関数を組み合わせる。
datetime型のレコードは、年月日が「-」で区切られ、時間分秒が、「:」で区切られる。
このフォーマットを変更するには、以前紹介したdate関数と、
strtotime関数を組み合わせる事で、指定日時のフォーマットを変更できる。
date関数 ・・・ 日付/時刻を書式化する関数date(フォーマット, タイムスタンプ)
省略可
タイムスタンプ: 「1970年1月1日 00:00:00」からの経過ミリ秒を指定。省略すると time() の値
詳細はPHPマニュアルを参照
strtotime関数 ・・・ 英文形式の日付をタイムスタンプに変換する関数strtotime(英文形式の日付)
英文形式の日付: 「2025-12-17 13:16:29」や「yesterday」や「+7 day」などが指定可能
詳細はPHPマニュアルを参照
date関数と、strtotime関数を使って、公開日のフォーマットを変更date('Y年m月d日', strtotime($row['created']))time要素に、datetime属性を追加
<dl>
<?php foreach($result as $row) : ?>
<dt><time datetime="<?php echo h($row['created']); ?>"><?php echo h(date('Y年m月d日', strtotime($row['created']))); ?></time></dt>
<dd><?php echo h($row['title']); ?></dd>
<?php endforeach; ?>
</dl>
どの記事であっても詳細ページは、1枚の同じPHPファイルで表示する。
まずは、記事を表示するテンプレートとなるHTMLを作成しておく。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>[記事のタイトル]</title>
</head>
<body>
<h1>[記事のタイトル]</h1>
<ul>
<li>公開日: [記事の公開日]</li>
<li>カテゴリ: [記事のカテゴリ名]</li>
</ul>
<p>
[記事の内容]
</p>
<p><a href="./">一覧に戻る</a></p>
</body>
</html>
GETパラメータを付ける。
「posts」テーブルには記事ごとに固有の番号を割り当てる「id」フィールドがある。
1枚のPHPファイルで、記事ごとに表示内容を切り分けるには、
その記事IDをGETパラメータとしてリンク先のURLに付加する。
detail.php?id=記事ID
GETパラメータを付加する
<dl>
<?php foreach($result as $row) : ?>
<dt><time datetime="<?php echo h($row['created']); ?>"><?php echo h(date('Y年m月d日', strtotime($row['created']))); ?></time></dt>
<dd>
<a href="detail.php?id=<?php echo h($row['id']); ?>">
<?php echo h($row['title']); ?>
</a>
</dd>
<?php endforeach; ?>
</dl>
GETパラメータが付いていることを確認
URLに付加された記事IDのGETパラメータを取得し、
その記事IDと一致するレコードをSELECT文で抽出すれば、
記事ごとに内容を切り分けれる。
SELECT文SELECT p.*, c.category_name FROM posts AS p JOIN categories AS c
ON p.category_id = c.id WHERE p.id = 記事ID;
GETパラメータのチェックをしてみよう。GETパラメータが付加されていなかった時の処理を記述
<?php
// ファイルの読み込み
require_once('inc/config.php');
require_once('inc/functions.php');
// GETパラメータのチェック
if ( empty($_GET['id']) ) {
// $_GET['id'] が 空 の場合
header('Location: index.php');
exit();
}
?>
GETパラメータを削除した際は、リダイレクトされるかも確認
GETパラメータを使って、try ~ catch構文を使って、PDOでデータベースに接続し、SELECT文で、記事のレコードを取得する処理を記述
<?php
// ファイルの読み込み
require_once('inc/config.php');
require_once('inc/functions.php');
// GETパラメータのチェック
if ( empty($_GET['id']) ) {
// $_GET['id'] が 空 の場合
header('Location: index.php');
exit();
}
try {
// データベースへ接続
$dbh = new PDO(DSN, DB_USER, DB_PASSWORD);
// エラー発生時に「PDOException」という例外を投げる設定に変更
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SQL文の作成
$sql = 'SELECT p.*, c.category_name FROM posts AS p JOIN categories AS c ON p.category_id = c.id WHERE p.id = '. $_GET['id'];
// SQLを実行
$stmt = $dbh->query($sql);
// 実行結果を連想配列として取得
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($result);
// データベースとの接続を終了
$dbh = null;
} catch (PDOException $e) {
// 例外発生時の処理
echo 'エラー' . h($e->getMessage());
exit();
}
?>
一見うまく行っているように見えるが、
上記のコードに、SQLインジェクションという脆弱性がある。
$sql = "select * from users where password = '{$_POST['passowrd']}'";
' or '' = ' というコードを渡すと以下のようなSELECT文になる。
select * from users where password = '' or '' = '';
SQL文にフォームから受け取ったデータを組み込む場合は、プリペアド・ステートメントを利用する。
プリペアド・ステートメントは、SQL文に「?(プレースホルダー)」を埋め込んで、
後から変数で値を渡すテンプレートのようなもの。
プリペアド・ステートメントを使うには、まずprepare()メソッドで、ステートメントを用意する必要がある。
prepare()メソッド ・・・ ステートメントを用意するメソッド$変数名 = $DBハンドル->prepare(プレースホルダーを埋め込んだSQL文)
戻り値: ステートメントハンドル
詳細はPHPマニュアルを参照
$_GET['id']」を埋め込んでいる箇所を「?」(プレースホルダー)に置き換え、prepareメソッドで、ステートメントを用意するコードに変更
<?php
// ファイルの読み込み
require_once('inc/config.php');
require_once('inc/functions.php');
// GETパラメータのチェック
if ( empty($_GET['id']) ) {
// $_GET['id'] が 空 の場合
header('Location: index.php');
exit();
}
try {
// データベースへ接続
$dbh = new PDO(DSN, DB_USER, DB_PASSWORD);
// エラー発生時に「PDOException」という例外を投げる設定に変更
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SQL文の作成
$sql = 'SELECT p.*, c.category_name FROM posts AS p JOIN categories AS c ON p.category_id = c.id WHERE p.id = ?';
// ステートメント用意
$stmt = $dbh->prepare($sql);
// 実行結果を連想配列として取得
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($result);
// データベースとの接続を終了
$dbh = null;
} catch (PDOException $e) {
// 例外発生時の処理
echo 'エラー' . h($e->getMessage());
exit();
}
?>
用意したステートメントのプレースホルダーに値を埋め込むには、bindValue()メソッドを使う。
bindValue()メソッド ・・・ ステートメントを用意するメソッド$ステートメントハンドル->bindValue(何番目の「?」か, 埋め込む値, データ型)
詳細はPHPマニュアルを参照
| 定数 | 説明 |
|---|---|
PDO::PARAM_INT |
整数値型 |
PDO::PARAM_STR |
文字列型 |
bindValue()メソッドで、$_GET['id']をバインド$_GET['id']の手前に「(int)」と記述し数値型に変換しておく)
<?php
// ファイルの読み込み
require_once('inc/config.php');
require_once('inc/functions.php');
// GETパラメータのチェック
if ( empty($_GET['id']) ) {
// $_GET['id'] が 空 の場合
header('Location: index.php');
exit();
}
try {
// データベースへ接続
$dbh = new PDO(DSN, DB_USER, DB_PASSWORD);
// エラー発生時に「PDOException」という例外を投げる設定に変更
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SQL文の作成
$sql = 'SELECT p.*, c.category_name FROM posts AS p JOIN categories AS c ON p.category_id = c.id WHERE p.id = ?';
// ステートメント用意
$stmt = $dbh->prepare($sql);
// プレースホルダーに値をガッチャンコ
$stmt->bindValue(1, (int)$_GET['id'] , PDO::PARAM_INT);
// 実行結果を連想配列として取得
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($result);
// データベースとの接続を終了
$dbh = null;
} catch (PDOException $e) {
// 例外発生時の処理
echo 'エラー' . h($e->getMessage());
exit();
}
?>
query()メソッドは、ステートメントには使えない。
プレースホルダーに値を埋め込んだステートメントを実行するには、execute()メソッドで実行する、
execute()メソッド ・・・ ステートメントを実行するメソッド$ステートメントハンドル->execute()
詳細はPHPマニュアルを参照
execute()メソッドで、ステートメントを実行
<?php
// ファイルの読み込み
require_once('inc/config.php');
require_once('inc/functions.php');
// GETパラメータのチェック
if ( empty($_GET['id']) ) {
// $_GET['id'] が 空 の場合
header('Location: index.php');
exit();
}
try {
// データベースへ接続
$dbh = new PDO(DSN, DB_USER, DB_PASSWORD);
// エラー発生時に「PDOException」という例外を投げる設定に変更
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SQL文の作成
$sql = 'SELECT p.*, c.category_name FROM posts AS p JOIN categories AS c ON p.category_id = c.id WHERE p.id = ?';
// ステートメント用意
$stmt = $dbh->prepare($sql);
// プレースホルダーに値をガッチャンコ
$stmt->bindValue(1, (int)$_GET['id'] , PDO::PARAM_INT);
// ステートメントを実行
$stmt->execute();
// 実行結果を連想配列として取得
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
print_r($result);
// データベースとの接続を終了
$dbh = null;
} catch (PDOException $e) {
// 例外発生時の処理
echo 'エラー' . h($e->getMessage());
exit();
}
?>
fetchAll()メソッドは全レコードを取得。fetch()メソッドは1レコードずつ取得。
詳細ページのSELECT文は、記事IDと一致する1レコードのみが結果として返ってくる。
その結果をfetch()メソッドで連想配列にした場合、無駄に多次元配列となる。
結果を1レコードのみ取得するには、fetch()メソッドを使うとよい
fetch()メソッド ・・・ SQLの実行結果から1行取得するメソッド$SQL実行結果が入った変数->fetch(配列のスタイル)
詳細はPHPマニュアルを参照
| スタイル | 説明 |
|---|---|
PDO::FETCH_ASSOC |
カラム名をキーとした連想配列として取得 |
PDO::FETCH_NUM |
カラムに「0」からの番号を付けて通常の配列として取得 |
PDO::FETCH_BOTH (デフォルト) |
上記の連想配列と配列の両方とも取得 |
fetchAll()メソッドの部分を、fetch()メソッドに変更
<?php
// ファイルの読み込み
require_once('inc/config.php');
require_once('inc/functions.php');
// GETパラメータのチェック
if ( empty($_GET['id']) ) {
// $_GET['id'] が 空 の場合
header('Location: index.php');
exit();
}
try {
// データベースへ接続
$dbh = new PDO(DSN, DB_USER, DB_PASSWORD);
// エラー発生時に「PDOException」という例外を投げる設定に変更
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SQL文の作成
$sql = 'SELECT p.*, c.category_name FROM posts AS p JOIN categories AS c ON p.category_id = c.id WHERE p.id = ?';
// ステートメント用意
$stmt = $dbh->prepare($sql);
// プレースホルダーに値をガッチャンコ
$stmt->bindValue(1, (int)$_GET['id'] , PDO::PARAM_INT);
// ステートメントを実行
$stmt->execute();
// 実行結果を1レコードのみ連想配列として取得
$result = $stmt->fetch(PDO::FETCH_ASSOC);
print_r($result);
// データベースとの接続を終了
$dbh = null;
} catch (PDOException $e) {
// 例外発生時の処理
echo 'エラー' . h($e->getMessage());
exit();
}
?>
print_r関数の部分をコメントにし、 // print_r($result);
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title><?php echo h($result['title']); ?></title>
</head>
<body>
<h1><?php echo h($result['title']); ?></h1>
<ul>
<li>公開日: <time datetime="<?php echo h($result['created']); ?>"><?php echo h(date('Y年m月d日', strtotime($result['created']))); ?></time></li>
<li>カテゴリ: <?php echo h($result['category_name']); ?></li>
</ul>
<p>
<?php echo h($result['content']); ?>
</p>
</body>
</html>
fetch()は実行する度にレコードを取得fetch()メソッドは実行する度に、先頭から1レコードずつ連想配列に結果を取得する。falseを返す。
従ってwhile文を使って全レコードを取得することもできる。
<?php
// ファイルの読み込み
require_once('inc/config.php');
require_once('inc/functions.php');
try {
// データベースへ接続
$dbh = new PDO(DSN, DB_USER, DB_PASSWORD);
// エラー発生時に「PDOException」という例外を投げる設定に変更
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SQL文の作成
$sql = 'SELECT * FROM posts ORDER BY created DESC';
// SQLを実行
$stmt = $dbh->query($sql);
// 1レコードずつ $result に連想配列して結果を取得
while ( $result = $stmt->fetch(PDO::FETCH_ASSOC) ) {
echo h($result['title']);
}
// データベースとの接続を終了
$dbh = null;
} catch (PDOException $e) {
// 例外発生時の処理
echo 'エラー' . h($e->getMessage());
exit();
}
?>
PHPでSELECT、INSERT、UPDATE、DELETE文を実行する時は、以下の手順が基本となる。
$dbh = new PDO(DSN, DB_USER, DB_PASSWORD);
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$sql = 'select * from users where name = ? and age >= ?';
$stmt = $dbh->prepare($sql);
$name = 'Tom';
$age = 20;
$stmt->bindValue(1, $name, PDO::PARAM_STR);
$stmt->bindValue(2, $age, PDO::PARAM_INT);
$stmt->execute();
fatchAll()で取り出す場合
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($result as $row) {
echo h( $row['name'] . '('. $row['age'] .')' );
}
1レコードずつfatch()で取り出す場
while ($result = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo h( $result['name'] . '('. $result['age'] .')' );
}
$stmt = null;
$dbh = null;
完成例を参考に「tranig」フォルダ内の「index.php」に
カテゴリ一覧ページを作成して下さい。
<?php
// データベースの定数
define('DB_NAME', 'mini_cms_app');
define('DB_USER', 'root');
define('DB_PASSWORD', ''); // パスワード (MAMPは「root」)
define('DSN', 'mysql:host=localhost;dbname='. DB_NAME . ';charset=utf8');
// XSS 対策
function h($s) {
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
try {
// データベースへ接続
$dbh = new PDO(DSN, DB_USER, DB_PASSWORD);
// エラー発生時に「PDOException」という例外を投げる設定に変更
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// SQL文の作成
$sql = 'SELECT * FROM categories';
// SQLを実行
$stmt = $dbh->query($sql);
// 実行結果を連想配列として取得
$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
// データベースとの接続を終了
$dbh = null;
} catch (PDOException $e) {
// 例外発生時の処理
echo 'エラー' . h($e->getMessage());
exit();
}
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>カテゴリ一覧</title>
</head>
<body>
<h1>カテゴリ一覧</h1>
<table border="1">
<thead>
<th>ID</th>
<th>カテゴリ名</th>
</thead>
<?php foreach($result as $row) : ?>
<tr>
<td><?php echo h($row['id']) ?></td>
<td><?php echo h($row['category_name']) ?></td>
</tr>
<?php endforeach; ?>
</table>
</body>
</html>
解答例は全問題のチェックボックスが on になるとご覧いただけます。
PHPでデータベースを操作する際は、SQLインジェクションなどの脆弱性に注意する必要がある。
SELECT文は結果を連想配列として取得する