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

mod_proxy_balancerでサービス毎に分散の設定をする。

以下の様にVirtualHostとProxyを組み合わせた設定をhttpd.confに記述する。

NameVirtualHost *:80

<VirtualHost *:80>
        ServerName balance1
        ProxyRequests Off
        ProxyPass / balancer://mycluster/ lbmethod=byrequests timeout=1
        <Proxy balancer://mycluster/>
                BalancerMember http://192.168.0.91 loadfactor=1
                BalancerMember http://192.168.0.93 loadfactor=1
        </Proxy>
</VirtualHost>

<VirtualHost *:80>
        ServerName balance2
        ProxyRequests Off
        ProxyPass / balancer://mycluster/ lbmethod=byrequests timeout=1
        <Proxy balancer://mycluster/>
                BalancerMember http://192.168.0.91 loadfactor=1
        </Proxy>
</VirtualHost>

http://balance1/でアクセスすると91と93で分散され、
http://balance2/でアクセスすると91のみに転送される。

PEAR::Logで/var/log/test.logを作ったらSELinuxに警告された。

Warning: chmod(): Permission denied in /usr/share/pear/Log/file.php on line 225 Call Stack: 0.0005 323868 1. {main}() /var/www/html/pear_log_test.php:0 0.0031 486228 2. Log_file->log() /var/www/html/pear_log_test.php:7 0.0031 486228 3. Log_file->open() /usr/share/pear/Log/file.php:287 0.0035 486912 4. chmod() /usr/share/pear/Log/file.php:225 
こんな感じ。

色々SELinuxのポリシーをいじってみたものの解消出来なかったので、
今度ちゃんとSELinuxを勉強して、リベンジする時の為にメモ。

PHPの値渡し・参照渡し

<?php
	
echo '---------------------------<br>';
// 変数。
$a = 0;
var_dump($a); echo '<br>';

function write1($arg)
{
	$arg = 1;
}

write1($a);
var_dump($a); echo '<br>';

write1(&$a);
var_dump($a); echo '<br>';

echo '---------------------------<br>';
// 配列。

$b = array(0);
var_dump($b); echo '<br>';

function write2($arg)
{
	$arg[0] = 2;
}

write2($b);
var_dump($b); echo '<br>';

write2(&$b);
var_dump($b); echo '<br>';

echo '---------------------------<br>';
// オブジェクト。
class Hoge
{
	public $c = 0;
};

$hoge = new Hoge();
var_dump($hoge); echo '<br>';

function write3($arg)
{
	$arg->c = 3;
}

write3($hoge);
var_dump($hoge); echo '<br>';

write3(&$hoge);
var_dump($hoge); echo '<br>';

?>

---------------------------
int(0)
int(0)
int(1)

                                                    • -

array(1) { [0]=> int(0) }
array(1) { [0]=> int(0) }
array(1) { [0]=> int(2) }

                                                    • -

object(Hoge)#1 (1) { ["c"]=> int(0) }
object(Hoge)#1 (1) { ["c"]=> int(3) }
object(Hoge)#1 (1) { ["c"]=> int(3) }

まとめ。
変数を値渡し→値渡し。
配列を値渡し→値渡し。
オブジェクトを値渡し→実際は参照渡し。

ちなみに配列は実際は参照渡しだが、関数内で変更が入った時にコピーされるらしい。
screw-axis.com - このウェブサイトは販売用です! -  リソースおよび情報

PHP+MySQL5.5.24 utf8mb4奮闘記。

PHPからUTF8の4バイト文字を入れてたらなんかおかしな文字コードで入ってたのに気がついて四苦八苦したメモ。

環境は
CentOS6.2、
PHP5.3.3(インストールはyum)
MySQL5.5.24(インストールはyum、remiリポジトリから)、
MySQLサーバの文字コードはutf8mb4。

そのままINSERT。

$db = new mysqli($host, $user, $password, $db_name, $port);
$db->query("INSERT INTO mojitest VALUES ('a')");
$db->query("INSERT INTO mojitest VALUES ('あ')");
$db->query("INSERT INTO mojitest VALUES ('&#131083;')");
$db->close();

※3つ目のクエリは実際には「𠀋」が直接書かれている。
PHP文字コードはUTF8。

MySQLクライアントで文字コードを表示してみる。クライアントの文字コードもutf8mb4にしてある。

mysql> select hex(text), char_length(text), length(text) from mojitest;
+----------------------+-------------------+--------------+
| hex(text)            | char_length(text) | length(text) |
+----------------------+-------------------+--------------+
| 61                   |                 1 |            1 |
| C3A3C281E2809A       |                 3 |            7 |
| C3B0C2A0E282ACE280B9 |                 4 |           10 |
+----------------------+-------------------+--------------+

何この文字コード。見たことない。「あ」は「E3 81 82」だし、「𠀋」は「F0 A0 80 8B」のはず。
てか1文字表現するのに7バイトとか10バイトになってるよ!
3バイト文字すら謎の文字コードに置き換わってるってなんだ?
でもPHPでこのデータを拾ってきて表示しても、ちゃんと表示される。

SET NAMESを使う。

調べてみると、SET NAMESを使うサンプルがいっぱいあったのでやってみた。

$db = new mysqli($host, $user, $password, $db_name, $port);
$db->query('SET NAMES utf8mb4');
// 以下略
+----------------------+-------------------+--------------+
| hex(text)            | char_length(text) | length(text) |
+----------------------+-------------------+--------------+
| 61                   |                 1 |            1 |
| E38182               |                 1 |            3 |
| F0A0808B             |                 1 |            4 |
+----------------------+-------------------+--------------+

正しい!
でもSET NAMESはセキュリティ的にダメなんだとか。
http://rhiz.jp/id/171.html
SET NAMESは禁止 – yohgaki's blog

SET NAMESによって文字エンコーディングを変更するとC言語などで書かれたエスケープAPI (libmysql, libpqなど)が想定しているエンコーディングと実際のエンコーディングが異なる状況が発生します。この違いにより、環境によっては文字エンコーディングを利用したSQLインジェクション攻撃が可能になります。

つまり、APIによって文字エンコーディング情報を変更しないと接続情報が更新されず、エスケープAPIを利用していても正しくエスケープできない場合発生する、ということです。

SET NAMES, SET CLIENT ENCODINGを利用しないで、mysql_set_charset, pg_set_client_encodingを利用すれば、このような不整合が発生しないので問題も発生しなくなります。

なるほど。

mysqli::set_charset関数を使ってみる。

SET NAMESはセキュリティ的にダメとの事なので、mysqli::set_charset

$db = new mysqli($host, $user, $password, $db_name, $port);
$db->set_charset('utf8mb4');
// 以下略
+----------------------+-------------------+--------------+
| hex(text)            | char_length(text) | length(text) |
+----------------------+-------------------+--------------+
| 61                   |                 1 |            1 |
| C3A3C281E2809A       |                 3 |            7 |
| C3B0C2A0E282ACE280B9 |                 4 |           10 |
+----------------------+-------------------+--------------+

ダメだ。試しにutf8にしてみた。

$db = new mysqli($host, $user, $password, $db_name, $port);
$db->set_charset('utf8');
// 以下略
+----------------------+-------------------+--------------+
| hex(text)            | char_length(text) | length(text) |
+----------------------+-------------------+--------------+
| 61                   |                 1 |            1 |
| E38182               |                 1 |            3 |
| 3F3F3F3F             |                 4 |            4 |
+----------------------+-------------------+--------------+

あれ?変わった…。4バイト文字はもちろんダメだけど。

結論。skip-character-set-client-handshakeを設定する。

http://blog.cheki.net/archives/349

PHPはmy.cnfで[mysql]、[client]を設定しようがクライアントの文字コードはビルド時に指定されたキャラクタセット(通常latin1)。
〜中略〜
MySQLの4.1.15以降、5.0.13以降で「skip-character-set-client-handshake」というオプションが追加された。
クライアントからリクエストがあった場合、クライアントの文字コードをサーバの文字コードと同じものをセットする。

確かにcharacter_set_name関数の戻り値を表示すると「latin1」になる。
早速設定。

[mysqld]
...
character-set-server=utf8mb4
skip-character-set-client-handshake
mysqldを再起動して、イチバン最初のコードをそのまま実行してみる。

+----------------------+-------------------+--------------+
| hex(text)            | char_length(text) | length(text) |
+----------------------+-------------------+--------------+
| 61                   |                 1 |            1 |
| E38182               |                 1 |            3 |
| F0A0808B             |                 1 |            4 |
+----------------------+-------------------+--------------+

キター!!
長かった。

でもあのC3で始まる謎の文字コードはなんだったんだろうか。

PHPでUUID生成

なんかいろいろもやもやしている。

uuid-phpを使う

これなんのライブラリなんだろう?

yum -y install uuid-php

再起動。

service httpd restart

PHPソースコード

<?php
uuid_create(&$context);
uuid_make($context, UUID_MAKE_V1);

// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 形式。
uuid_export($context, UUID_FMT_STR, &$uuid);
echo $uuid . '<br>';

// バイト列。DBに入れたりするならこちらのがスリム。
uuid_export($context, UUID_FMT_BIN, &$uuid);
echo $uuid . '<br>'; // bin2hexすれば16進数の文字列にになる。

?>

Call-time pass-by-reference has been deprecated 
こういうエラーが出てしまうので、php.iniのallow_call_time_pass_referenceをOnにする。

再起動。

service httpd restart

どのバージョンを使うかはわかりやすいが、記法的に非推奨らしいので出来れば使いたくない…。

pecl uuid extensionを使う

事前に必要なものをインストールする。

yum -y install php-devel # for phpize
yum -y install gcc
yum -y install make
yum -y install libuuid-devel # for uuid.h

インストール。

pecl install uuid

php.iniに以下の行を追加する。

extension=uuid.so

再起動。

service httpd restart

PHPソースコード

<?php
echo uuid_create(UUID_TYPE_TIME) . '<br>';	// v1(Mac Address)っぽい
echo uuid_create(UUID_TYPE_RANDOM) . '<br>';	// v4(Random)っぽい
?>

使い方は簡単なんだが、いまいちタイプがどのバージョンを示すかわかりにくい。
出力された値を見てバージョンの判別は可能。

Universally unique identifier - Wikipedia

The variant indicates the layout of the UUID. The UUID specification covers one particular variant. Other variants are reserved or exist for backward compatibility reasons (e.g. for values assigned before the UUID specification was produced). An example of a UUID that is a different variant is the nil UUID, which is a UUID that has all 128 bits set to zero.

In the canonical representation, xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx, the most significant bits of N indicates the variant (depending on the variant; one, two or three bits are used). The variant covered by the UUID specification is indicated by the two most significant bits of N being 1 0 (i.e. the hexadecimal N will always be 8, 9, a, or b).

In the variant covered by the UUID specification, there are five versions. For this variant, the four bits of M indicates the UUID version (i.e. the hexadecimal M will either be 1, 2, 3, 4, or 5).

TortoiseGitでgithubのリポジトリをダウンロードしてみた。

githubリポジトリを作っておく。

アカウント取って適当にリポジトリを作っておく。
とりあえすtestリポジトリを作った。

githubSSH秘密鍵を登録する。

Puttygenで鍵生成。

TortoiseGitをインストールしたらPuttygenも入ってた。
鍵を作る。
http://www.kuins.kyoto-u.ac.jp/news/47/putty-gen.html


Generateを押してマウスをもそもそ動かし、鍵生成。
Save public keyで公開鍵保存。github.pubとしといた。でもこれ使わない。
Save private keyで秘密鍵保存。github.ppkとしといた。
青枠で囲った所をコピーしておく。

githubのアカウントに公開鍵を設定。

windowsでgitを使う - Takazudo hamalog
github > Account Settings > SSH Keys > Add SSH Key

緑枠に、さっきコピーした公開鍵の文字列をペースト。
Add key。

リポジトリを落としてくる。

Git Clone

エクスプローラを開いて、適当なディレクトリでGit Clone。
したらなんか↓が出た。

もっかいGit Clone


黄色枠に、先に保存した秘密鍵を指定。
ピンク枠に、githubのtestリポジトリのページ↓の赤枠の部分をコピペ。

OK。
これでリポジトリがチェックアウトされたはず。