【PHP】これで分かる関数! – データの検索・サンプル2 –

PHP

更新履歴

更新日更新者更新内容
2021/8/9JJI・サンプルプログラムに0件の判定方法を追記
・プリペアドステートメントの場合、件数カウント用SQLの実行が
必要である旨を追記。

はじめに

この記事で紹介するサンプルプログラムは次の記事で書いている共通関数を使用している場合があります。

サンプルプログラム2

サンプルプログラム2では、次のように検索条件入力画面から検索結果表示画面①~③に遷移するプログラムを使ってデータを検索するプログラムについて紹介します。

検索条件入力画面

検索条件入力画面の動きは次の通りです。
検索条件入力画面は検索条件未入力、一つの検索条件、全検索条件の入力が可能となっています。

検索条件の入力パターン

検索条件の入力パターンにより次のように商品テーブル(GOODS)を検索します。


①検索条件未入力の場合
検索条件未入力の場合、 商品テーブル(GOODS) の全データを検索する。

②商品コードを入力した場合
検索画面の商品コードに一致するデータを検索する。

③商品名を入力した場合
画面の商品名でデータをあいまい検索する。

④複数の検索条件を入力した場合
複数の検索条件に一致するデータを検索する。

⑤検索条件に一致しない場合
検索条件に一致するデータが存在しない場合、検索結果表示画面に一致するデータがない旨のメッセージを表示する。

ソースコード

検索条件入力画面

検索条件入力画面のコードです。

<?php
    // 引数の値への厳密な型付け
    declare(strict_types = 1);
    // 共通ライブラリの読み込み
    require_once __DIR__ . "../../../lib/sample_common.php";
    $filename = basename(__FILE__);
 
    // 共通HTMLヘッダ出力処理
    common_html_header($filename);
 
    echo "ファイル名:${filename}<br>\n";
    echo "テーブルの検索・検索画面<br>\n";
?>
<form id="sendfrm" method="POST">
<table border="1" width="450px">
    <tr>
        <th width="100px">商品コード</th>
        <td><input type="text" name="code" style="width: 300px;"></td>
    </tr>
    <tr>
        <th>商品名</th>
        <td><input type="text" name="name" style="width: 300px;"></td>
    </tr>
</table>
<br>
<input type="button" name="send1" value="検索01" style="width: 100px;" onclick="open_next('02')">
<input type="button" name="send2" value="検索02" style="width: 100px;" onclick="open_next('03')">
<input type="button" name="send3" value="検索03" style="width: 100px;" onclick="open_next('04')">
</form>

<script>
function open_next(id) {
    var frm = document.getElementById("sendfrm");

    frm.action = "sample02_09_03_" + id + ".php";
    frm.submit();
}
</script>

<?php
    // 共通HTMLフッター出力処理
    common_html_footer();
?>

検索結果表示画面①

検索条件入力画面の「検索01」ボタン押下に表示される画面でSQLインジェクション対応なし、PDO::query()のコードです。
検索条件の入力値によりSQLが変わるため、条件分岐を入れてSQLの検索条件(WHERE句)を組み立てた上で実行しています。
検索条件を組み立てる時、文字列を単純に連結しているため、SQLインジェクションが発生しやすいコードになっています。

<?php
    // 引数の値への厳密な型付け
    declare(strict_types = 1);
    // 共通ライブラリの読み込み
    require_once __DIR__ . "../../../lib/sample_common.php";
    // テーブル一覧表示の共通関数の読み込み
    require "sample02_09_03_05.php";

    $filename = basename(__FILE__);
 
    // 共通HTMLヘッダ出力処理
    common_html_header($filename);
 
    echo "ファイル名:${filename}<br>\n";
    echo "テーブルの検索01・結果出力画面<br>\n";

    // POSTデータの取得
    $code = $_POST["code"];         // 商品コード
    $name = $_POST["name"];         // 商品名

    $dbn = "mysql:dbname=sampledb;host=localhost;port=3306";    // DB接続文字列
    $usr = "sampleusr";                                         // DBユーザ名
    $pw = "sample";                                             // DBユーザのパスワード

    $pdo = null;        // PDOオブジェクト
    $stmt = null;       // PDOStatementオブジェクト

    try {
        // DB接続
        $pdo = connect_db($dbn, $usr, $pw);

        // SELECT文の生成
        $sql = "SELECT * FROM GOODS WHERE 1=1";

        // WHERE句の生成
        // 商品コードはイコール条件で検索
        if($code !== "") {
            $sql = $sql. " AND GOODS_CODE='" . $code . "'";
        }

        // 商品名はLIKE条件(あいまい検索)で検索
        if($name !== "") {
            $sql = $sql. " AND GOODS_NAME LIKE '%" . $name . "%'";
        }

        // SELECT文の出力、SELECT文の実行
        echo "SQL:" . $sql . ":<br>\n";
        $stmt = $pdo->query($sql);

        // SELECT文の実行結果を判定し、画面表示する
        output_table_data1($stmt);
    } catch(PDOException $e) {
        echo $e->getLine() . " " . $e->getCode() . " " . $e->getMessage() . "<br>\n";
    } finally {
        // PDOStatementオブジェクトの破棄
        if(!is_null($stmt)) {
            $stmt = null;
        }
        // DB接続のクローズ
        if(!is_null($pdo)) {
            $pdo = null;
        }
    }
    
    // 共通HTMLフッター出力処理
    common_html_footer();
?>

sample02_09_03_05.php

検索結果表示画面①、 検索結果表示画面②の検索結果を表示するコードです。
検索結果の0件はPDOStatement::rowCount()を使用し、判定しています。

<?php
    // 
    function output_table_data1(PDOStatement $stmt) :void {
    if($stmt === false) {
        echo "SQL文の実行に失敗しました<br>\n";
    } else if($stmt->rowCount() === 0) {
        echo "条件に一致するデータは存在しません<br>\n";
    } else {
?>
<table border='1'>
<tr>
    <th>商品コード</th><th>商品名</th><th>価格</th>
</tr>
<?php foreach($stmt as $row) { ?>
<tr>
    <td><?php echo $row["goods_code"]; ?></td>
        <td><?php echo $row["goods_name"]; ?></td>
        <td><?php echo $row["price"]; ?></td>
    </tr>
<?php } ?>
</table>
<?php   } ?>
<?php } ?>

検索結果表示画面②

検索条件入力画面の「検索02」ボタン押下に表示される画面でSQLインジェクション対応あり、PDO::query()のコードです。
SQLの検索条件(WHERE句)を組み立てる際、PDO::quote()を使用し、SQLインジェクションが発生するのを防止しています。

<?php
    // 引数の値への厳密な型付け
    declare(strict_types = 1);
    // 共通ライブラリの読み込み
    require_once __DIR__ . "../../../lib/sample_common.php";
    // テーブル一覧表示の共通関数の読み込み
    require "sample02_09_03_05.php";

    $filename = basename(__FILE__);
 
    // 共通HTMLヘッダ出力処理
    common_html_header($filename);
 
    echo "ファイル名:${filename}<br>\n";
    echo "テーブルの検索02・結果出力画面<br>\n";

    // POSTデータの取得
    $code = $_POST["code"];         // 商品コード
    $name = $_POST["name"];         // 商品名

    $dbn = "mysql:dbname=sampledb;host=localhost;port=3306";    // DB接続文字列
    $usr = "sampleusr";                                         // DBユーザ名
    $pw = "sample";                                             // DBユーザのパスワード

    $pdo = null;        // PDOオブジェクト
    $stmt = null;       // PDOStatementオブジェクト

    try {
        // DB接続
        $pdo = connect_db($dbn, $usr, $pw);

        // SELECT文の生成
        $sql = "SELECT * FROM GOODS WHERE 1=1";

        // WHERE句の生成
        // 商品コードはイコール条件で検索
        if($code !== "") {
            $sql = $sql. " AND GOODS_CODE=" . $pdo->quote($code);
        }

        // 商品名はLIKE条件(あいまい検索)で検索
        if($name !== "") {
            $sql = $sql. " AND GOODS_NAME LIKE " . $pdo->quote("%" . $name . "%");
        }

        // SELECT文の出力、SELECT文の実行
        echo "SQL:" . $sql . ":<br>\n";
        $stmt = $pdo->query($sql);

        // SELECT文の実行結果を判定し、画面表示する
        output_table_data1($stmt);
    } catch(PDOException $e) {
        echo $e->getLine() . " " . $e->getCode() . " " . $e->getMessage() . "<br>\n";
    } finally {
        // PDOStatementオブジェクトの破棄
        if(!is_null($stmt)) {
            $stmt = null;
        }
        // DB接続のクローズ
        if(!is_null($pdo)) {
            $pdo = null;
        }
    }
    
    // 共通HTMLフッター出力処理
    common_html_footer();
?>

検索結果表示画面③

検索条件入力画面の「検索03」ボタン押下に表示される画面でプリペアドステートメントでSQLを実行するコードです。
このコードでは、プリペアドステートメントは名前付きプレースホルダーを使用し、SQLの検索条件(WHERE句)を組み立ててます。

プリペアドステートメントの場合、PDOStatement::rowCount()で検索結果の件数が判定できないため、次のようにデータを取得するSELECT文をカウントするSQLを事前に実行し、判定しています。

$stmt = $pdo->prepare("SELECT COUNT(*) FROM (" . "データ取得のSELECT" . ") P");
$record_cnt = $stmt->fetchColumn();
if($record_cnt === "0") {           // 文字列で返却されることに注意
    // 0件の判定処理を行う
}
<?php
    // 引数の値への厳密な型付け
    declare(strict_types = 1);
    // 共通ライブラリの読み込み
    require_once __DIR__ . "../../../lib/sample_common.php";
    // テーブル一覧表示の共通関数の読み込み
    require "sample02_09_03_06.php";

    $filename = basename(__FILE__);
 
    // 共通HTMLヘッダ出力処理
    common_html_header($filename);
 
    echo "ファイル名:${filename}<br>\n";
    echo "テーブルの検索03・結果出力画面<br>\n";

    // POSTデータの取得
    $code = $_POST["code"];         // 商品コード
    $name = $_POST["name"];         // 商品名

    $dbn = "mysql:dbname=sampledb;host=localhost;port=3306";    // DB接続文字列
    $usr = "sampleusr";                                         // DBユーザ名
    $pw = "sample";                                             // DBユーザのパスワード

    $pdo = null;        // PDOオブジェクト
    $stmt = null;        // PDOStatementオブジェクト

    try {
        // DB接続
        $pdo = connect_db($dbn, $usr, $pw);

        // SELECT文の生成
        $sql = "SELECT * FROM GOODS WHERE 1=1";
        $params = [];

        // WHERE句の生成
        // 商品コードはイコール条件で検索
        if($code !== "") {
            $sql = $sql. " AND GOODS_CODE=:code";
            $params[":code"] = $code;
        }

        // 商品名はLIKE条件(あいまい検索)で検索
        if($name !== "") {
            $sql = $sql. " AND GOODS_NAME LIKE :name";
            $params[":name"] = "%${name}%";
        }

        // レコード件数取得用SQL
        $count_sql = "SELECT COUNT(*) FROM (" . $sql . ") P";
        // プリペアードステートメント
        $stmt = $pdo->prepare($count_sql);
        // プリペアードステートメントの実行
        $stmt->execute($params);
        // プリペアードステートメントの出力
        $stmt->debugDumpParams();

        // レコード件数の取得、判定
        $record_cnt = $stmt->fetchColumn();
        if($record_cnt === "0") {           // 文字列で返却されることに注意
            echo "<br>\n条件に一致するデータは存在しません<br>\n";
        } else {
            // プリペアードステートメント
            $stmt = $pdo->prepare($sql);
            // プリペアードステートメントの実行
            $stmt->execute($params);
            // プリペアードステートメントのデバッグ出力
            $stmt->debugDumpParams();

            // SELECT文の実行結果を判定し、画面表示する
            output_table_data2($stmt);
        }
    } catch(PDOException $e) {
        echo $e->getLine() . " " . $e->getCode() . " " . $e->getMessage() . "<br>\n";
    } finally {
        // PDOStatementオブジェクトの破棄
        if(!is_null($stmt)) {
            $stmt = null;
        }
        // DB接続のクローズ
        if(!is_null($pdo)) {
            $pdo = null;
        }
    }
    
    // 共通HTMLフッター出力処理
    common_html_footer();
?>

sample02_09_03_06.php

検索結果表示画面②の検索結果を表示するコードです。

<?php
    // 
    function output_table_data2(PDOStatement $stmt) :void {
    if($stmt === false) {
        echo "SQL文の実行に失敗しました<br>\n";
    } else {
?>
<table border='1'>
<tr>
    <th>商品コード</th><th>商品名</th><th>価格</th>
</tr>
<?php while($row = $stmt->fetch()) { ?>
<tr>
    <td><?php echo $row["goods_code"]; ?></td>
        <td><?php echo $row["goods_name"]; ?></td>
        <td><?php echo $row["price"]; ?></td>
    </tr>
<?php } ?>
</table>
<?php   } ?>
<?php } ?>

SQLインジェクション

検索結果表示画面①

次のように検索結果表示画面①は検索条件入力画面で入力した不正なSQLが実行されます。

この場合の検索条件表示画面①の出力結果は次のようになります。


<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>sample02_09_03_04.php</title>
</head>
<body>
ファイル名:sample02_09_03_04.php<br>
テーブルの検索03・結果出力画面<br>
SQL: [75] SELECT COUNT(*) FROM (SELECT * FROM GOODS WHERE 1=1 AND GOODS_CODE=:code) P
Sent SQL: [83] SELECT COUNT(*) FROM (SELECT * FROM GOODS WHERE 1=1 AND GOODS_CODE='\' OR 1=1;-') P
Params:  1
Key: Name: [5] :code
paramno=-1
name=[5] ":code"
is_param=1
param_type=2
<br>
条件に一致するデータは存在しません<br>
</body>

検索結果表示画面②

次のように検索結果表示画面②はSQLインジェクション対応を行っているため、不正なSQLが入力された場合は実行されません。

検索結果表示画面③

次のように検索結果表示画面③はプリペアドステートメントを使用しているため、SQLインジェクションは実行されません。

プリペアドステートメントの実行結果

プリペアドステートメントによるSQLの実行結果を確認する場合、次のようにPDOStatement::debugDumpParams()を使用すると出力できます。

・画面

・HTML

<!DOCTYPE html>
<html lang="ja">
<head>
	<meta charset="UTF-8">
	<title>sample02_09_03_04.php</title>
</head>
<body>
ファイル名:sample02_09_03_04.php<br>
テーブルの検索03・結果出力画面<br>
SQL: [101] SELECT COUNT(*) FROM (SELECT * FROM GOODS WHERE 1=1 AND GOODS_CODE=:code AND GOODS_NAME LIKE :name) P
Sent SQL: [113] SELECT COUNT(*) FROM (SELECT * FROM GOODS WHERE 1=1 AND GOODS_CODE='1000000002' AND GOODS_NAME LIKE '%鉛筆%') P
Params:  2
Key: Name: [5] :code
paramno=-1
name=[5] ":code"
is_param=1
param_type=2
Key: Name: [5] :name
paramno=-1
name=[5] ":name"
is_param=1
param_type=2
SQL: [76] SELECT * FROM GOODS WHERE 1=1 AND GOODS_CODE=:code AND GOODS_NAME LIKE :name
Sent SQL: [88] SELECT * FROM GOODS WHERE 1=1 AND GOODS_CODE='1000000002' AND GOODS_NAME LIKE '%鉛筆%'
Params:  2
Key: Name: [5] :code
paramno=-1
name=[5] ":code"
is_param=1
param_type=2
Key: Name: [5] :name
paramno=-1
name=[5] ":name"
is_param=1
param_type=2
<table border='1'>
<tr>
    <th>商品コード</th><th>商品名</th><th>価格</th>
</tr>
<tr>
    <td>1000000002</td>
        <td>B鉛筆</td>
        <td>40</td>
    </tr>
</table>
</body>

コメント