[PHP] mysqli_stmt が使いにくかったのでラッパー関数を作った


はじめに
--------

[前回の記事](http://qiita.com/from_kyushu/items/0023a2b0816f82e1740f)で旧MySQL関数からMySQLi関数に乗り換えを行った。
折角MySQLi関数に乗り換えたのだからプリペアドステートメントが使いたい、ということで `mysqli_stmt` について調べてみたところ、非常に使いづらそうな印象を受けた。
この記事は筆者が `mysqli_stmt` を使用するまでの作業ログである。

mysqli_stmt のイケてない点
--------------------------

### バインドするパラメータの型を指定しなければならない

`PDO` でプリペアドステートメントを利用する場合、引数にバインドするパラメータを配列形式で渡せばよいだけである。
`PDOStatement::bindParam` や `PDOStatement::bindValue` を使用すれば型を指定できるが、こちらも省略可能となっている。

```php
$pdo = new PDO('mysql:host=host;dbname=somedb', 'user', 'pass');
$stmt = $pdo->prepare('SELECT colA, colB, colC FROM sometable WHERE colA = ?');
$stmt->execute(array($argA));
```

MySQLi関数でもこんな感じで使えるのだろう、と楽観していたのだが、`mysqli_stmt` はバインドするときに引数の型が必須となっている。

```php
$conn = mysqli_connect('host', 'user', 'password', 'somedb');
$stmt = mysqli_prepare($conn, 'SELECT colA, colB, colC FROM sometable WHERE colA = ?');
$stmt->bind_param('s', $argA); // $argAが文字列の場合
$stmt->bind_param('i', $argB); // $argBが数値の場合
$stmt->bind_param('s', $argC); // $argCが文字列の場合
mysqli_stmt_execute($stmt);
```

もちろん型を明示的に指定しておいたほうが安全ではあるのだろうが...正直...面倒である。

### 結果を取得するために、事前に変数をバインドさせなければならない

`PDO` の場合、1行で結果の1行を配列で取得できる。
こちらも `PDOStatement::bindColumn` で変数にバインドできるが、大体は配列で取るか、1カラムだけであればフェッチモードを `PDO::FETCH_COLUMN` にしてカラム番号指定でフェッチするほうが面倒がないと思う。

```php
$row = $stmt->fetch();
```

`mysqli_stmt` の場合、必ず `mysqli_stmt::bind_result` で変数をバインドさせなければならない。

```php

$colA = null;
$colB = null;
$colC = null;
mysqli_stmt_bind_result($stmt, $colA, $colB, $colC);
mysqli_stmt_fetch($stmt);
```

長い...長すぎる...！
例で挙げたのは3カラムだけなのでまだよいが、カラム数が多くなれば多くなるほどコードが長くなってしまう。

このような仕様ならば使うのが非常に億劫になってしまう。

mysqli_stmt の使い勝手を向上させる
----------------------------------

上記2点は我慢が出来なかったのでどうにかできないものか...、と思案していたところ、PHPマニュアルのコメント欄に非常に有用なコードが書かれていることに気がついた。
これだ...！これで勝てる...！

### パラメータの型を自動判定

下記リンクの投稿ではパラメータの型を自動判定し `mysqli_stmt_bind_param()` の第2引数に渡す文字列を自動生成する関数が書かれている。

* [mysqli_stmt::bind_paramの改善](http://php.net/manual/ja/mysqli-stmt.bind-param.php#80118)

PHPマニュアルのコメント欄にあったものだとバイナリの判定を行っていなかった。
「どのようなデータをバイナリとするか」という話は非常にややこしいので、筆者はシンプルに「データにNULバイトを含めばバイナリ」と判断させることにした。
また、元のものの場合、 `is_int()` も `is_double()` も `is_string()` も `false` の場合悲惨なことになってしまうため、 `is_int()` と `is_double()` が `false` かつ筆者基準でバイナリでないものは強制的に文字列にキャストすることにした。

```php
function _create_bind_param_args($params){
  $args = array("");
  foreach($params as $key => $param){
    if(is_int($param)){
      $args[0] .= "i";
    } elseif(is_double($param)){
      $args[0] .= "d";
    } else {
      if(strpos($param, "\0") === false){
        $args[0] .= "s";
        $param = (string) $param;
      } else {
        $args[0] .= "b";
      }
    }
    $args[] = $param;
  }
  return $args;
}

function mysqli_stmt_execute_fixed(&$stmt, $args){
  $args = _create_bind_param_args($args); // => array('s', $argA);
  array_unshift($args, $stmt);
  call_user_func_array('mysqli_stmt_bind_param', $args);
  mysqli_stmt_execute($stmt);
}

mysqli_stmt_execute_fixed($stmt, array($argA));

```

オブジェクト指向型で書けばもう少しすっきりする（少なくとも `array_unshift()` が不要になる）のだが、呼び出し時には1行で済むようになった。
型を指定する必要がある場合は直で `mysqli_stmt_bind_param()` を呼んで `mysqli_stmt_execute()` すればよい。

### バインドする配列を自動生成

下記リンクの投稿ではフィールド情報からバインドする連想配列を作り、`call_user_func_array()` で引数を配列で指定できることを利用して連想配列に値をバインドさせている。

* [mysqli_stmt::bind_resultの改善](http://php.net/manual/ja/mysqli-stmt.bind-result.php#102179)

データをフェッチする度にフィールド情報をフェッチするのは少し無駄だと感じたため、少々野暮ったいがこのような実装とした。

```php
function mysqli_stmt_bind_result_assoc($stmt){
  $meta = mysqli_stmt_result_metadata($stmt);
  $bind = array();
  $params = array();
  if(!is_null($meta)){
    while($field = mysqli_fetch_field($meta)){
      $params[] = $bind[$field->name];
    }
    call_user_func_array('mysqli_stmt_bind_result', $params);
  }
  return $bind;
}

function mysqli_stmt_fetch_assoc($stmt, $bind){
  mysqli_stmt_fetch($stmt);
  $row = array();
  foreach($bind as $key => $val){
    $row[$key] = $val;
  }
  return $row;
}

$bind = mysqli_stmt_bind_result_assoc($stmt);
$row = mysqli_stmt_fetch_assoc($stmt, $bind);
```

`mysqli_stmt_fetch_assoc()` を使わず直で `mysqli_stmt_fetch()` を呼んでも `$bind` に値は入るが、筆者の好みでこのようにしている。

蛇足
----

ここで手続き型で実装した例を記載したのだが、`mysqli_prepare()` の返り値は `mysqli_stmt` オブジェクトであるため、 `$stmt->bind_param()` のようにこの部分だけオブジェクト指向型で書くことも可能である。