XML Clone node in PHP - php

I have to clone an XML node and its childs and append it to a new XML in a specifics tag.
Ie:
Source XML:
<root>
<element>
<back>
<item1>ABC</item1>
<item2>DEF</item2>
<more>
<moreitem>GHI</moreitem>
</more
</back>
</element>
</root>
Destination XML:
<root>
<base1>
<item1>FOO</item1>
<item2>BAR</item2>
<base2>
**<back>From source XML and all its childs here</back>**
</base2>
</base1>
<root>

DOMXpath::evaluate() allows you to fetch nodes using Xpath expressions. DOMDocument::importNode() duplicates a node and imports a node into a target document. DOMNode::cloneNode() create a duplicate of node to add in the same document. DOMNode::appendChild() allows you to append the imported/cloned node.
$source = <<<'XML'
<root>
<element>
<back>
<item1>ABC</item1>
<item2>DEF</item2>
<more>
<moreitem>GHI</moreitem>
</more>
</back>
</element>
</root>
XML;
$target = <<<'XML'
<root>
<base1>
<item1>FOO</item1>
<item2>BAR</item2>
<base2>
</base2>
</base1>
</root>
XML;
$sourceDocument = new DOMDocument();
$sourceDocument->loadXml($source);
$sourceXpath = new DOMXpath($sourceDocument);
$targetDocument = new DOMDocument();
$targetDocument->loadXml($target);
$targetXpath = new DOMXpath($targetDocument);
foreach ($targetXpath->evaluate('/root/base1/base2[1]') as $targetNode) {
foreach ($sourceXpath->evaluate('/root/element/back') as $backNode) {
$targetNode->appendChild($targetDocument->importNode($backNode, TRUE));
}
}
echo $targetDocument->saveXml();
Output:
<?xml version="1.0"?>
<root>
<base1>
<item1>FOO</item1>
<item2>BAR</item2>
<base2>
<back>
<item1>ABC</item1>
<item2>DEF</item2>
<more>
<moreitem>GHI</moreitem>
</more>
</back>
</base2>
</base1>
</root>

Of course you can use XSLT, the native programming language to restructure XML documents to any nuanced needs. Specifically here, you require pulling XML content from an external source XML file. And PHP like other general purpose languages (Java, C#, Python, VB) maintain libraries for XSLT processing.
XSLT (save as .xsl or .xslt file to be used in PHP below and be sure Source and Destination XML files are in same directory)
<?xml version="1.0" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output version="1.0" encoding="UTF-8" indent="yes" />
<xsl:strip-space elements="*" />
<!-- Identity Transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="back">
<back>
<xsl:copy-of select="document('Source.xml')"/>
</back>
</xsl:template>
</xsl:transform>
PHP (loading XML and XSL files externally but can be embedded as string)
$destinationdoc = new DOMDocument();
$doc1->load('Destination.xml');
$xsl = new DOMDocument;
$xsl->load('XSLTScript.xsl');
// Configure the transformer
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
// Transform XML source
$newXml = $proc->transformToXML($doc1);
// Save output to file
$xmlfile = 'FinalOutput.xml';
file_put_contents($xmlfile, $newXml);
OUTPUT (using your above posted Source and Destination xml)
<?xml version="1.0" encoding="UTF-8"?>
<root>
<base1>
<item1>FOO</item1>
<item2>BAR</item2>
<base2>
<back>
<root>
<element>
<back>
<item1>ABC</item1>
<item2>DEF</item2>
<more>
<moreitem>GHI</moreitem>
</more>
</back>
</element>
</root>
</back>
</base2>
</base1>
</root>

This is an easy way to do this:
$src = new DOMDocument();
$dst = new DOMDocument();
$src->loadXML($src_xml);
$dst->loadXML($dst_xml);
$back = $src->getElementsByTagName('back')->item(0);
$base = $dst->getElementsByTagName('base2')->item(0);
$base->appendChild( $dst->importNode( $back, true ) );
echo $dst->saveXML();

Related

Domdocument: Why XSLT Transformation Output became single line?

Hi I wanted to know how to retail the xml structure when using XSLT,
I have these code below,
/* XSLT File */
$xsl = new \DOMDocument;
$xsl->loadXML($xsltData[0]->xslt_template);
$xsl->preserveWhiteSpace = false;
$xsl->formatOutput = true;
/* Combine and Transform XML and XSLT */
$proc = new \XSLTProcessor;
$proc->importStyleSheet($xsl);
$proc->preserveWhiteSpace = false;
$proc->formatOutput = true;
$transformedOutPut = $proc->transformToXML($xml);
Here is my input xml
<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<cd>
<titleA style="test" size="123">Kevin del </titleA>
<address>1119 Johnson Street, San Diego, California</address>
<country>USA</country>
<company>Columbia</company>
<price>10.90</price>
<year>1985</year>
</cd>
</catalog>
and here is my XSLT from the database with preserve spacing,
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<catalognew>
<container-title>My Data</container-title>
<xsl:for-each select="catalog/cd">
<cdnew>
<titlenew><xsl:value-of select="titleA"/></titlenew>
<addressnew>
<street><xsl:value-of select="address/street"/></street>
<city><xsl:value-of select="address/city"/></city>
<state><xsl:value-of select="address/state"/></state>
</addressnew>
</cdnew>
</xsl:for-each>
</catalognew>
</xsl:template>
</xsl:stylesheet>
Why doe it give me a single line result and not retaining its original structure,
<?xml version="1.0"?>
<catalognew><container-title>My Data</container-title><cdnew><titlenew>Kevin del </titlenew><addressnew><street>1119 JOHNSON STREET</street><city>SAN DIEGO</city><state>CALIFORNIA</state></addressnew></cdnew></catalognew>
I hope someone can help me,
Thank You,
The problem seems to be that you need to ensure the final document object has the settings to format the output. So rather than use transformToXML($xml), this creates a new document and ensures that this new document has the formatting options set before outputting the result...
$transformedOutPut = $proc->transformToDoc($xml);
$transformedOutPut->preserveWhiteSpace = true;
$transformedOutPut->formatOutput = true;
print_r($transformedOutPut->saveXML());
gives...
<?xml version="1.0"?>
<catalognew>
<container-title>My Data</container-title>
<cdnew>
<titlenew>Kevin del </titlenew>
<addressnew>
<street/>
<city/>
<state/>
</addressnew>
</cdnew>
</catalognew>

PHP Change XML node values

I'm having some difficulties in changing XML Node values with PHP.
My XML is the following
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ProcessTransaction
xmlns="http://example.com">
<TransactionRequest
xmlns="http://example.com">
<Header>
<RequestType>SALE</RequestType>
<RequestMethod>SYNCHRONOUS</RequestMethod>
<MerchantInfo>
<PosName>kwstasna</PosName>
<PosID>1234</PosID>
</MerchantInfo>
</Header>
</TransactionRequest>
</ProcessTransaction>
</soap:Body>
</soap:Envelope>
And i want to change PosName and PosID.
The XML is received from a POST Request.
If i print_r($REQUEST['xml']
I get the values in text.
And what i've tried is the following
$posid = '321';
$posname = 'nakwsta';
$result = $xml->xpath("/soap:Envelope/soap:Body/ProcessTransaction/TransactionRequest/Header/MerchantInfo");
$result[0]->PosID = $posid;
$result[0]->PosName = $posname;
echo $result;
But i get an empty array Array[]
I think my mistake is in the values of <soap:Envelope for example.
Anyone that had the same issue and find out the way to solve it?
Thanks a lot for your time.
The ProcessTransaction element (and all of its child nodes) are in the "http://example.com" namespace. If you want to access them using xpath(), you'll need to register a namespace prefix:
$xml->registerXPathNamespace('ex', 'http://example.com');
You can then use the ex prefix on all relevant parts of your query
$result = $xml->xpath("/soap:Envelope/soap:Body/ex:ProcessTransaction/ex:TransactionRequest/ex:Header/ex:MerchantInfo");
The rest of your code should function correctly, see https://eval.in/916856
Consider a parameterized XSLT (not unlike parameterized SQL) where PHP passes value to the underlying script with setParameter().
As information, XSLT (sibling to XPath) is a special-purpose language designed to transform XML files. PHP can run XSLT 1.0 scripts with the XSL class. Specifically, below runs the Identity Transform to copy XML as is and then rewrites the PosName and PosID nodes. Default namespace is handled accordingly in top root tag aligned to doc prefix.
XSLT (save as .xsl file, a special well-formed .xml file)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:doc="http://example.com">
<xsl:output method="xml" indent="yes" />
<xsl:strip-space elements="*" />
<xsl:param name="PosNameParam"/>
<xsl:param name="PosIDParam"/>
<!-- IDENTITY TRANSFORM -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!-- RE-WRITE PosName NODE -->
<xsl:template match="doc:PosName">
<xsl:copy>
<xsl:value-of select="$PosNameParam"/>
</xsl:copy>
</xsl:template>
<!-- RE-WRITE PosID NODE -->
<xsl:template match="doc:PosID">
<xsl:copy>
<xsl:value-of select="$PosIDParam"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
PHP
$posid = '321';
$posname = 'nakwsta';
// Load XML and XSL
$xml = new DOMDocument;
$xml->load('Input.xml');
$xsl = new DOMDocument;
$xsl->load('XSLTScript.xsl');
// Configure transformer
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
// Assign values to XSLT parameters
$proc->setParameter('', 'PosNameParam', $posid);
$proc->setParameter('', 'PosIDParam', $posname);
// Transform XML source
$newXML = new DOMDocument;
$newXML = $proc->transformToXML($xml);
// Output to console
echo $newXML;
// Output to file
file_put_contents('Output.xml', $newXML);
Output
<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ProcessTransaction xmlns="http://example.com">
<TransactionRequest>
<Header>
<RequestType>SALE</RequestType>
<RequestMethod>SYNCHRONOUS</RequestMethod>
<MerchantInfo>
<PosName>nakwsta</PosName>
<PosID>321</PosID>
</MerchantInfo>
</Header>
</TransactionRequest>
</ProcessTransaction>
</soap:Body>
</soap:Envelope>

Split XML files with PHP not outputting top level parent node

I'm trying to separate an XML file into two files, longrentals.xml and shortrentals.xml but have hit a last hurdle I'm stuck on. The following is what I would like to happen:
rentals.xml is parsed and for each instance of term = "short" the top parent "property" node of that entry is saved to shortrentals.xml.
Each instance is removed from the rentals.xml file (after extracting).
The shortrentals.xml file is saved.
The remaining entries in the original file is saved to longrentals.xml.
The XML structure is as follows:
<property>
...
<rent>
<term>short</term>
<freq>week</freq>
<price_peak>5845</price_peak>
<price_high>5845</price_high>
<price_medium>4270</price_medium>
<price_low>3150</price_low>
</rent>
...
</property>
The code I'm using is as follows:
$destination = new DOMDocument;
$destination->preserveWhiteSpace = true;
$destination->loadXML('<?xml version="1.0" encoding="utf-8"?><root></root>');
$source = new DOMDocument;
$source->load('file/rentals.xml');
$xp = new DOMXPath($source);
$destRoot = $destination->getElementsByTagName("root")->item(0);
foreach ($xp->query('/root/property/rent[term = "short"]') as $item) {
$newItem = $destination->importNode($item, true);
$destRoot->appendChild($newItem);
$item->parentNode->removeChild($item);
}
$source->save("file/longrentals.xml");
$destination->formatOutput = true;
$destination->save("file/shortrentals.xml");
This works except the output in shortrentals.xml only contains the rent node not the top level parent Property node. Also the removed entry from longrentals.xml only removes the Rent child node. So, how do I go up a level using my code please?
You can use the parentNode attribute of a DOMNode to go up a level in the structure (similar to how you do it in the removeChild code)...
foreach ($xp->query('/root/property/rent[term = "short"]') as $item) {
$property = $item->parentNode;
$newItem = $destination->importNode($property, true);
$destRoot->appendChild($newItem);
$property->parentNode->removeChild($property);
}
Alternatively, consider XSLT, the special-purpose XML transformation language, to create both such XML files without foreach loops. Here, XSLT is embedded as string but can be parsed from file like any other XML file. Assumed XML structure: <root><property><rent>...
shortrentals.xml output
// Load XML and XSL sources
$xml = new DOMDocument;
$xml->load('file/rentals.xml');
$xslstr = '<?xml version="1.0" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/root">
<xsl:copy>
<xsl:apply-templates select="property[rent/term=\'short\']"/>
</xsl:copy>
</xsl:template>
<xsl:template match="property">
<xsl:copy>
<xsl:copy-of select="*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>';
$xsl = new DOMDocument;
$xsl->loadXML($xslstr);
// Configure transformer
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
// Transform XML source
$newXML = new DOMDocument;
$newXML = $proc->transformToXML($xml);
// Output file
file_put_contents('file/shortrentals.xml', $newXML);
longrentals.xml (Using Identity Transform and empty template to remove nodes)
// Load XML and XSL sources
$xml = new DOMDocument;
$xml->load('file/rentals.xml');
$xslstr = '<?xml version="1.0" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- Identity Transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!-- Remove Non-Short Terms -->
<xsl:template match="property[rent/term=\'short\']"/>
</xsl:stylesheet>';
$xsl = new DOMDocument;
$xsl->loadXML($xslstr);
// Configure transformer
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
// Transform XML source
$newXML = new DOMDocument;
$newXML = $proc->transformToXML($xml);
// Output file
file_put_contents('file/longrentals.xml', $newXML);

How to create XML file with top 50 records from another XML file in PHP

I have an XML file and has 300 elements. I just want to pull 10 latest records from it and create another XML file.
I will really appreciate if you can just give me some ideas about it?
PHP
$file = '/directory/xmlfile.xml';
if(!$xml = simplexml_load_file($file)){
exit('Failed to open '.$file);
} else{
print_r($xml);
// I want to do some logic here to retrieve top 10 records from file and then create another xml file with 10 records
}
XML Sample Data
<data>
<total>212</total>
<start>0</start>
<count>212</count>
<data>
<item0>
<id>123</id>
<title>abc-test1</title>
<clientContact>
<id>111</id>
<firstName>abc</firstName>
<lastName>xyz</lastName>
<email>abc#xyz.ca</email>
</clientContact>
<isOpen>1</isOpen>
<isPublic>1</isPublic>
<isJobcastPublished>1</isJobcastPublished>
<owner>
<id>222</id>
<firstName>testname</firstName>
<lastName>testlastname</lastName>
<address>
<address1>test address,</address1>
<address2>test</address2>
<city>City</city>
<state>state</state>
<zip>2222</zip>
<countryID>22</countryID>
<countryName>Country</countryName>
<countryCode>ABC</countryCode>
</address>
<email>test#test.com</email>
<customText1>test123</customText1>
<customText2>testxyz</customText2>
</owner>
<publicDescription>
<p>test info</p>
</publicDescription>
<status>test</status>
<dateLastModified>22222</dateLastModified>
<customText4>test1</customText4>
<customText10>test123</customText10>
<customText11>test</customText11>
<customText16>rtest</customText16>
<_score>123</_score>
</item0>
<item1>
...
</item1>
...
</data>
</data>
Consider XSLT, the special-purpose language designed to transform/manipulate XML to various end uses like extracting top ten <item*> tags. No need of foreach or if logic. PHP maintains an XSLT processor that can be enabled in .ini file (php-xsl).
Specifically, XSLT runs the Identity Transform to copy document as is then writes a blank template for item nodes with position over 10. XML was a bit difficult due to same parent/child <data> tags.
XSLT (save as .xsl file which is a well-formed xml)
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[substring(name(),1,4)='item' and position() > 10]"/>
</xsl:stylesheet>
PHP
$file = '/directory/xmlfile.xml';
if(!$xml = simplexml_load_file($file)) {
exit('Failed to open '.$file);
} else {
// Load XSLT
$xsl = new DOMDocument;
$xsl->load('/path/to/xsl_script.xsl');
// Configure transformer
$proc = new XSLTProcessor;
$proc->importStyleSheet($xsl);
// Transform XML source
$newXML = new DOMDocument;
$newXML = $proc->transformToXML($xml);
// Echo new XML tree
echo $newXML;
// Save output to file
$xmlfile = '/path/to/output.xml';
file_put_contents($xmlfile, $newXML);
}

How to distinguish between empty element and null-size string in DOMDocument?

I have trouble to load XML document into DOM preserving empty tags and null-size strings. Here the example:
$doc = new DOMDocument("1.0", "utf-8");
$root = $doc->createElement("root");
$doc->appendChild($root);
$element = $doc->createElement("element");
$root->appendChild($element);
echo $doc->saveXML();
produces following XML:
<?xml version="1.0" encoding="utf-8"?>
<root><element/></root>
Empty element, exactly as expected. Now let's add empty text node into element.
$doc = new DOMDocument("1.0", "utf-8");
$root = $doc->createElement("root");
$doc->appendChild($root);
$element = $doc->createElement("element");
$element->appendChild($doc->createTextNode(""));
$root->appendChild($element);
echo $doc->saveXML();
produces following XML:
<?xml version="1.0" encoding="utf-8"?>
<root><element></element></root>
Non-empty element with null-size string. Good! But when I am trying to do:
$doc = new DOMDocument();
$doc->loadXML($xml);
echo $doc->saveXML($doc);
on these XML documents I always get
<?xml version="1.0" encoding="utf-8"?>
<root><element/></root>
ie null-size string is removed and just empty element is loaded. I believe it happens on loadXML(). Is there any way to convince DOMDocument loadXML() not to convert null-size string into empty element? It would be preferable if DOM would have TextNode with null-size string as element's child.
Solution is needed to be in PHP DOM due to the way what would happen to the loaded data further.
The problem to distinguish between those two is, that when DOMDocument loads the XML serialized document, it does only follow the specs.
By the book, in <element></element> there is no empty text-node in that element - which is what others have commented already as well.
However DOMDocument is perfectly fine if you insert an empty text-node there your own. Then you can easily distinguish between a self-closing tag (no children) and an empty element (having one child, an empty text-node).
So how to enter those empty text-nodes? For example by using from the XMLReader based XMLReaderIterator library, specifically the DOMReadingIteration, which is able to build up the document, while offering each current XMLReader node for interaction:
$doc = new DOMDocument();
$iterator = new DOMReadingIteration($doc, $reader);
foreach ($iterator as $index => $value) {
// Preserve empty elements as non-self-closing by making them non-empty with a single text-node
// children that has zero-length text
if ($iterator->isEndElementOfEmptyElement()) {
$iterator->getLastNode()->appendChild(new DOMText(''));
}
}
echo $doc->saveXML();
That gives for your input:
<?xml version="1.0" encoding="utf-8"?>
<root><element></element></root>
This output:
<?xml version="1.0"?>
<root><element></element></root>
No strings attached. A fine build DOMDocument. The example is from examples/read-into-dom.php and a fine proof that it is no problem when you load the document via XMLReader and you deal with that single special case you have.
Here is no difference for the loading XML parser. The DOM is exactly the same.
If you load/save a XML format that has a problem with empty tags, you can use an option to avoid the empty tags on save:
$dom = new DOMDocument();
$dom->appendChild($dom->createElement('foo'));
echo $dom->saveXml();
echo "\n";
echo $dom->saveXml(NULL, LIBXML_NOEMPTYTAG);
Output:
<?xml version="1.0"?>
<foo/>
<?xml version="1.0"?>
<foo></foo>
You can trick XSLT processors to not use self-closing elements, by pretending a xsl:value-of inserting a variable, but that variable being an empty string ''.
Input:
<?xml version="1.0" encoding="utf-8"?>
<root>
<foo>
<bar some="value"></bar>
<self-closing attr="foobar" val="3.5"/>
</foo>
<goo>
<gle>
<nope/>
</gle>
</goo>
</root>
Stylesheet:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[not(node())]">
<xsl:copy>
<xsl:for-each select="#*">
<xsl:attribute name="{name()}">
<xsl:value-of select="."/>
</xsl:attribute>
</xsl:for-each>
<xsl:value-of select="''"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Output:
<?xml version="1.0" encoding="utf-8"?>
<root>
<foo>
<bar some="value"></bar>
<self-closing attr="foobar" val="3.5"></self-closing>
</foo>
<goo>
<gle>
<nope></nope>
</gle>
</goo>
</root>
To solve this in PHP without the use of a XSLT processor, I can only think of adding empty text nodes to all elements with no children (like you do in the creation of the XML).

Categories