Skip to content

Instantly share code, notes, and snippets.

@matb33
Last active May 23, 2020 21:26
Show Gist options
  • Save matb33/1206054 to your computer and use it in GitHub Desktop.
Save matb33/1206054 to your computer and use it in GitHub Desktop.
DOMDocument->saveJSON()
<?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="'&#x9;'" />
<xsl:with-param name="by" select="'\t'" />
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="replace" select="'&#xD;'" />
<xsl:with-param name="by" select="'\r'" />
</xsl:call-template>
</xsl:with-param>
<xsl:with-param name="replace" select="'&#xA;'" />
<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="'&quot;'" />
<xsl:with-param name="by" select="'\&quot;'" />
</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
);
@matb33
Copy link
Author

matb33 commented Mar 5, 2013

Try $doc = new DOMDocumentExtended() instead

@mikandros
Copy link

Hi Matb,

I tried to use your solution but get error:
Parse error: syntax error, unexpected T_SL in /home/ccc/domains/ccc.com/public_html/amvl/DOMDocumentExtended.php on line 48

This line 48 = define( "XML_TO_JSON_STYLESHEET", <<<'XML'

Do you understand what is wrong?

rgrds, Mike

@matb33
Copy link
Author

matb33 commented Jun 19, 2013

Probably because of the "nowdoc" syntax: <<<'XML'

You'll need PHP 5.3.0 or higher

@abcarroll
Copy link

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.

Copy link

ghost commented Mar 4, 2020

For json_encoding a DOMDocument $a

$output = json_encode(simplexml_import_dom($a->documentElement))

@matt-o-matic
Copy link

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="'&#x9;'" />
											<xsl:with-param name="by" select="'\t'" />
										</xsl:call-template>
									</xsl:with-param>
									<xsl:with-param name="replace" select="'&#xD;'" />
									<xsl:with-param name="by" select="'\r'" />
								</xsl:call-template>
							</xsl:with-param>
							<xsl:with-param name="replace" select="'&#xA;'" />
							<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="'&quot;'" />
			<xsl:with-param name="by" select="'\&quot;'" />
		</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