Last active
May 23, 2020 21:26
-
-
Save matb33/1206054 to your computer and use it in GitHub Desktop.
DOMDocument->saveJSON()
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// saveJSON: XML to JSON conversion/transformation | |
// The saveJSON method is implemented as a method of the example DOMDocumentExtended class, which | |
// extends DOMDocument. It will transform the currently loaded DOM into JSON using XSLT. Since | |
// there isn't a reliable automatic way of detecting if certain siblings nodes should be | |
// represented as arrays or not, a "forceArray" parameter can be passed to the saveJSON method. | |
// It should be noted that the forceArray functionality doesn't recognize namespaces. This is | |
// an easy enough fix, I just didn't need it at the time of writing (look for local-name() and | |
// modify accordingly). | |
// It should be quite trivial to port this code to another language since the bulk of the work is | |
// done using an XSL stylesheet. | |
class DOMDocumentExtended extends DOMDocument | |
{ | |
public function saveJSON( $forceArray = array() ) | |
{ | |
$xsltParameters = array( "force_array" => "|" . implode( "|", $forceArray ) . "|" ); | |
$xslDocument = new DOMDocument(); | |
$xslDocument->loadXML( XML_TO_JSON_STYLESHEET ); | |
try | |
{ | |
$processor = new XSLTProcessor(); | |
$processor->importStyleSheet( $xslDocument ); | |
$processor->setParameter( "", $xsltParameters ); | |
$result = $processor->transformToXML( $this ); | |
$failure = $result === false || empty( $result ); | |
} | |
catch( Exception $e ) | |
{ | |
$failure = $result = false; | |
} | |
if( $failure ) | |
{ | |
// TODO: implement error handling (throw an exception, preferably looking up libxml errors) | |
} | |
return $result; | |
} | |
} | |
define( "XML_TO_JSON_STYLESHEET", <<<'XML' | |
<?xml version="1.0"?> | |
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> | |
<xsl:output | |
method="text" | |
encoding="UTF-8" | |
indent="no" | |
omit-xml-declaration="yes" | |
/> | |
<xsl:param name="force_array" select="''" /> | |
<xsl:template match="/"> | |
<xsl:text>{</xsl:text> | |
<xsl:apply-templates select="." mode="normal" /> | |
<xsl:text>}</xsl:text> | |
</xsl:template> | |
<xsl:template match="*" mode="normal"> | |
<xsl:choose> | |
<xsl:when test="contains( $force_array, concat( '|', local-name(), '|' ) )"> | |
<xsl:if test="position() = 1"> | |
<xsl:call-template name="element_as_array" /> | |
</xsl:if> | |
</xsl:when> | |
<xsl:when test="not(child::*)"> | |
<xsl:call-template name="element_as_leaf" /> | |
</xsl:when> | |
<xsl:otherwise> | |
<xsl:call-template name="element_as_object" /> | |
</xsl:otherwise> | |
</xsl:choose> | |
</xsl:template> | |
<xsl:template name="element_as_object"> | |
<xsl:text>"</xsl:text><xsl:value-of select="local-name()" /><xsl:text>":{</xsl:text> | |
<xsl:call-template name="attributes" /> | |
<xsl:if test="@*"><xsl:if test="*"><xsl:text>,</xsl:text></xsl:if></xsl:if> | |
<xsl:apply-templates select="*" mode="normal" /> | |
<xsl:text>}</xsl:text> | |
</xsl:template> | |
<xsl:template name="element_as_leaf"> | |
<xsl:text>"</xsl:text><xsl:value-of select="local-name()" /><xsl:text>":"</xsl:text><xsl:call-template name="escape"><xsl:with-param name="string" select="." /></xsl:call-template><xsl:text>"</xsl:text> | |
<xsl:if test="position() != last()"><xsl:text>,</xsl:text></xsl:if> | |
</xsl:template> | |
<xsl:template name="element_as_array"> | |
<xsl:variable name="element_name" select="name()" /> | |
<xsl:variable name="elements" select="preceding-sibling::*[name()=$element_name]|self::*|following-sibling::*[name()=$element_name]" /> | |
<xsl:if test="count($elements)"> | |
<xsl:text>"</xsl:text><xsl:value-of select="local-name()" /><xsl:text>":[</xsl:text> | |
<xsl:for-each select="$elements"> | |
<xsl:text>{</xsl:text> | |
<xsl:call-template name="attributes" /> | |
<xsl:if test="@*"><xsl:if test="*"><xsl:text>,</xsl:text></xsl:if></xsl:if> | |
<xsl:apply-templates select="*" mode="normal" /> | |
<xsl:text>}</xsl:text> | |
<xsl:if test="position() != last()">,</xsl:if> | |
</xsl:for-each> | |
<xsl:text>]</xsl:text> | |
</xsl:if> | |
</xsl:template> | |
<xsl:template name="attributes"> | |
<xsl:if test="@*"> | |
<xsl:text>"@":{</xsl:text> | |
<xsl:apply-templates select="@*" /> | |
<xsl:text>}</xsl:text> | |
</xsl:if> | |
</xsl:template> | |
<xsl:template match="@*"> | |
<xsl:call-template name="element_as_leaf" /> | |
</xsl:template> | |
<xsl:template name="escape"> | |
<xsl:param name="string" select="''" /> | |
<xsl:call-template name="string-replace-all"> | |
<xsl:with-param name="text"> | |
<xsl:call-template name="string-replace-all"> | |
<xsl:with-param name="text"> | |
<xsl:call-template name="string-replace-all"> | |
<xsl:with-param name="text"> | |
<xsl:call-template name="string-replace-all"> | |
<xsl:with-param name="text"> | |
<xsl:call-template name="string-replace-all"> | |
<xsl:with-param name="text" select="$string" /> | |
<xsl:with-param name="replace" select="'	'" /> | |
<xsl:with-param name="by" select="'\t'" /> | |
</xsl:call-template> | |
</xsl:with-param> | |
<xsl:with-param name="replace" select="'
'" /> | |
<xsl:with-param name="by" select="'\r'" /> | |
</xsl:call-template> | |
</xsl:with-param> | |
<xsl:with-param name="replace" select="'
'" /> | |
<xsl:with-param name="by" select="'\n'" /> | |
</xsl:call-template> | |
</xsl:with-param> | |
<xsl:with-param name="replace" select="'\'" /> | |
<xsl:with-param name="by" select="'\\'" /> | |
</xsl:call-template> | |
</xsl:with-param> | |
<xsl:with-param name="replace" select="'"'" /> | |
<xsl:with-param name="by" select="'\"'" /> | |
</xsl:call-template> | |
</xsl:template> | |
<xsl:template name="string-replace-all"> | |
<xsl:param name="text" /> | |
<xsl:param name="replace" /> | |
<xsl:param name="by" /> | |
<xsl:choose> | |
<xsl:when test="contains($text, $replace)"> | |
<xsl:value-of select="substring-before($text, $replace)" /> | |
<xsl:value-of select="$by" /> | |
<xsl:call-template name="string-replace-all"> | |
<xsl:with-param name="text" select="substring-after($text, $replace)" /> | |
<xsl:with-param name="replace" select="$replace" /> | |
<xsl:with-param name="by" select="$by" /> | |
</xsl:call-template> | |
</xsl:when> | |
<xsl:otherwise> | |
<xsl:value-of select="$text" /> | |
</xsl:otherwise> | |
</xsl:choose> | |
</xsl:template> | |
</xsl:stylesheet> | |
XML | |
); |
Couldn't get this to work fully. It generally worked, but for some strange reason some of the attribute's values were randomly missing in the resulting JSON even though they had normal everyday values in the XML.
However, insanely enough,
<?php
$xml = simplexml_load_file("foo.xml"); // or simplexml_load_string()
$json = json_encode($xml);
echo $json;
Worked beautifully.
For json_encoding a DOMDocument $a
$output = json_encode(simplexml_import_dom($a->documentElement))
Used this for some SOAP applications where I couldn't get nuSOAP or the built in SOAP functionality to work very well in PHP. Couple of minor bugs that I got fixed and it works like a charm now...
<?php
// saveJSON: XML to JSON conversion/transformation
// The saveJSON method is implemented as a method of the example DOMDocumentExtended class, which
// extends DOMDocument. It will transform the currently loaded DOM into JSON using XSLT. Since
// there isn't a reliable automatic way of detecting if certain siblings nodes should be
// represented as arrays or not, a "forceArray" parameter can be passed to the saveJSON method.
// It should be noted that the forceArray functionality doesn't recognize namespaces. This is
// an easy enough fix, I just didn't need it at the time of writing (look for local-name() and
// modify accordingly).
// It should be quite trivial to port this code to another language since the bulk of the work is
// done using an XSL stylesheet.
class DOMDocumentExtended extends DOMDocument
{
public function saveJSON( $forceArray = array() )
{
$xsltParameters = array( "force_array" => "|" . implode( "|", $forceArray ) . "|" );
$xslDocument = new DOMDocument();
$xslDocument->loadXML( XML_TO_JSON_STYLESHEET );
try
{
$processor = new XSLTProcessor();
$processor->importStyleSheet( $xslDocument );
$processor->setParameter( "", $xsltParameters );
$result = $processor->transformToXML( $this );
$failure = $result === false || empty( $result );
}
catch( Exception $e )
{
$failure = $result = false;
}
if( $failure )
{
// TODO: implement error handling (throw an exception, preferably looking up libxml errors)
}
return $result;
}
}
define( "XML_TO_JSON_STYLESHEET", <<<'XML'
<?xml version="1.0"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output
method="text"
encoding="UTF-8"
indent="no"
omit-xml-declaration="yes"
/>
<xsl:param name="force_array" select="''" />
<xsl:template match="/">
<xsl:text>{</xsl:text>
<xsl:apply-templates select="." mode="normal" />
<xsl:text>}</xsl:text>
</xsl:template>
<xsl:template match="*" mode="normal">
<xsl:choose>
<xsl:when test="contains( $force_array, concat( '|', local-name(), '|' ) )">
<xsl:if test="position() = 1">
<xsl:call-template name="element_as_array" />
</xsl:if>
</xsl:when>
<xsl:when test="not(child::*)">
<xsl:call-template name="element_as_leaf" />
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="element_as_object" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="element_as_object">
<xsl:text>"</xsl:text><xsl:value-of select="local-name()" /><xsl:text>":{</xsl:text>
<xsl:call-template name="attributes" />
<xsl:if test="@*"><xsl:if test="*"><xsl:text>,</xsl:text></xsl:if></xsl:if>
<xsl:apply-templates select="*" mode="normal" />
<xsl:text>}</xsl:text>
<xsl:if test="position() != last()"><xsl:text>,</xsl:text></xsl:if>
</xsl:template>
<xsl:template name="element_as_leaf">
<xsl:text>"</xsl:text><xsl:value-of select="local-name()" /><xsl:text>":"</xsl:text><xsl:call-template name="escape"><xsl:with-param name="string" select="." /></xsl:call-template><xsl:text>"</xsl:text>
<xsl:if test="position() != last()"><xsl:text>,</xsl:text></xsl:if>
</xsl:template>
<xsl:template name="element_as_array">
<xsl:variable name="element_name" select="name()" />
<xsl:variable name="elements" select="preceding-sibling::*[name()=$element_name]|self::*|following-sibling::*[name()=$element_name]" />
<xsl:if test="count($elements)">
<xsl:text>"</xsl:text><xsl:value-of select="local-name()" /><xsl:text>":[</xsl:text>
<xsl:for-each select="$elements">
<xsl:text>{</xsl:text>
<xsl:call-template name="attributes" />
<xsl:if test="@*"><xsl:if test="*"><xsl:text>,</xsl:text></xsl:if></xsl:if>
<xsl:apply-templates select="*" mode="normal" />
<xsl:text>}</xsl:text>
<xsl:if test="position() != last()">,</xsl:if>
</xsl:for-each>
<xsl:text>]</xsl:text>
</xsl:if>
</xsl:template>
<xsl:template name="attributes">
<xsl:if test="@*">
<xsl:text>"@":{</xsl:text>
<xsl:apply-templates select="@*" />
<xsl:text>}</xsl:text>
</xsl:if>
</xsl:template>
<xsl:template match="@*">
<xsl:call-template name="element_as_leaf" />
</xsl:template>
<xsl:template name="escape">
<xsl:param name="string" select="''" />
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text">
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text" select="$string" />
<xsl:with-param name="replace" select="'	'" />
<xsl:with-param name="by" select="'\t'" />
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="replace" select="'
'" />
<xsl:with-param name="by" select="'\r'" />
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="replace" select="'
'" />
<xsl:with-param name="by" select="'\n'" />
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="replace" select="'\'" />
<xsl:with-param name="by" select="'\\'" />
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="replace" select="'"'" />
<xsl:with-param name="by" select="'\"'" />
</xsl:call-template>
</xsl:template>
<xsl:template name="string-replace-all">
<xsl:param name="text" />
<xsl:param name="replace" />
<xsl:param name="by" />
<xsl:choose>
<xsl:when test="contains($text, $replace)">
<xsl:value-of select="substring-before($text, $replace)" />
<xsl:value-of select="$by" />
<xsl:call-template name="string-replace-all">
<xsl:with-param name="text" select="substring-after($text, $replace)" />
<xsl:with-param name="replace" select="$replace" />
<xsl:with-param name="by" select="$by" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$text" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
XML
);
?>
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Probably because of the "nowdoc" syntax: <<<'XML'
You'll need PHP 5.3.0 or higher