PDOでフェッチした数値型カラムの値が文字列で取得されるのでなんとかしようと頑張った。

MySQLから取得したデータをjson_encodeしてて気づいた。
いくら型無し言語でもコレはないわー。

現象

desc hoge;
+-------+------------------+------+-----+---------+-------+
| Field | Type             | Null | Key | Default | Extra |
+-------+------------------+------+-----+---------+-------+
| id    | int(10) unsigned | NO   |     | 0       |       |
| name  | char(8)          | NO   |     |         |       |
+-------+------------------+------+-----+---------+-------+

SELECT id, name FROM hoge;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | えりお    |
+----+-----------+

こういうテーブルのレコードをPDOStatement::fetchAllして中身を表示する。

$statement = $pdo->prepare('SELECT id, name FROM hoge');
$statement->execute();
$result = $statement->fetchAll());
foreach ($result as $row)
{
  var_dump($row);
  echo '<br>';
  var_dump(json_encode($row));
  echo '<br>';
}

こう出力される。

array(2) { ["id"]=> string(1) "1" ["name"]=> string(9) "えりお" } 
string(38) "{"id":"1","name":"\u3048\u308a\u304a"}"
idがstring・・・ぐぬぬ

解決編だけを見たい人は一番下へ。

PDO::ATTR_STRINGIFY_FETCHES

http://php.net/manual/ja/pdo.setattribute.php

フェッチする際、数値を文字列に変換する。bool を必要とする

これだ!

php - How to get numeric types from MySQL using PDO? - Stack Overflow
PDOのMySQLドライバでは対応していないらしい。
実際FALSEにしても結果は変わらなかった。ぐぬぬ

ちなみにmysqliを使っても同じだった。

これをどうにかするにはもうこちらで頑張って
PDOStatement::bindColumnを使って整数である事を明示するしかなさそう。
せめてひとつひとつカラムと型を指定しなくても済むようにしたい…。

PDOStatement::getColumnMeta

http://www.php.net/manual/ja/pdostatement.getcolumnmeta.php
便利そうなの発見。

pdo_type PDO::PARAM_* 定数によって表現されるカラムの型

こんなのあるじゃん!

for ($i = 0; $i < $statement->columncount(); ++$i)
{
  var_dump($statement->getColumnMeta($i));
  echo '<br>';
}

array(7) { ["native_type"]=> string(4) "LONG" ["flags"]=> array(1) { [0]=> string(8) "not_null" } ["table"]=> string(4) "hoge" ["name"]=> string(2) "id" ["len"]=> int(10) ["precision"]=> int(0) ["pdo_type"]=> int(2) } 
array(7) { ["native_type"]=> string(6) "STRING" ["flags"]=> array(1) { [0]=> string(8) "not_null" } ["table"]=> string(4) "hoge" ["name"]=> string(4) "name" ["len"]=> int(40) ["precision"]=> int(0) ["pdo_type"]=> int(2) }
pdo_typeはどの型でも2(PDO::PARAM_STR)だ…orz

仕方ないのでnative_typeを使う事にする。

native_type カラム値を表現するために使用される PHP のネイティブ型

ネイティブ型にはどんなのがあるのか、PHPのCのソースを直接調べた。

native_typeでgrepして、mysql_statement.cのtype_to_name_native関数までたどり着いた。

static char *type_to_name_native(int type) /* {{{ */
{
#define PDO_MYSQL_NATIVE_TYPE_NAME(x)	case FIELD_TYPE_##x: return #x;

    switch (type) {
        PDO_MYSQL_NATIVE_TYPE_NAME(STRING)
        PDO_MYSQL_NATIVE_TYPE_NAME(VAR_STRING)
#ifdef MYSQL_HAS_TINY
        PDO_MYSQL_NATIVE_TYPE_NAME(TINY)
#endif
        PDO_MYSQL_NATIVE_TYPE_NAME(SHORT)
        PDO_MYSQL_NATIVE_TYPE_NAME(LONG)
        PDO_MYSQL_NATIVE_TYPE_NAME(LONGLONG)
        PDO_MYSQL_NATIVE_TYPE_NAME(INT24)
        PDO_MYSQL_NATIVE_TYPE_NAME(FLOAT)
        PDO_MYSQL_NATIVE_TYPE_NAME(DOUBLE)
        PDO_MYSQL_NATIVE_TYPE_NAME(DECIMAL)
#ifdef FIELD_TYPE_NEWDECIMAL
        PDO_MYSQL_NATIVE_TYPE_NAME(NEWDECIMAL)
#endif
#ifdef FIELD_TYPE_GEOMETRY
        PDO_MYSQL_NATIVE_TYPE_NAME(GEOMETRY)
#endif
        PDO_MYSQL_NATIVE_TYPE_NAME(TIMESTAMP)
#ifdef MYSQL_HAS_YEAR
        PDO_MYSQL_NATIVE_TYPE_NAME(YEAR)
#endif
        PDO_MYSQL_NATIVE_TYPE_NAME(SET)
        PDO_MYSQL_NATIVE_TYPE_NAME(ENUM)
        PDO_MYSQL_NATIVE_TYPE_NAME(DATE)
#ifdef FIELD_TYPE_NEWDATE
        PDO_MYSQL_NATIVE_TYPE_NAME(NEWDATE)
#endif
        PDO_MYSQL_NATIVE_TYPE_NAME(TIME)
        PDO_MYSQL_NATIVE_TYPE_NAME(DATETIME)
        PDO_MYSQL_NATIVE_TYPE_NAME(TINY_BLOB)
        PDO_MYSQL_NATIVE_TYPE_NAME(MEDIUM_BLOB)
        PDO_MYSQL_NATIVE_TYPE_NAME(LONG_BLOB)
        PDO_MYSQL_NATIVE_TYPE_NAME(BLOB)
        PDO_MYSQL_NATIVE_TYPE_NAME(NULL)
        default:
            return NULL;
    }
#undef PDO_MYSQL_NATIVE_TYPE_NAME
} /* }}} */

FIELD_TYPE_XXX とゆーのは、MySQLのCのソースに定義されている模様。
↓と一緒みたい。
http://dev.mysql.com/doc//refman/4.1/ja/c-api-datatypes.html

ではこれを基にbindColumnしよう。

TINYINTには注意!!

yumでインストールしたPHPMySQLドライバでは、先述のCのソースのコンパイルスイッチ「MYSQL_HAS_TINY」がOFFらしく、
getColumnMetaの戻り値にnative_typeが入ってないくさい。

idをTINYINTにしてみたらこうなった。

array(6) { ["flags"]=> array(1) { [0]=> string(8) "not_null" } ["table"]=> string(4) "hoge" ["name"]=> string(2) "id" ["len"]=> int(3) ["precision"]=> int(0) ["pdo_type"]=> int(2) } 
native_typeが無い。

再びmysql_statement.cの、pdo_mysql_stmt_col_meta関数では以下の様になっている。

static int pdo_mysql_stmt_col_meta(pdo_stmt_t *stmt, long colno, zval *return_value TSRMLS_DC) /* {{{ */
{
	pdo_mysql_stmt *S = (pdo_mysql_stmt*)stmt->driver_data;
	const MYSQL_FIELD *F;
	zval *flags;
	char *str;	
中略
	str = type_to_name_native(F->type);
	if (str) {
		add_assoc_string(return_value, "native_type", str, 1);
	}
中略
}

type_to_name_nativeがNULLを返した場合、native_typeが追加されない様になっている。
ムキィー!

更にその下を見ると

#ifdef PDO_USE_MYSQLND
	switch (F->type) {
		case MYSQL_TYPE_BIT:
		case MYSQL_TYPE_YEAR:
		case MYSQL_TYPE_TINY:
		case MYSQL_TYPE_SHORT:
		case MYSQL_TYPE_INT24:
		case MYSQL_TYPE_LONG:
#if SIZEOF_LONG==8
		case MYSQL_TYPE_LONGLONG:
#endif
			add_assoc_long(return_value, "pdo_type", PDO_PARAM_INT);
			break;
		default:
			add_assoc_long(return_value, "pdo_type", PDO_PARAM_STR);
			break;
	}
#endif

とか書いてあるけど、なるほどmysqlndを使う設定でビルドしないとこのコードはコンパイルされないのだね。
どこかでデフォルト値としてPDO_PARAM_STRがpdo_typeに入れられているのだろう。

bindColumnの罠…

コンパイルしなおしたりするのはちょっと遠慮したいので、
おとなしく1行ずつフェッチして、native_typeを見てbindColumnする事に。
TINYINTの場合は、native_typeが無かったらとりあえずPDO::PARAM_INTでバインドする事で対応。
他のNULLが帰ってくるケースがあったらあわわだけど…。

$statement = $pdo->prepare('SELECT id, name FROM hoge LIMIT 1');
$statement->execute();
$row = array();
for ($i = 0; $i < $statement->columnCount(); ++$i)
{
	$column = $statement->getColumnMeta($i);
	$pdo_type = PDO::PARAM_INT;
	if (isset($column['native_type']) == TRUE)
	{
		switch ($column['native_type'])
		{
			case 'TINY':
			case 'SHORT':
			case 'INT24':
			case 'LONG':
			case 'LONGLONG':
				$pdo_type = PDO::PARAM_INT;
				break;
			default:
				$pdo_type = PDO::PARAM_STR;
		}
	}
	$row[$column['name']] = NULL;
	$statement->bindColumn($column['name'], $row[$column['name']], $pdo_type);
}
while ($statement->fetch(PDO::FETCH_BOUND))
{
	var_dump($row);
	echo '<br>';
	var_dump(json_encode($row));
	echo '<br>';
}

array(2) { ["id"]=> &int(1) ["name"]=> &string(9) "えりお" } 
string(36) "{"id":1,"name":"\u3048\u308a\u304a"}"
Mission Complete。
直接var_dumpした時の型が参照になってるのが気になる…これもしかして…。
>|php|while ($statement->fetch(PDO::FETCH_BOUND))
{
$result[] = $row;
}
var_dump($result);
|

ってやったらまずいんじゃ…。
なかやまっていうデータを入れてやってみたら、

array(2) { [0]=> array(2) { ["id"]=> &int(1) ["name"]=> &string(12) "なかやま" } [1]=> array(2) { ["id"]=> &int(1) ["name"]=> &string(12) "なかやま" } }
OH…GOD…

解決編

あっさり解決した。php-mysqlndを入れたら全て目的の動作になった。
コードは全くいじってない。

yum --enablerepo=remi -y php-mysqlnd
競合してインストール出来ない場合もあるので、その時はphp-mysqlをremoveする。

$statement = $pdo->prepare('SELECT id, name FROM hoge');
$statement->execute();
while ($row = $statement->fetch())
{
	var_dump($row);
	echo '<br>';
	var_dump(json_encode($row));
	echo '<br>';
	$result[] = $row;
}
echo '<br>';
var_dump($result);

array(2) { ["id"]=> int(1) ["name"]=> string(9) "えりお" } 
string(36) "{"id":1,"name":"\u3048\u308a\u304a"}"
array(2) { ["id"]=> int(1) ["name"]=> string(12) "なかやま" }
string(42) "{"id":1,"name":"\u306a\u304b\u3084\u307e"}"

array(2) { [0]=> array(2) { ["id"]=> int(1) ["name"]=> string(9) "えりお" } [1]=> array(2) { ["id"]=> int(1) ["name"]=> string(12) "なかやま" } }

治った。

終わりorz