Posted at

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

More than 3 years have passed since last update.


はじめに

前回の記事で旧MySQL関数からMySQLi関数に乗り換えを行った。

折角MySQLi関数に乗り換えたのだからプリペアドステートメントが使いたい、ということで mysqli_stmt について調べてみたところ、非常に使いづらそうな印象を受けた。

この記事は筆者が mysqli_stmt を使用するまでの作業ログである。


mysqli_stmt のイケてない点


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

PDO でプリペアドステートメントを利用する場合、引数にバインドするパラメータを配列形式で渡せばよいだけである。

PDOStatement::bindParamPDOStatement::bindValue を使用すれば型を指定できるが、こちらも省略可能となっている。

$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 はバインドするときに引数の型が必須となっている。

$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 にしてカラム番号指定でフェッチするほうが面倒がないと思う。

$row = $stmt->fetch();

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


$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引数に渡す文字列を自動生成する関数が書かれている。

PHPマニュアルのコメント欄にあったものだとバイナリの判定を行っていなかった。

「どのようなデータをバイナリとするか」という話は非常にややこしいので、筆者はシンプルに「データにNULバイトを含めばバイナリ」と判断させることにした。

また、元のものの場合、 is_int()is_double()is_string()false の場合悲惨なことになってしまうため、 is_int()is_double()false かつ筆者基準でバイナリでないものは強制的に文字列にキャストすることにした。

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() で引数を配列で指定できることを利用して連想配列に値をバインドさせている。

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

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() のようにこの部分だけオブジェクト指向型で書くことも可能である。