【PHP】SimpleXMLオブジェクトの不思議な挙動

PHPのSimpleXMLオブジェクトに対してエラー処理をしようとしたところ、改めてこのオブジェクトの仕組みが分からなくなり、理解するのにだいぶかかった。「あれっ」となるところがあり一筋縄ではいかなかったので、まとめておきます。
※ 以下のコードの実行環境は、PHP 7.4 です

# 存在しないプロパティを参照すると「空のSimpleXMLオブジェクト」が返る

次の処理は、XMLから、ユーザ名を取得し配列にしています。

```PHP
<?php
$xmlstr = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<users>
    <user>
        <code>001</code>
        <name>Yamada</name>
    </user>
    <user>
        <code>002</code>
        <name>Tanaka</name>
    </user>
    <user>
        <code>003</code>
        <name>Sato</name>
    </user>
</users>
XML;

$xml = new SimpleXMLElement($xmlstr);

$user_names = [];
foreach ($xml->user as $user) {
    $user_names[] = (string)$user->name;
}
print_r($user_names); // Array ( [0] => Yamada [1] => Tanaka [2] => Sato ) 
```

SimpleXMLオブジェクトの存在しないプロパティ（XMLにタグがない）を参照してみると
`空のSimpleXMLオブジェクト`が戻り値になることが分かります。
通常のオブジェクトは、Noticeエラーが発生しますが、SimpleXMLオブジェクトではそれが出ない。

```PHP
print_r($xml->hoge); // SimpleXMLElement Object ( ) 

$std = new stdClass();
print_r($std->hoge); // Notice: Undefined property: stdClass::$hoge
```

存在しないプロパティを検証した時は、次のようになりました。

|   |isset |!empty|!is_null|
|:-:|:-:|:-:|:-:|
| SimpleXMLElement  | false  | false | true  |
|  stdClass | false  | false  |  false |

_※ stdClass の、`!is_null` では、「Notice: Undefined property: stdClass::$hoge」とエラーが出ます_

`isset`は、プロパティを直接見に行き、存在しない（宣言されていない）ので、`false`になる。
`!empty`も、`isset`と同じく、プロパティを直接見に行き、空と見なすということだと考えられます。
`!is_null`は、`is_null()`を実行するとき、存在しないプロパティへの参照が発生し、
`空のSimpleXMLオブジェクト`が返されるため、`null`ではないと判断されるようです。
`stdClass`は、存在しない値で、`null`が返るため、逆の結果になります。
関数は参照が発生するので、発生しない言語構造の`isset`、`!empty` とは動きが
違ってくるということでしょう。

##「空のSimpleXMLオブジェクト」は、オブジェクトだけど空
SimpleXMLオブジェクト と stdClass の比較では、空の時の検証結果が変わってきます。
`空のSimpleXMLオブジェクト`は、空とみなされるが、
`空のstdClassオブジェクト`は、空とみなされない。

|   |isset |!empty|!is_null|
|:-:|:-:|:-:|:-:|
| 空のSimpleXMLオブジェクト  | true  | false  | true  |
|  空のstdClass | true  | true  |  true |

配列と同じように`空のSimpleXMLオブジェクト`は、`empty` で空とみなされます。
この辺りは、マニュアルにも記載がありました。
`empty`は、「booleanへの変換」で結果を判断しており、
false（空）とみなす値に`空のSimpleXMLオブジェクト`も入っています。

> 属性がない空要素から作成された SimpleXML オブジェクト。つまり、子要素も属性もない要素です。  

https://www.php.net/manual/ja/language.types.boolean.php#language.types.boolean.casting


##「空のSimpleXMLオブジェクト」にみえるけどデータがあることがある

以下のようにXML名前空間が要素に付いているという時は、注意が必要です

```PHP
<?php
$xmlstr = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<hoge:users xmlns:hoge="http://mokkii-hoge.org/2021/">
    <hoge:user>
        <hoge:code>001</hoge:code>
        <hoge:name>Yamada</hoge:name>
    </hoge:user>
    <hoge:user>
        <hoge:code>002</hoge:code>
        <hoge:name>Tanaka</hoge:name>
    </hoge:user>
    <hoge:user>
        <hoge:code>003</hoge:code>
        <hoge:name>Sato</hoge:name>
    </hoge:user>
</hoge:users>
XML;

$xml = new SimpleXMLElement($xmlstr);

print_r($xml); // SimpleXMLElement Object ( )
var_export(isset($xml)); // true
var_export(!empty($xml)); // false
var_export(!is_null($xml)); // true

$user_names = [];
foreach ($xml->children('hoge', true) as $user) {
    $user_names[] = (string)$user->name;
}
print_r($user_names); // Array ( [0] => Yamada [1] => Tanaka [2] => Sato ) 
```

中身があるはずなのですが、`print_r()` で中身を出力してみると、
みかけは `空のSimpleXMLオブジェクト`と同じになります。
別のXML名前空間にデータが存在していても、プリフィクスなしの
デフォルトの名前空間にデータが無いと空と見なすようです。
もちろん、`children()`を実行し、名前空間を指定すれば、データを持ってこれます。

## SimpleXMLオブジェクト、難しい

SimpleXMLオブジェクトは、かなり独特な動きをするように思いました。
普段のPHPを使っている時の動きとは異なる点があり、
もしかしたら、エラーが出にくいように配慮されているのかもしれないですが、
値の検証などをやりだすとなかなか難しいです。
ある程度、特殊なものとして認識して使うのがいいように思います。