PHPでXML文書をオブジェクトじゃなくて普通の配列としてパースしたり、普通の配列からXMLに変換する関数(失敗編)

本題の結論

PHP環境が変わったときはその都度対応しよう.

動機

昔昔のPHP4時代に開発したWeblogシステムがある。

RDBMSも使用せず、何故かCSVXML、iniファイルを、何故かDBと同じ階層で無理矢理
DBコントローラー用のファイル(not クラス, but ファイル[PHP4時代のクラスの機能はやる意味あんの?という水準だった。])で制御するという、ある意味とんんでもないことなのだが、今のところ正常に動作している(ようにみえる。)。

それが、何年か前に本番環境がPHP5に、いつの間にか変更になり、Notice:が出まくっていたが、対処療法的に警告さえでなけりゃいっか!というノリでいじって済ませていた。

ところがそれから何年かして、開発用の自宅サーバで利用していた無料のDDNSであるno-ipが有料化になり、別の無料DDNSを導入しようとしてhttpd.confやssl.confをいじっているとApacheが壊れた。

で、portからreinstallとかdeinstallとかしてみても、conflictが大量に発生して依存関係でデッドロックしてしまった。

で、開発用の自宅サーバFreeBSDのバージョンを9から11に上げると、portsには、PHP7がある。

新しいものを入れたいと思うのが人の性なので当然最新バージョンを導入した。

そしたら、なんとPHP7では、PHP4時代に重宝していたxml.phpライブラリがFatal Errorを吐きまくった。まじで。
で、もしかしたらxml.phpのバージョンアップバージョンがあるかも!?と思ってサイトを除くとまさかの404。
http://keithdevens.com/software/phpxml


元ソースは私には難解すぎてどうしてもエラーを消せず断念。
色々最近PHPを調べてみるとsimple_xmlなんていう素晴らしいクラスがあることを知り試してみるが、配列がなんかよくわからないオブジェクト形式になっていた。

これまでのデータ制御関係のソースコードを全て書き換えるのは、心が折れるのでxml.phpと同じ動作をする関数を作ろうと思ったのがきっかけ。

本職がプログラマではないので、勤務時間中にコーディングする時間がないのもあった。

使わせてもらった先人の知恵

PHP 配列をxml形式に変換
ichiy.hatenablog.com

変更点

本来はid:ichiy さんのような、上記の方法で全く問題ないのでだが、如何せん私のミッションは、元のソースコードを一切いじらず完全互換のphp.xmlを作ることだったので、このままでは動かなかった。

まず、クラスを使わず、関数にしないといけない。これはすぐできた。$thisとかselfとか ::とかそこらへんをいじるだけでOK.

問題はsimple_xml
simple_xmlは、xmlから配列にする時や、配列からxmlにする時に先頭のタグが消されてしまう。
具体的には、

<?xml version="1.0"?>
<abc>
   <def>
       <ghi>
            a
       </ghi>
       <ghi>
             b
       </ghi>
   </def>
<abc>

は、

array(
    [def]
         [0]
              [ghi]
                   a
         [1]
              [ghi]
                    b

となってしまう。

そこで、無理矢理 xmlのルートタグにダミータグを挿入する必要がある。

タグの検出には、昔買ったオライリーの詳説正規表現に載っていたが、コードに入れるのがめんどくさいのでやめた。でもいい本で、買って良かったと思う。

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

詳説正規表現第3版 [ ジェフリー・E.F.フリードル ]
価格:5184円(税込、送料無料) (2018/1/11時点)


コード

使い方の説明は、コメント参照または、先に紹介した方々のサイトを参照。

<?php  /* vim: set fdm=marker: */ 
/*
もともとのxml.phpは、内部的にutf-8で処理しているので、
ファイルの方で、毎回mb_convert_encodingしている。
従ってこっちのほうで、文字エンコードを変換しなくていい。
XML_unserialize( $buffer ); <- xmlからarray

*/
//$var = Libs::parseAPI('', array('zn' => '1600000'), 'GET');
//$var = Libs::XMLtoArray("./5.xml");
//$fBuf = mb_convert_encoding($fBuf, "UTF-8", "sjis-win");
$xml = mb_convert_encoding(file_get_contents("./5.xml"), "UTF-8","sjis-win");

$var = XML_unserialize($xml);
print_r($var);
print $var['dary']['article'][2]['contents'];

//simpleXML用に文字列を作成
//$newxml = XML_serialize($var, '');
$newxml = XML_serialize($var);
print $newxml;

/**
* 配列を再帰的にXML用の文字列に変換
*
* foreachの$valueの値が配列でなくなるまで再帰
* ※ ['key' => array()]という場合も考慮
* @see http://tk2-207-13211.vs.sakura.ne.jp/2015/08/278/
* @param array or string $data 配列もしくは要素の中身
* @param string $name 要素の名前 例えば、これに"ABC"とすると、<abc>$dataをxmlにしたやつ</abc>になる。
* @return string $str 
*/
function XML_serialize($data, $name = '') {
	$ret = XML_serialize_2($data, $name);
	$ret = "<?xml version=\"1.0\"?>\n".$ret;
	return $ret;
}

/** XML_serializeから受けて呼ばれる.
 * 面倒だがXML_serialize_2は再帰関数なので、最後に<?xml version="1.0"?>を入れたいから仕方なく.
 * @param $data XML_serializeの$data
 * @param $name XML_serializeの$name
 * @ret <?xml version = "1.0" ?>なしのxmlデータ
 */
function XML_serialize_2($data, $name = '') { /** {{{*/
    $str = '';
    if(!empty($name)) {
        $str .= "<".$name.">";
 	}
    if(!is_array($data)) {
        $str .= $data;
    } else {
        foreach ($data as $key => $val) {
            if(is_numeric($key)) {
                $str .= XML_serialize_2($val, '');
            } else {
                if(is_array($val) && !empty($val)) {
                    $str .= XML_serialize_2($val, $key);
                } else {
                    $str .= "<".$key.">";
                    $str .= (empty($val)) ? "" : $val;
                    $str .= "</".$key.">";
                }
            }
        }
    }
    if(!empty($name)) {
        $str .= "</".$name.">";
 	}
    return $str;
}
/**}}}*/

/** XML文書を解析して、配列として返す.
 * XML文書を、simplexml_load_string($xml)を利用して、配列に変換する.
 * その関数の構造上一番先頭のXMLタグは消去されてしまうため、
 * 無理矢理dummyタグを入れ子にしている.これでダミータグが消えるだけですむので、
 * ルートタグを含めて配列にできる.
 * @param $xml xml[必ずUTF-8]
 */
function XML_unserialize( $xml ) { /** {{{*/
	//無理やり.
	//$xml = file_get_contents($fXmlPath);

	//一番先頭のタグの名前を取得する.
	//$rootTag = preg_match("正規表現のパタン", "入力文字列","検索結果");
	preg_match("/<([a-z0-9]+)>/", $xml, $data_match);
	$rootTag = $data_match[1];

	$xml = preg_replace("/<$rootTag>/", "<dummy><$rootTag>", $xml, 1);//1回だけ置換する.
	$xml = preg_replace("/<.$rootTag>/", "</$rootTag></dummy>", $xml, 1);//1回だけ置換する.

	// 帰ってきたXMLをパース
	$parse_xml = simplexml_load_string($xml);
	if (false === $parse_xml) return false;

	return xml2arr($parse_xml);
}
/**}}}*/

/** */
function xml2arr($xmlobj) { /** {{{*/
	$arr = array();
	if (is_object($xmlobj)) {
		$xmlobj = get_object_vars($xmlobj);
	} else {
		$xmlobj = $xmlobj;
	}

	foreach ($xmlobj as $key => $val) {
		if (is_object($xmlobj[$key])) {
			$arr[$key] = xml2arr($val);
		} else if (is_array($val)) {
			foreach($val as $k => $v) {
				if (is_object($v) || is_array($v)) {
					$arr[$key][$k] = xml2arr($v);
				} else {
					$arr[$key][$k] = $v;
				}
			}
		} else {
			$arr[$key] = $val;
		}
	}
	return $arr;
}
/**}}}*/

?>