【PHP】PDOのプリペアドステートメントでバインドを柔軟に行う自作関数

#はじめに
PDOでプリペアドステートメントを使う際、

- PDOStatement::execute で すべての値が PDO::PARAM_STR として扱われるのを避けたいが、いちいちbindValueやbindParamを使うのは面倒。
- 疑問符パラメータだと対応がわかりにくいので名前付きパラメータを使いたいが、パラメータの数が不定の句が混じると全体を疑問符パラメータにせざるを得ないのが不満。
- パラメータを配列にまとめるのが面倒。

と感じたので、その辺りをうまいことやってくれる関数を作ってみました。

**ただし、前付きパラメータと疑問符パラメータを混在させると[PDO::prepare](http://php.net/manual/ja/pdo.prepare.php)で「名前と疑問符パラメータを同一 SQL ステートメント中で使用することはできません。」とあるのを無視することになりますので、混在させる場合は自己責任でお願いします（混在させない場合は問題ありません）。**

使用したPHPのバージョン: 7.1.12

#作成した関数

- パラメータのデータ型に応じて PDO::PARAM_* 定数 が選択されます。
- パラメータはスカラ・配列いずれの形でも渡すことができます。
- パラメータは可変長引数で渡すことができます。

```php:PHP
// 引数
//   $dbh: PDOインスタンス
//   $sql: SQL文（バインドパラメータを含めることが可能）
//   $params_array:  SQL文の中のバインドパラメータに代入する値またはその配列（可変長引数）
// 返り値
//   PDOStatementオブジェクト
function execute($dbh, $sql, ...$params_array) {
	$stmt = $dbh->prepare($sql);

	// ----- パラメータを一つの配列に統合 ----- //
	$integrated_params = array();
	$index = 1;

	foreach ($params_array as $params) {
		// $paramsがスカラまたはnullの時は配列に変換
		if (is_scalar($params) || $params === null) {
			$params = array($params);
		}

		foreach ($params as $param_id => $value) {
			// 数値添字のときは疑問符パラメータとみなす
			if (gettype($param_id) == 'integer') {
				$integrated_params[$index] = $value; // 疑問符パラメータ
			} else {
				$integrated_params[$param_id] = $value; // 名前付きパラメータ
			}

			$index++;
		}
	}

	// ----- データ型に応じてバインド ----- //
	foreach ($integrated_params as $param_id => $value) {
		switch (gettype($value)) {
			case 'boolean':
				$param_type = PDO::PARAM_BOOL;
				break;

			case 'integer':
				$param_type = PDO::PARAM_INT;
				break;

			case 'double':
				$param_type = PDO::PARAM_STR;
				break;

			case 'string':
				$param_type = PDO::PARAM_STR;
				break;

			case 'NULL':
				$param_type = PDO::PARAM_NULL;
				break;

			default:
				$param_type = PDO::PARAM_STR;
		}

		$stmt->bindValue($param_id, $value, $param_type);
	}

	$stmt->execute();

	return $stmt;
}
```

注意点

- パラメータの数値が文字列で与えられた場合は PDO::PARAM_STR でバインドされるので、 PDO::PARAM_INT でバインドしたい場合は事前に整数へキャストする必要があります。
- 疑問符パラメータは名前付きパラメータも含めて順番通りに渡す必要があります。
- PDOの[クラス定数](http://php.net/manual/ja/pdo.constants.php)で小数を表すデータ型は定められていないので double には PDO::PARAM_STR を当てています。
- プリペアドステートメントで疑問符パラメータと名前付きパラメータを混在させたときにパラメータがどう扱われるかについては[こちら](https://qiita.com/Kunikata/items/979b23ce4b9b1302d2d0)を参照してください。

#メモリ上にテスト用のSQLiteデータベースを作成
使用例では以下のデータベースを用いています。

テーブル名: result

| ID | name | gender | score |
|--:|:--|:--|:--|
| 1 | Alice | female | 7.5 |
| 2 | Bob | male | 5.4 |
| 3 | Cindy | female | 9.1 |
| 4 | Danny | male | 8.2 |
| 5 | Edward | male | 4.5 |

```php:PHP
$dbh = new PDO('sqlite::memory:', '', '', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE =>PDO::FETCH_ASSOC
]);

$dbh->exec("
	CREATE TABLE result (
	    id INTEGER PRIMARY KEY AUTOINCREMENT,
	    name TEXT,
	    gender TEXT,
		score REAL
	);

	INSERT INTO result (name, gender, score) VALUES ('Alice', 'female', 7.5);
	INSERT INTO result (name, gender, score) VALUES ('Bob', 'male', 5.4);
	INSERT INTO result (name, gender, score) VALUES ('Cindy', 'female', 9.1);
	INSERT INTO result (name, gender, score) VALUES ('Danny', 'male', 8.2);
	INSERT INTO result (name, gender, score) VALUES ('Edward', 'male', 4.5);
");
```

#使用例

###パラメータなし

```php:PHP
$sql = "SELECT name FROM result";
$stmt = execute($dbh, $sql);
print_r($stmt->fetchAll());
```

結果

```
Array
(
    [0] => Array
        (
            [name] => Alice
        )

    [1] => Array
        (
            [name] => Bob
        )

    [2] => Array
        (
            [name] => Cindy
        )

    [3] => Array
        (
            [name] => Danny
        )

    [4] => Array
        (
            [name] => Edward
        )

)
```

###疑問符パラメータ

```php:PHP
$sql = "
	SELECT name, gender, score
	FROM result
	WHERE name = ? OR name = ?
";

$params = array('Alice', 'Bob');
$stmt = execute($dbh, $sql, $params);
print_r($stmt->fetchAll());
```

結果

```
Array
(
    [0] => Array
        (
            [name] => Alice
            [gender] => female
            [score] => 7.5
        )

    [1] => Array
        (
            [name] => Bob
            [gender] => male
            [score] => 5.4
        )

)
```

わざわざ配列を作らずに、以下のようにしても同じ結果が得られます。

```php
$stmt = execute($dbh, $sql, 'Alice', 'Bob');
```

###名前付きパラメータ

```php:PHP
$sql = "
	SELECT name, gender, score
	FROM result
	WHERE name = :name1 OR name = :name2
";

$params = array(
	'name1' => 'Cindy',
	'name2' => 'Danny'
);
$stmt = execute($dbh, $sql, $params);
print_r($stmt->fetchAll());
```

結果

```
Array
(
    [0] => Array
        (
            [name] => Cindy
            [gender] => female
            [score] => 9.1
        )

    [1] => Array
        (
            [name] => Danny
            [gender] => male
            [score] => 8.2
        )

)
```

###疑問符パラメータと名前付きパラメータの混在
一応できるようにしていますが、内部仕様に依存するのでどちらかにした方が無難です。

```php:PHP
$sql = "
	SELECT name, gender, score
	FROM result
	WHERE
		gender = :gender
		AND name IN (?, ?, ?)
		AND (score > :score_lower AND score < :score_upper)
";

$params1 = array('gender' => 'female');
$params2 = array('Alice', 'Bob', 'Cindy');
$params3 = array('score_lower' => 5.0, 'score_upper' => 8.0);
$stmt = execute($dbh, $sql, $params1, $params2, $params3);
print_r($stmt->fetchAll());
```

結果

```
Array
(
    [0] => Array
        (
            [name] => Alice
            [gender] => female
            [score] => 7.5
        )

)
```

###パラメータ数が不定の場合
クエリを動的に作成するような時にパラメータの数が不定の場合は、以下のような渡し方ができます。
もちろん一つの配列にまとめて渡してもOKです。

```php:PHP
$params_array = array();
$params_array[] = $params1;
$params_array[] = $params2;
$params_array[] = $params3;

$stmt = execute($dbh, $sql, ...$params_array);
```