SimpleXMLで名前空間に属する要素を出力

<?xml version="1.0"?>
<services xmlns:nequal="http://nequal.jp/schema">
    <nequal:service id="openpear">
        <nequal:name>Openpear</nequal:name>
        <nequal:uri>http://openpear.org/</nequal:uri>
    </nequal:service>
    <nequal:service id="deadlinetimer">
        <nequal:name>DEADLINETIMER ジェネレーター</nequal:name>
        <nequal:uri>http://deadlinetimer.com/</nequal:uri>
    </nequal:service>
</services>

こんな感じで名前空間に属した要素を出力したいんだけど、SimpleXMLでどうしたらいいのかよくわからないということで調べてみました。

とりあえずnameとuriプロパティを持つNequal\Serviceクラスがあるものとします。

<?php

use Nequal\Service;

// rootとなる要素に必ず名前空間を指定する
$xml = new \SimpleXMLElement('<services xmlns:nequal="http://nequal.jp/schema"></services>');

$services = array(
    new Service('Openpear', 'http://openpear.org/'),
    new Service('DEADLINETIMER ジェネレーター', 'http://deadlinetimer.com/'),
);

foreach ($services as $service) {
    // addChild()メソッドの第3引数に、同じ名前空間を指定する
    $element = $xml->addChild('service', null, 'http://nequal.jp/schema');
    $element->addChild('name', $service->getName());
    $element->addChild('uri', $service->getUri());
}

echo $xml->asXML();

重要なのが、SimpleXMLElementのコンストラクタに渡すデータに名前空間を指定することと、そこに指定した名前空間の値をaddChild()の第3引数にも指定することです。名前空間に属している要素の中
に更に子要素を追加する場合はaddChild()メソッドに名前空間を指定しなくてもいいみたいです。名前空間を変えたい場合は当然指定する必要があります。

実行結果は以下。

<?xml version="1.0"?>
<services xmlns:nequal="http://nequal.jp/schema"><nequal:service><nequal:name>Openpear</nequal:name><nequal:uri>http://openpear.org/</nequal:uri></nequal:service><nequal:service><nequal:name>DEADLINETIMER &#x30B8;&#x30A7;&#x30CD;&#x30EC;&#x30FC;&#x30BF;&#x30FC;</nequal:name><nequal:uri>http://deadlinetimer.com/</nequal:uri></nequal:service></services>

SimpleXMLElement::asXML()って整形されないんですよね。DOMDocumentなら整形できるので、そちらを使います。

<?php

// ...

$dom = new \DOMDocument();
$dom->loadXml($xml->asXML());

// formatOutputオプションにtrueを指定
$dom->formatOutput = true;

echo $dom->saveXml();
<?xml version="1.0"?>
<services xmlns:nequal="http://nequal.jp/schema">
  <nequal:service>
    <nequal:name>Openpear</nequal:name>
    <nequal:uri>http://openpear.org/</nequal:uri>
  </nequal:service>
  <nequal:service>
    <nequal:name>DEADLINETIMER &#x30B8;&#x30A7;&#x30CD;&#x30EC;&#x30FC;&#x30BF;&#x30FC;</nequal:name>
    <nequal:uri>http://deadlinetimer.com/</nequal:uri>
  </nequal:service>
</services>

できあがり。PHP 5.3.3でやっているのですが、5.2系でも同じようにできると思います(調べてませんけど)。

調べ方

上記で書いた方法がわかるまでに、色々な経緯がありました。そもそもSimpleXMLでは名前空間に属した要素は出力できないんではないかとすら。

で、マニュアルみてもよくわからないときに僕がまずすることは、PHPのテストをみることです。
とりあえず手元にphp-5.3.2のソースがあったので、そこからSimpleXMLのテストコードを探します。

コア部分のコードはZendディレクトリ、ライブラリなどはextディレクトリに大体あります。SimpleXMLはたぶんextディレクトリに入っていそうなので、ext/simplexmlがないかみてみます。

$ ls ext/simplexml
CREDITS  README*  config.m4  config.w32  examples/  php_simplexml.h  php_simplexml_exports.h*  simplexml.c  simplexml.dsp  sxe.c*  sxe.h*  tests/

やはりありました。一番右にtestsディレクトリがありますが、こいつの中にテストが入っています。.phpt拡張子のファイルがPHPのテストファイルです。lsしたらいっぱいあったので、そこから探します。

名前空間に関連するのでたぶん「namespace」という文字が使われていること、出力なのでasXML()メソッドを呼び出していることを考えてgrepかけてみます。

$ cd ext/simplexml
$ grep -rinl namespace *.phpt | xargs grep -rnl 'asXML'
031.phpt

たまたまでしょうが、1つに絞り込まれました。031.phptをみてみると、addChild()メソッドに関するテストが書かれているファイルでした。ここから情報を探ります。

--EXPECTF--
Warning: SimpleXMLElement::addAttribute(): Attribute already exists in %s031.php on line %d

Warning: SimpleXMLElement::addChild(): Cannot add element to attributes in %s031.php on line %d
<?xml version="1.0"?>
<root xmlns:s="urn::test" xmlns:t="urn::test-t" xmlns:v="urn::test-v" s:att1="b" att1="a" v:att11="xxx" att2="no-ns">
   <child1>test</child1>
   <child1>test 2</child1>
   <s:child3/>
<s:test1>myval</s:test1><m:test2 xmlns:m="urn::testnew">myval</m:test2><test3 xmlns="urn::testnew">myval</test3><test4>myval</test4><test5>myval</test5></root>

名前空間付きの要素を出力することはできるみたいですね。「myval」のあたりがやりたいことに近かったので、それを追加しているコードを探して色々やってみると、やりたかったことが無事できるようになりました。これで見つからなかったらソースを読んだりしますが、ちょっとみてもよくわからなったのでテストに書いてあってよかった。。。

言いたいことは、フレームワークでもなんでもそうだけど、テストってやっぱ重要ですよね!ってことです。


全然関係ないけど、WEB+DB PRESS vol.58を読んでいて、Rails 3すげーなーって思いました。Symfony2と並べて考えると、RackまわりとActionDispatchはHttpFoundation、HttpKernel、Routerあたりがカバーできるかなって感じがするんだけど、モデルまわりが断然Railsの方がスマートというか...。