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

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

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

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

# どのような問題か

「条件を１つ増やすのも　２つ増やすのも　変わらないだろう」というと　変わるし、「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 = ?
```

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

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

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

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

# 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( ) 関数というのは　ちょっと変わったやつで、

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

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

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

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

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

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

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

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

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

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

## 参照渡し？

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

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

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

とりあえず

rensyu.php 抜粋

```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 の第１引数 "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 条件文」をつなげるための番兵のようなものなんだが、急に　マ　（愚痴を言う職業プログラマの略称）っぽくなる。

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

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

```php
// 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[ ]配列で調べることができるのだろうか？