Pulog

PHP上でXSLTを実行してXMLを変換する

ふと案件にて、そこそこ大きな XML ファイルを別のフォーマットの XML に変換するタスクが舞い込んできました。

その際、どうしたものかと色々考えたのですが、結論 XSLT を利用して別のフォーマットの XML に変換することにしました。

今回初めて XSLT を触ったのですが、その際に手こずった箇所の解決方法も織り交ぜつつ、基本的な実装を紹介していこうと思います。

環境構築

今回は Docker 上で PHP を動かし XSLT と XML を読み込んで変換するような環境を構築してみます。
Dockerの入れ方は省略します。Docker でなくても xml , xmlreader , xmlwriter , xsl の拡張機能が入っていれば動作すると思います(未検証)。

参考までに、以下で必要な拡張機能が入っているか確認ができます。

$ php -m
[PHP Modules]
Core
ctype
curl
date
dom
fileinfo
~~中略~~
standard
tokenizer
xml
xmlreader
xmlwriter
xsl
zlib

[Zend Modules]

Docker 公式の PHP Image だと XSLT が含まれていないので XSLT 拡張機能を有効にしてイメージのビルドをします。

Dockerfile 例

FROM php:7.4-apache

# Dockekr の PHP には XSL の拡張機能が入っていないので追加する
RUN apt-get update && apt-get install -y libxslt-dev
RUN docker-php-ext-install xsl

Dockerfileをビルドする

docker build -t xsltexample:latest [Dockerfileのあるディレクトリ]

PHP で XSLT を扱う例

PHP で XSLT を実行できる環境が作成できたら実際に XML ファイルの変換を実行してみます。

以下は /users/user/uid を大文字に変換し、 /users/birth にある生年月日を年齢に変換する処理を行ってみます。

PHP 上で XSLT を実行するメリットとして、PHP にある関数を XSLT で利用できることが挙げられます。
アルファベットの小文字を大文字に変換するなどは XSLT の標準関数でも実装できますが、PHP の関数を利用する方法を紹介するために、意図的に PHPの関数を利用しています。

<?php
$xml = <<<EOB
<?xml version="1.0" encoding="UTF-8"?>
<allusers>
  <user>
    <uid>9c3ddded-f6ae-caeb-008c-af6f10647db8</uid>
    <name middle-name="none">山田 太郎</name>
    <birth>
      <year>1984</year>
      <month>01</month>
      <day>11</day>
    </birth>
  </user>
  <user>
    <uid>9e438f8e-f502-0b08-ec92-7484394bcc8f</uid>
    <name middle-name="none">佐藤 花子</name>
    <birth>
      <year>1983</year>
      <month>10</month>
      <day>19</day>
    </birth>
  </user>
</allusers>
EOB;

$xsl = <<<EOB
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:php="http://php.net/xsl">
  <xsl:output method="xml" encoding="utf-8" indent="yes" cdata-section-elements="unique-id name" />
  <xsl:template match="allusers">
    <users>
      <xsl:for-each select="user">
        <user>
          <unique-id><xsl:value-of select="php:function('mb_strtoupper' , string(uid))" /></unique-id>
          <name><xsl:value-of select="name" /></name>
          <age><xsl:value-of select="php:function('get_age' , string(birth/year), string(birth/month), string(birth/day))" /></age>
        </user>
      </xsl:for-each>
    </users>
 </xsl:template>
</xsl:stylesheet>
EOB;

// 年齢を出力する関数
function get_age($year, $month, $day) {
  $birthday = "$year$month$day";
  return floor((date("Ymd") - $birthday) / 10000);
}

// XML及びXSLを読み込む
$xmlDocument = DOMDocument::loadXML($xml);
$xslDocument = DOMDocument::loadXML($xsl);

// XSLTProcessorを呼び PHPの関数も使えるような設定を付与する
$xsltProcessor = new XSLTProcessor();
$xsltProcessor->registerPHPFunctions();
$xsltProcessor->importStyleSheet($xslDocument);

// XSLTの実行結果を出力
echo $xsltProcessor->transformToXML($xmlDocument);

registerPHPFunctions を宣言しないと PHP 側で用意した関数が実行できないので、注意が必要です。

上記ソースを先程作成した Docker Image を利用し、以下コマンドにて実行してみます。
XML が XSLT により別フォーマットの XML に変換できていることが確認できると思います。

$ docker run --rm -v "$(PWD):/var/www/html" xsltexample php example.php
<?xml version="1.0" encoding="utf-8"?>
<users xmlns:php="http://php.net/xsl">
  <user>
    <unique-id><![CDATA[9C3DDDED-F6AE-CAEB-008C-AF6F10647DB8]]></unique-id>
    <name><![CDATA[山田 太郎]]></name>
    <age>37</age>
  </user>
  <user>
    <unique-id><![CDATA[9E438F8E-F502-0B08-EC92-7484394BCC8F]]></unique-id>
    <name><![CDATA[佐藤 花子]]></name>
    <age>37</age>
  </user>
</users>

おまけ XSLT から PHP に渡される引数について

上記の例では PHP の関数に渡す際に XSLT の string 関数で文字列型に変換していました。
この関数を挟まないと、PHP 側ではどのように扱われるのか確認してみます。

上記で作成した PHP ファイルに以下のような追記を加え、再度ファイルの変換を実行してみます。

   <age><xsl:value-of select="php:function('get_age' , string(birth/year), string(birth/month), string(birth/day))" /></age>
+  <types>
+    <xsl:value-of select="concat(
+      php:function('get_instance' , /), ' ,',
+      php:function('get_instance' , name), ' ,',
+      php:function('get_instance' , name/text()), ' ,',
+      php:function('get_instance' , name/@middle-name), ' ,',
+      string(name/@middle-name))"
+    />
+  </types>
  </user>
      return floor((date("Ymd") - $birthday)/10000);
  }

+ // 型を出力
+ function get_instance($obj) : string{
+   echo gettype($obj) . " : " . print_r($obj, true) . "\n";
+   return gettype($obj);
+ }

  // XML及びXSLを読み込む
$ docker run --rm -v "$(PWD):/var/www/html" xsltexample php example.php
array : Array
(
    [0] => DOMDocument Object
        (
            [doctype] =>
~中略~
            [textContent] =>

    9c3ddded-f6ae-caeb-008c-af6f10647db8
    山田 太郎

      1984
      01
      11




        )

)

array : Array
(
    [0] => DOMElement Object
        (
            [tagName] => name
~中略~
            [textContent] => 山田 太郎
        )

)

array : Array
(
    [0] => DOMText Object
        (
            [wholeText] => 山田 太郎
~中略~
            [textContent] => 山田 太郎
        )

)

array : Array
(
    [0] => DOMAttr Object
        (
            [name] => middle-name
~中略~
            [textContent] => none
        )

)
~以下略~

PHP の DOMDocument だったり、DOMText だったりと、総じて Document Object Model (Dom) なオブジェクトが渡されてくるようです。
これを利用して、 XSLT だけではなし得られない処理を補ったりすることもできそうですね。