PHP

PHPのissetの罠

More than 5 years have passed since last update.

これはADVENTARのPHP Advent Calendar17日目のエントリです。

早速ですが、このコードのvar_dumpは何を出力すると思いますか?

$hoge = "fuga";

var_dump(isset($hoge["fuga"]));

実行してみましょう

$ php -r '$hoge="fuga";var_dump(isset($hoge["fuga"]));'

bool(true)

trueです。なぜだか分かりますか?僕は最初このphpの処理系が茶目っ気を出してふざけてるのかと思いました。

一度issetを外してみましょう

$ php -r '$hoge="fuga";var_dump($hoge["fuga"]);'

string(1) "f"

fが返りました。このfはなんでしょう?

実は$hogeに格納された文字列の1文字目です。

つまり、$hoge["fuga"]"fuga"が数値にキャストされ0になり、$hoge[0]が評価され文字列の1文字目が取り出されたわけです。で、最初の話に戻ってisset($hoge["fuga"])trueになったわけです。PHPの豆腐のような柔軟性にはほとほと困ったものです。

ですが、流石にPHPの中の人も「流石にねーわ」と思ったのか、PHP5.4.0からは挙動が変わりました。

Changelog

Version Description
5.4.0 Checking non-numeric offsets of strings now returns FALSE.

http://php.net/isset#refsect1-function.isset-changelog

5.4.0から文字列入った変数のnon-numericなoffsetsのチェックはfalseを返すよ、って感じですね。

ちなみにさっきの実行したphpは5.3でした。

$ php -v

PHP 5.3.26 (cli) (built: Jul 7 2013 19:05:08)
Copyright (c) 1997-2013 The PHP Group
Zend Engine v2.3.0, Copyright (c) 1998-2013 Zend Technologies

では5.4で実行してみましょう。

$ phpenv global 5.4snapshot

$ php -v
PHP 5.4.24-dev (cli) (built: Dec 11 2013 01:49:21)
Copyright (c) 1997-2013 The PHP Group
Zend Engine v2.4.0, Copyright (c) 1998-2013 Zend Technologies
with Xdebug v2.2.1, Copyright (c) 2002-2012, by Derick Rethans
$ php -r '$hoge="fuga";var_dump(isset($hoge["fuga"]));'
bool(false)

確かにfalseが返りますね。ちなみにほぼこれやるためだけにphpenv入れました。

ここで一つ疑問に思ったんですが、"non-numeric"ってどこまでがnumericでどこからnon-numericなんでしょう?

PHPにはis_numericという関数がありますし、それに従う気もします。

というわけでis_numericと合わせて試してみました。versionはさっきと同じ5.4.24です。

$ php -r '$hoge="fuga";

$numerics = array(0, "0", 0x0, "0x0", 0.0, "0.0", 0.5, "0.5", 01, "01", 0b0, "0b0", 1e0, "1e0", false, null, array());
foreach ($numerics as $numeric) {
var_dump($numeric, isset($hoge[$numeric]), is_numeric($numeric));
echo "\n";
}'

このスクリプトを実行した結果を表にしたのが以下です。

$numeric
isset($hoge[$numeric])
is_numeric($numeric)

0
true
true

"0"
true
true

0x0
true
true

"0x0"
true
true

0.0
true
true

"0.0"
false
true

0.5
true
true

"0.5"
false
true

01
true
true

"01"
true
true

0b0
true
true

"0b0"
false
false

1e0
true
true

"1e0"
false
true

false
true
false

null
true
false

array()
false
false

一貫性がなさすぎてびっくりですね...

この結果をまとめると


  • $hogeの文字列の長さの範囲の)数値を渡した場合はisset, is_numericともにtrueになる



    • 0.5issettrueになるのが気に食わない

    • ちなみに$hoge[0.5]を使おうとするとNoticeが出る




  • nullfalseis_numericfalseのくせにissettrue


  • array()は当然どちらもfalse

  • 文字列のis_numeric"0b0"false、それ以外はtrue



    • 0bで始まるのは2進数の整数リテラル


    • is_numericは本題ではないが"0x0"とかtrueになるのに"0b0"falseになるのが気に食わない(バグ?)



  • 文字列のisset"0", "01", "0x0"true、他はfalse



    • "0b0""1e0"false"0x0"がtrueなのが気に食わない。



この実験から分かることは、そもそも文字列にisset使うな、という話ですね。issetを使う時は配列を相手にすることが多いと思うので、そういう場合はis_arrayで配列であることを確認してからissetを使うのが良さそうです。

ちなみにphp5.3で(0b0を抜いて)実行した場合、issetの列は全てtrueになりました。