2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

PHP、MySQLで検索フォームを仕様変更しようぜ

Last updated at Posted at 2017-04-12

コーヒーに砂糖を1つ入れるぐらいの感覚で 仕様変更 は起こるんだが、
コーヒーでも飲みながら対応しよう。

先人が記事を残してくれている。

「PHPでMySQLiの変数の数を可変(動的)にする方法」ある蜜柑の上にアルミ缶
https://s8a.jp/php-mysqli-bind-param

どのような問題か

「条件を1つ増やすのも 2つ増やすのも 変わらないだろう」というと 変わるし、「SQLはただの文字列だし ちょちょっと足すだけじゃないか」というと クラッカーも「SQLはただの文字列だし ちょちょっと足し」てきて クラッキングしてくるし、

「SQLインジェクション」Wikipedia
https://ja.wikipedia.org/wiki/SQL%E3%82%A4%E3%83%B3%E3%82%B8%E3%82%A7%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3

SQLは ただの文字列ではあるものの バインド という手法を使おう、というのが 手 なわけだ。

おっと、家が揺れている。萎える。

例えば

WHERE name = ? AND age = ? AND weight = ?

みたいな条件文があるとして、2000年代なら 社内のシステムだのクラッキングは無いだの 頭に「dammy = TRUE」でも足して

WHERE dammy = TRUE AND name = ? AND age = ? AND weight = ?

みたいな構文にしておけば あとは「 AND 変数名 = ?」を文字列連結だ ワハハ、みたいな素朴なテクニックで済んだんだが

Web系で仕事を増やそうと思うと ワハハ とも言ってられないので 問題にぶち当たるたびに 手持ちのスキルを アップデートしていこう。

call_user_func_array( ) 関数

「call_user_func_array」php
http://php.net/manual/en/function.call-user-func-array.php

call_user_func_array( ) 関数 自体は、SQLとは関係なく、あなたの関数を呼び出しますよ、引数は配列指定で、といった汎用的なものだが、これが bind_param( ) 関数にも使えるらしい。へー

bind_param( ) 関数

bind_param( ) 関数というのは ちょっと変わったやつで、

$stmt->bind_param('sii', $name, $age, $weight);

第一引数が 型指定 になっていて、s が文字列、 i が数字になっていて、
第二引数以降が 名前、年齢、体重 なら 第一引数は 'sii' になるわけだ。
慣れてくると もちろん sssssis とか書き出す。

こんな変わった関数に どうやって call_user_func_array( ) を使うのかと思ったら なんのことはない、

配列 params[0] に .= 演算子を使って ssssisisisis とか文字列連結していき、
params[]= 演算子(要するに Addメソッドだ)を使って 第二引数以降を追加していくだけだ。

でも、WHERE句の方はどうすんだ、と思ってみると

$sql .= ' WHERE ' . implode(",", $sqlWhere);

とかいう変わった書き方をしている。

「implode」sql
http://php.net/manual/en/function.implode.php

WHERE 句のところに 条件式をカンマで並べていいのだろうか?
調べても出てこないので 無難に AND に書き換えておくか……。

使い方は分かったが もっと簡単な方法が欲しいもんだ。
試しに 書いてみる。

参照渡し?

サンプル・プログラムにこう書いてあった。

// bind_paramに渡す引数を参照渡しに変更
$params = [];
foreach ($sqlParams as $key => $value) {
    $params[$key] = &$sqlParams[$key];
}

じゃあ、bind_param( ) の引数は 参照渡しだったのか?

とりあえず

rensyu.php 抜粋

// 参考: 「[PHP] mysqli使い方まとめ(MySQL接続~SELECT実行まで)」Qiita http://qiita.com/yasumodev/items/bd2ba476f31804d527d3
// 参考: 「PHPでMySQLiの変数の数を可変(動的)にする方法」ある蜜柑の上にアルミ缶 https://s8a.jp/php-mysqli-bind-param

// ~略~

// http://★サイトアドレス/rensyu.php?name=むずでょ&age=37&weight=80
// c_ ... criteria 検索条件
$c_name   = $_GET['name'];   // 絞り込み: 名前
$c_age    = $_GET['age'];    // 絞り込み: 年齢
$c_weight = $_GET['weight']; // 絞り込み: 体重

if(!empty($c_name) || !empty($c_age) || !empty($c_weight)){

    $bindParam_args = [];
    $bindParam_args[0] = "";

    $query  = 'SELECT NAME, AGE, WEIGHT FROM RENSYU_TBL WHERE 1=1';

    if(!empty($c_name)){
        $bindParam_args[0] .= "s";
        $bindParam_args[]   = $c_name;
        $query .= ' AND NAME = ?';
    }

    if(!empty($c_age)){
        $bindParam_args[0] .= "i";
        $bindParam_args[]   = $c_age;
        $query .= ' AND AGE = ?';
    }

    if(!empty($c_weight)){
        $bindParam_args[0] .= "i";
        $bindParam_args[]   = $c_weight;
        $query .= ' AND WEIGHT = ?';
    }

    $query .= ' ORDER BY AGE, WEIGHT DESC LIMIT 20';

    // bind_paramに渡す引数を参照渡しに変更
    $bindParam_prms = [];
    foreach ($bindParam_args as $key => $value) {
        $bindParam_prms[$key] = &$bindParam_args[$key];
    }

    $stmt = $mysqli->stmt_init();
    if($stmt->prepare($query))
    {
        // 条件値をSQLにバインドする
        // bind_param の第1引数 "is" は後続のデータ型を表します。
        // i=integer、s=string、d=double、b=blob など。DATE型は s で良いみたいです。
        // また下記のように値を引数内に直書きすることはできません。
        // 誤)$stmt->bind_param("is", 123, "hanako");

        call_user_func_array(array($stmt, 'bind_param'), $bindParam_prms);

        // 実行
        $stmt->execute();

        // 結果はこの変数に入れるようにする
        $stmt->bind_result($NAME, $AGE, $WEIGHT);

        // TODO: ここで結果出力

        // 結果セットを閉じる
        $stmt->close();
    }
    else
    {
        print "Failed to prepare statement\n";
    }

}
else
{
    // バインドが要らない方
}

// 以下略

このコードは そのままでは使えないが、要点だけ書きぬくと こうなる。
コードの読みにくさも 抑えた方だろうか。

なんということだろう。 WHERE 1=1 テクニックから離れられない。「これは何ですか?」と言われれば うしろで「 AND 条件文」をつなげるための番兵のようなものなんだが、急に マ (愚痴を言う職業プログラマの略称)っぽくなる。

ページの頭に 検索条件リンクを並べようぜ

こんなんで いいんだろうか?

// http://★サイトアドレス/rensyu.php?name=むずでょ&age=37&weight=80
// c_ ... criteria 検索条件
$c_name   = $_GET['name'];   // 絞り込み: 名前
$c_age    = $_GET['age'];    // 絞り込み: 年齢
$c_weight = $_GET['weight']; // 絞り込み: 体重

function createGETString($c_name, $c_age, $c_weight){
    $arr = [];
    if(!empty($c_name)){
        $arr[]='name='.$c_name;
    }

    if(!empty($c_age)){
        $arr[]='age='.$c_age;
    }

    if(!empty($c_weight)){
        $arr[]='weight='.$c_weight;
    }

    return implode('&', $arr);
}

//----------
// 絞り込み検索 name (名前)
//----------
$query = 'SELECT DISTINCT NAME FROM RENSYU_TBL';
if ($result = $mysqli->query($query)) {

    if(0 < $result->num_rows){
        // 絞り込み条件なし
        echo '<a href="./' . basename($_SERVER['PHP_SELF']) . '?' . createGETString('', $c_age, $c_weight) . '">全て</a> '. "\n";
    }

    // 連想配列を取得
    while ($row = $result->fetch_assoc()) {
        // URLの末尾に絞り込み条件を付けます
        echo '<a href="./' . basename($_SERVER['PHP_SELF']) . '?' . createGETString($row['NAME'], $c_age, $c_weight) . '">' . $row['NAME'] . '</a> '. "\n";
    }
    echo '<br />' . "\n";
    // 結果セットを閉じる
    $result->close();
}

//----------
// 絞り込み検索 age (年齢)
//----------
$query = 'SELECT DISTINCT AGE FROM RENSYU_TBL';
if ($result = $mysqli->query($query)) {

    if(0 < $result->num_rows){
        // 絞り込み条件なし
        echo '<a href="./' . basename($_SERVER['PHP_SELF']) . '?' . createGETString($c_name, '', $c_weight) . '">全て</a> '. "\n";
    }

    // 連想配列を取得
    while ($row = $result->fetch_assoc()) {
        // URLの末尾に絞り込み条件を付けます
        echo '<a href="./' . basename($_SERVER['PHP_SELF']) . '?' . createGETString($c_name, $row['AGE'], $c_weight) . '">' . $row['AGE'] . '</a> '. "\n";
    }
    echo '<br />' . "\n";
    // 結果セットを閉じる
    $result->close();
}

//----------
// 絞り込み検索 weight (体重)
//----------
$query = 'SELECT DISTINCT WEIGHT FROM RENSYU_TBL';
if ($result = $mysqli->query($query)) {

    if(0 < $result->num_rows){
        // 絞り込み条件なし
        echo '<a href="./' . basename($_SERVER['PHP_SELF']) . '?' . createGETString($c_name, $c_age, '') . '">全て</a> '. "\n";
    }

    // 連想配列を取得
    while ($row = $result->fetch_assoc()) {
        // URLの末尾に絞り込み条件を付けます
        echo '<a href="./' . basename($_SERVER['PHP_SELF']) . '?' . createGETString($c_name, $c_age, $row['WEIGHT']) . '">' . $row['WEIGHT'] . '</a> '. "\n";
    }
    echo '<br />' . "\n";
    // 結果セットを閉じる
    $result->close();
}

ざっくりした サンプル・プログラムを置いとくんで、実践で使うには データベースのテーブルの方にキーも設定するとか、何かしら改造がいるだろう。

おわり!

ソート

メールを よく読んでみると ソートに使うキーの順番も書いてある。

そりゃそうか。

WHERE 句に条件を足したなら ORDER BY 句にも足したい。

だったら ORDER BY に並べる順序で if 文を並べるか?

もっと よくよく読んでみると、最後にクリックした方を 優先順位を高くしてほしいらしい。

後入れ先出し法、つまり スタックを使えば それは可能だが、PHP でスタックとかあるのか?

「配列をスタック、キュー、セットとして使う編集する」SeeSaaWiki
http://seesaawiki.jp/izt_php/d/%C7%DB%CE%F3%A4%F2%A5%B9%A5%BF%A5%C3%A5%AF%A1%A2%A5%AD%A5%E5%A1%BC%A1%A2%A5%BB%A5%C3%A5%C8%A4%C8%A4%B7%A4%C6%BB%C8%A4%A6

array_ナントカ関数でいけそうだ。

じゃあ、 ?name=むずでょ&age=37&weight=80 がどういう順序で並んでいるのか、$_GET[ ]配列で調べることができるのだろうか?

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?