HHeLiBeXの日記 正道編

日々の記憶の記録とメモ‥

issetの罠(emptyの罠でもある)

PHPで(知らずに)以下のようなコードを書いていてはまったのでメモ。
実にくだらない話なんだが‥

<?php
$a = array(
    'hoge' => 'HOGEHOGE',
    'uga' => array(
        'text' => 'UGAUGA',
        'shortText' => 'UGA',
    ),
);

var_dump(PHP_VERSION);
$textList = array();
foreach (array('hoge', 'uga') as $key) {
    if (isset($a[$key]['text'])) {
        $textList[$key] = $a[$key]['text'];
    } else if (isset($a[$key])) {
        $textList[$key] = $a[$key];
    } else {
        $textList[$key] = $key;
    }
}
var_dump($textList);

ちなみに、実際のコードでは、Zend FrameworkでZend_Config_Iniを使って.iniファイルから読み込んだデータをPHPの配列に(toArray()で)変換している。それが上記コードの配列「$a」。
で、期待に胸を躍らせて(違)実行するわけだが‥

string(5) "5.3.3"
array(2) {
  ["hoge"]=>
  string(1) "H"
  ["uga"]=>
  string(6) "UGAUGA"
}

何かがおかしい‥「"hoge"」に対する値が「"H"」ってなんだ‥

というわけでマニュアルを見ると、書いてある‥


Example #2 isset() on String Offsets

PHP 5.4 changes how isset() behaves when passed string offsets.

<?php
$expected_array_got_string = 'somestring';
var_dump(isset($expected_array_got_string['some_key']));
var_dump(isset($expected_array_got_string[0]));
var_dump(isset($expected_array_got_string['0']));
var_dump(isset($expected_array_got_string[0.5]));
var_dump(isset($expected_array_got_string['0.5']));
var_dump(isset($expected_array_got_string['0 Mostel']));
?>

Output of the above example in PHP 5.3:

bool(true)
bool(true)
bool(true)
bool(true)
bool(true)
bool(true)

Output of the above example in PHP 5.4:

bool(false)
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)

つまり、「$a['hoge']」がstringなので、PHP 5.3までは「$a['hoge']['text']」は「$a['hoge'][0]」と等価とみなされ、文字列"HOGEHOGE"の先頭の文字が返されるというわけである。実に傍迷惑である。
まぁ、「$a['hoge']」が配列だと期待して書くコードの場合、チェックするキーが「'0'」だとPHP 5.4以降も数値の「0」と等価とみなされるので、その可能性を確実に排除するためには以下のようなコードにしておくのが安全。さらに、まず「$a['hoge']」が配列かどうかをチェックする際に「$aが配列でかつキー『'hoge'』が存在するか」というチェックもしておかないとNoticeが出る可能性があるので、以下のような感じになる。

<?php
$a = array(
    'hoge' => 'HOGEHOGE',
    'uga' => array(
        'text' => 'UGAUGA',
        'shortText' => 'UGA',
    ),
);

var_dump(PHP_VERSION);
$textList = array();
foreach (array('hoge', 'uga') as $key) {
    if (is_array($a) && isset($a[$key])) {
        if (is_array($a[$key]) && isset($a[$key]['text'])) {
            $textList[$key] = $a[$key]['text'];
        } else {
            $textList[$key] = $a[$key];
        }
    } else {
        $textList[$key] = $key;
    }
}
var_dump($textList);

これで期待する結果が得られる。

string(5) "5.3.3"
array(2) {
  ["hoge"]=>
  string(8) "HOGEHOGE"
  ["uga"]=>
  string(6) "UGAUGA"
}

ついでに言うと、emptyにも同じ罠が仕掛けられている。

なお、issetの代わりにarray_key_existsを使うという方法もあるが‥

$textList = array();
foreach (array('hoge', 'uga') as $key) {
    if (is_array($a) && array_key_exists($key, $a)) {
        if (is_array($a[$key]) && array_key_exists('text', $a[$key])) {
            $textList[$key] = $a[$key]['text'];
        } else {
            $textList[$key] = $a[$key];
        }
    } else {
        $textList[$key] = $key;
    }
}
var_dump($textList);

結局は配列かどうかのチェックをしないとNoticeが出るので、よほどの事情(値がNULLの場合の評価結果を真としたい等)がない限り、大して変わらない。

まぁ、Zend Frameworkを使っているなら、Zend_Config_Iniオブジェクトをそのまま使えばいいじゃんって言われたら身も蓋もない‥

しかし、ガチのシステム開発スクリプト言語を使えば使うほどスクリプト言語が嫌いになっていくのは、データ型の曖昧性の許容が(自分の中での)主要因のような気がする‥