I have this XML:
<destinos>
<destino>
<location>Spain</location>
<programas>
<item></item>
<item></item>
</programas>
</destino>
<destino>
<location>France</location>
<programas>
<item></item>
<item></item>
</programas>
</destino>
</destinos>
I need to include or copy the value of "Location" within each "item" and I am not able to do so.
<destinos>
<destino>
<location>Spain</location>
<programas>
<item>
<location>Spain</location>
</item>
<item>
<location>Spain</location>
</item>
</programas>
</destino>
<destino>
<location>France</location>
<programas>
<item>
<location>France</location>
</item>
<item>
<location>France</location>
</item>
</programas>
</destino>
</destinos>
I have no knowledge of PHP and I have been reading but I can't find the solution.
If someone could help me and explain I would be very grateful.
My code:
$url = file_get_contents("archive.xml");
$xml = simplexml_load_string($url);
$changes = $xml->xpath("//*[starts-with(local-name(), 'item')]");
foreach ($changes as $change)
$change[0] = $xml->destinos->destino->location;
header('Content-Type: application/xml');
echo $xml->asXML();
One option could be to use xpath with addChild with with the value of the location:
$url = file_get_contents("archive.xml");
$xml = simplexml_load_string($url);
$changes = $xml->xpath("/destinos/destino");
foreach ($changes as $change) {
$text = (string)$change->location;
foreach ($change->xpath("programas/item") as $i) {
$i->addChild("location", $text);
}
}
header('Content-Type: application/xml');
echo $xml->asXML();
Output
<destinos>
<destino>
<location>Spain</location>
<programas>
<item><location>Spain</location></item>
<item><location>Spain</location></item>
</programas>
</destino>
<destino>
<location>France</location>
<programas>
<item><location>France</location></item>
<item><location>France</location></item>
</programas>
</destino>
</destinos>
Php demo
Using SimpleXML, you can just use object notation to access the various elements of the document, this stops the need for XPath and can also make the code more readable...
$url = file_get_contents("archive.xml");
$xml = simplexml_load_string($url);
foreach ($xml->destino as $destino) {
// Process each item
foreach ( $destino->programas->item as $item ) {
// Set the location from the destino location value
$item->location = (string)$destino->location;
}
}
header('Content-Type: application/xml');
echo $xml->asXML();
One thing to note is that when using SimpleXML, the root node (<destinos> in this case) is the $xml object. This is why $xml->destino is accessing the <destino> elements.
With DOM you can append clones of the location nodes to the respective item elements.
$document = new DOMDocument();
$document->load($url);
$xpath = new DOMXpath($document);
// iterate the location child of the destino elements
foreach($xpath->evaluate('//destino/location') as $location) {
// iterate the item nodes inside the same parent node
foreach ($xpath->evaluate('parent::*/programas/item', $location) as $item) {
// append a copy of the location to the item
$item->appendChild($location->cloneNode(TRUE));
}
}
echo $document->saveXML();
Related
I'm trying to modify a 130mb+ XML file via PHP so it only shows the results where a child node is a specific value. I'm trying to filter this because of limitations via the software we're using to import the XML into our website.
Example: (mockup data)
<Items>
<Item>
<Barcode>...</Barcode>
<BrandCode>...</BrandCode>
<Title>...</Title>
<Content>...</Content>
<ShowOnWebsite>false</BrandDescr>
</Item>
<Item>
<Barcode>...</Barcode>
<BrandCode>...</BrandCode>
<Title>...</Title>
<Content>...</Content>
<ShowOnWebsite>true</BrandDescr>
</Item>
<Item>
<Barcode>...</Barcode>
<BrandCode>...</BrandCode>
<Title>...</Title>
<Content>...</Content>
<ShowOnWebsite>false</BrandDescr>
</Item>
</Items>
Desired result:
I want to create a new XML file with only the records where the child "ShowOnWebsite" is true.
Problems I've run into
Because the XML is so large simple solutions like using SimpleXML or loading the XML into the body and editing the nodes in there don't work. Because they all read the entire file into memory which is too slow and usually fails.
I've also looked at prewk/xml-string-streamer (https://github.com/prewk/xml-string-streamer) which is great for streaming large XML files because it doesn't place them in memory, although I can't find any way to modify the XML via that solution. (Other online posts say you need to have the nodes in memory to edit them).
Anyone got an idea on how to tackle this problem?
Goal
Desired result: I want to create a new XML file with only the records where the child "ShowOnWebsite" is true.
Given
test.xml
<Items>
<Item>
<Barcode>...</Barcode>
<BrandCode>...</BrandCode>
<Title>...</Title>
<Content>...</Content>
<ShowOnWebsite>false</ShowOnWebsite>
</Item>
<Item>
<Barcode>...</Barcode>
<BrandCode>...</BrandCode>
<Title>...</Title>
<Content>...</Content>
<ShowOnWebsite>true</ShowOnWebsite>
</Item>
<Item>
<Barcode>...</Barcode>
<BrandCode>...</BrandCode>
<Title>...</Title>
<Content>...</Content>
<ShowOnWebsite>false</ShowOnWebsite>
</Item>
</Items>
Code
This is the implementation I wrote. The getItems yields the childs without loading the xml at once into the memory.
function getItems($fileName) {
if ($file = fopen($fileName, "r")) {
$buffer = "";
$active = false;
while(!feof($file)) {
$line = fgets($file);
$line = trim(str_replace(["\r", "\n"], "", $line));
if($line == "<Item>") {
$buffer .= $line;
$active = true;
} elseif($line == "</Item>") {
$buffer .= $line;
$active = false;
yield new SimpleXMLElement($buffer);
$buffer = "";
} elseif($active == true) {
$buffer .= $line;
}
}
fclose($file);
}
}
$output = new SimpleXMLElement('<?xml version="1.0" encoding="utf-8"?><Items></Items>');
foreach(getItems("test.xml") as $element)
{
if($element->ShowOnWebsite == "true") {
$item = $output->addChild('Item');
$item->addChild('Barcode', (string) $element->Barcode);
$item->addChild('BrandCode', (string) $element->BrandCode);
$item->addChild('Title', (string) $element->Title);
$item->addChild('Content', (string) $element->Content);
$item->addChild('ShowOnWebsite', $element->ShowOnWebsite);
}
}
$fileName = __DIR__ . "/test_" . rand(100, 999999) . ".xml";
$output->asXML($fileName);
Output
<?xml version="1.0" encoding="utf-8"?>
<Items><Item><Barcode>...</Barcode><BrandCode>...</BrandCode><Title>...</Title><Content>...</Content><ShowOnWebsite>true</ShowOnWebsite></Item></Items>
XMLReader has an expand() method, but XMLWriter is missing the counterpart. So I added a XMLWriter::collapse() method in FluentDOM.
This allows to read the XML with XMLReader, expand it to DOM, use DOM methods to filter/manipulate the it and write it back with XMLWriter:
require __DIR__.'/../../vendor/autoload.php';
// Create the target writer and add the root element
$writer = new \FluentDOM\XMLWriter();
$writer->openUri('php://stdout');
$writer->setIndent(2);
$writer->startDocument();
$writer->startElement('Items');
// load the source into a reader
$reader = new \FluentDOM\XMLReader();
$reader->open(getXMLAsURI());
// iterate the Item elements - the iterator expands them into a DOM node
foreach (new FluentDOM\XMLReader\SiblingIterator($reader, 'Item') as $item) {
/** #var \FluentDOM\DOM\Element $item */
// only "ShowOnWebsite = true"
if ($item('ShowOnWebsite = "true"')) {
// write expanded node to the output
$writer->collapse($item);
}
}
$writer->endElement();
$writer->endDocument();
function getXMLAsURI() {
$xml = <<<'XML'
<Items>
<Item>
<Barcode>...</Barcode>
<BrandCode>...</BrandCode>
<Title>...</Title>
<Content>...</Content>
<ShowOnWebsite>false</ShowOnWebsite>
</Item>
<Item>
<Barcode>...</Barcode>
<BrandCode>...</BrandCode>
<Title>...</Title>
<Content>...</Content>
<ShowOnWebsite>true</ShowOnWebsite>
</Item>
<Item>
<Barcode>...</Barcode>
<BrandCode>...</BrandCode>
<Title>...</Title>
<Content>...</Content>
<ShowOnWebsite>false</ShowOnWebsite>
</Item>
</Items>
XML;
return 'data://text/plain;base64,'.base64_encode($xml);
}
The XML file contains a 1000 product entries. Some of them for the country Portugal.
I would like to get only the products where the country is Portugal and write that information into a new XML file onto my server.
How would I do that in PHP?
The XML content structure:
<products>
<product ID="38450">
<name>Aparthotel Alfagar</name>
<price currency="EUR">239.00</price>
<URL>https://website.com/</URL>
<images>
<image>https://website.com/1.jpg</image>
<image>https://website.com/2.jpg</image>
<image>https://website.com/3.jpg</image>
</images>
<description>
<![CDATA[<p>some text</p>]]>
</description>
<categories/>
<properties>
<property name="country">
<value>Portugal</value>
</property>
<property name="lowestPrice">
<value>239.00</value>
</property>
<property name="lowestPriceDate">
<value>13-01-2020</value>
</property>
</properties>
<variations/>
</product>
<!-- more product entries -->
My approuch started out as this:
<?php
// Define source
$source_url = 'https://website.net/?encoding=utf-8&type=xml&id=';
// Define target
$file_url = '/home/website/public_html/media/';
$file_ext = '.xml';
// Load data
$array = simplexml_load_file($source_url.'654321');
// Filter data
$results_portugal = '';
foreach($array->product->properties->property->value['Portugal'] as $results) {
}
// Create datafiles
copy ($results_portugal,$file_url.'portugal'.$file_ext);
Obiously I got stuck pretty soon. Can anyone help me out please? Many thanks in advance!
You can fetch a part of an XML in SimpleXML or DOM using XPath expressions:
$products = new SimpleXMLElement($xml);
var_dump(
count(
$products->xpath('//product[properties/property[#name = "country"]/value = "Portugal"]')
)
);
var_dump(
count(
$products->xpath('//product[properties/property[#name = "country"]/value = "Spain"]')
)
);
However here is no "nice" way to copy nodes in SimpleXML. DOM allows that:
// create source document and load XML
$source = new DOMDocument();
$source->loadXML($xml);
$xpath = new DOMXpath($source);
// create target document and append root node
$target = new DOMDocument();
$target->appendChild($target->createElement('products'));
$expression = '//product[properties/property[#name = "country"]/value = "Portugal"]';
// iterate filtered nodes
foreach ($xpath->evaluate($expression) as $product) {
// import node into target document and append
$target->documentElement->appendChild(
$target->importNode($product, TRUE)
);
}
echo $target->saveXML();
For really large XMLs you need to use XMLReader/XMLWriter. They allow you to load only a part of the XML file into memory. Originally here is no easy way to copy nodes but I added this to FluentDOM.
// Create the target writer and add the root element
$writer = new \FluentDOM\XMLWriter();
$writer->openUri('php://stdout');
$writer->setIndent(2);
$writer->startDocument();
$writer->startElement('products');
// load the source into a reader
$reader = new \FluentDOM\XMLReader();
$reader->open('data://text/plain;base64,'.base64_encode($xml));
// iterate the product elements - the iterator expands them into a DOM node
foreach (new FluentDOM\XMLReader\SiblingIterator($reader, 'product') as $product) {
/** #var \FluentDOM\DOM\Element $product */
// validate country property
if ($product('properties/property[#name = "country"]/value = "Portugal"')) {
// write expanded node to the output
$writer->collapse($product);
}
}
$writer->endElement();
$writer->endDocument();
Following is my XML file i want to update the doller and cent values which are inside latestBid. I first tried the doller values but it's not working. i even tried to update the description ('//item[id="4"]/description') even that didn't work. Please tell me what i'm doing wrong here.
XML file
<?xml version="1.0"?>
<items>
<item>
<itemNumber>4</itemNumber>
<latestBid>
<latestCustomerId>1</latestCustomerId>
<bidPrice>
<doller>2342</doller>
<cent>23</cent>
</bidPrice>
</latestBid>
</item>
<item>
<itemNumber>5</itemNumber>
<latestBid>
<latestCustomerId>1</latestCustomerId>
<bidPrice>
<doller>35345</doller>
<cent>78</cent>
</bidPrice>
</latestBid>
</item>
</items>
PHP file
<?php
$url = '../../data/auction2.xml';
$itemNumber ="4";
$bidDoller = 45;
$bidCent=55;
$doc = new DomDocument();
$xml=simplexml_load_file($url);
//echo "came 1";working
foreach ($xml->xpath('//item[#itemNumber="4"]/latestBid/bidPrice/doller') as $desc) {
echo "came 2";//nt working
$dom=dom_import_simplexml($desc);
$dom->nodeValue = $bidDoller;
}
file_put_contents($url, $xml->asXML());
?>
edited. Still not working
thank you every one for the support by editing and answering I finally did it. since it wasn't easy for me to do this i'm posting the answer to help someone like me :).
i didn't change the xml.
php file
$url = '../../data/auction2.xml';
$itemNumber ="4";
$bidDoller = 85;
$bidCent=95;
$xml=simplexml_load_file($url);
$resultDoller= $xml->xpath('//item[itemNumber="'.$itemNumber.'"]/latestBid/bidPrice/doller');
$resultCent= $xml->xpath('//item[itemNumber="'.$itemNumber.'"]/latestBid/bidPrice/cent');
$resultDoller[0][0]=$bidDoller;
$resultCent[0][0]=$bidCent;
print $xml->asXML();
file_put_contents($url, $xml->asXML());
Following worked for me,
//XML
<?xml version="1.0"?>
<items>
<item id="4">
<itemNumber>4</itemNumber>
<latestBid>
<latestCustomerId>1</latestCustomerId>
<bidPrice>
<doller>2342</doller>
<cent>23</cent>
</bidPrice>
</latestBid>
</item>
<item>
<itemNumber>5</itemNumber>
<latestBid>
<latestCustomerId>1</latestCustomerId>
<bidPrice>
<doller>35345</doller>
<cent>78</cent>
</bidPrice>
</latestBid>
</item>
</items>
//PHP
<?php
$url = '../../data/auction2.xml';
$itemNumber ="4";
$bidDoller = 45;
$bidCent=55;
$doc = new DomDocument();
$xml=simplexml_load_file($url);
$result = $xml->xpath('//item[#id="4"]/latestBid/bidPrice/doller');
echo "<pre>";
print_r($result);
//echo "came 1";working
foreach ($xml->xpath('//item[#id="4"]/latestBid') as $desc) {
echo "came 2";//nt working
$dom=dom_import_simplexml($desc);
$dom->nodeValue = $bidDoller;
}
//file_put_contents($url, $xml->asXML());
?>
I am new to PHP and XML.
Can somebody tell me how can I get the values of a sub element or child node of a an xml element?
index.php
$domdoc = new DOMDocument();
$domdoc->load('actionstars.xml');
foreach ($domdoc->getElementsByTagName("actionstar") as $star) {
echo $star->item(0)->nodeValue; // displays the <id> element
echo $star->item(1)->nodeValue; // displays the <name> element
echo "<br />";
}
actionstars.xml
<?xml version="1.0" encoding="ISO-8859-1"?>
<actionstars>
<actionstar>
<id>1</id>
<name>Jean Claude Van Damme</name>
</actionstar>
<actionstar>
<id>2</id>
<name>Scott Adkins</name>
</actionstar>
<actionstar>
<id>3</id>
<name>Dolph Ludgren</name>
</actionstar>
<actionstar>
<id>4</id>
<name>Michael Jai White</name>
</actionstar>
<actionstar>
<id>5</id>
<name>Michael Worth</name>
</actionstar>
</actionstars>
Pls help...
If you can guarantee their order, you can use childNodes and the offset, otherwise...
$domdoc = new DOMDocument();
$domdoc->load('actionstars.xml');
foreach ($domdoc->getElementsByTagName("actionstar") as $star) {
echo $shit->getElementsByTagName('id')->item(0)->nodeValue; // displays the <id> element
echo $shit->getElementsByTagName('name')->item(0)->nodeValue; // displays the <name> element
echo "<br />";
}
I'm trying to build a rather complex XML document.
I have a bunch of sections of the XML document that repeats. I thought I'd use multiple string templates as base document for the sections and create instances of XML elements using simplexml_load_string.
So I have one instance of SimpleXMLElement as the base document
$root =
simplexml_load_string($template_root);
then I loop through some items in my database, create new SimpleXMLElement, something like this:
for (bla bla bla):
$item = simplexml_load_string($template_item);
// do stuff with item
// try to add item to the root document..
// Stuck here.. can't do $root->items->addChild($item)
endfor;
I can't call addChild because it just expects a tag name and value.. you can't addChild another SimpleXMLElement.
Am I missing something here? seems really dumb that addChild can't take a SimpleXMLELement as a parameter.
Is there any other way to do this? (apart from using a different xml lib)
As far as I know, you can't do it with SimpleXML because addChild doesn't make a deep copy of the element (being necessary to specify the tag name can easily be overcome by calling SimpleXMLElement::getName()).
One solution would be to use DOM instead:
With this function:
function sxml_append(SimpleXMLElement $to, SimpleXMLElement $from) {
$toDom = dom_import_simplexml($to);
$fromDom = dom_import_simplexml($from);
$toDom->appendChild($toDom->ownerDocument->importNode($fromDom, true));
}
We have for
<?php
header("Content-type: text/plain");
$sxml = simplexml_load_string("<root></root>");
$n1 = simplexml_load_string("<child>one</child>");
$n2 = simplexml_load_string("<child><k>two</k></child>");
sxml_append($sxml, $n1);
sxml_append($sxml, $n2);
echo $sxml->asXML();
the output
<?xml version="1.0"?>
<root><child>one</child><child><k>two</k></child></root>
See also some user comments that use recursive functions and addChild, e.g. this one.
You could use this function that is based in creating the children with attributes from the source:
function xml_adopt($root, $new) {
$node = $root->addChild($new->getName(), (string) $new);
foreach($new->attributes() as $attr => $value) {
$node->addAttribute($attr, $value);
}
foreach($new->children() as $ch) {
xml_adopt($node, $ch);
}
}
$xml = new SimpleXMLElement("<root/>");
$child = new SimpleXMLElement("<content><p a=\"aaaaaaa\">a paragraph</p><p>another <br/>p</p></content>");
xml_adopt($xml, $child);
echo $xml->asXML()."\n";
This will produce:
<?xml version="1.0"?>
<root><content><p a="aaaaaaa">a paragraph</p><p>another p<br/></p></content></root>
The xml_adopt() example doesn't preserve namespace nodes.
My edit was rejected because it changed to much? was spam?.
Here is a version of xml_adopt() that preserves namespaces.
function xml_adopt($root, $new, $namespace = null) {
// first add the new node
// NOTE: addChild does NOT escape "&" ampersands in (string)$new !!!
// replace them or use htmlspecialchars(). see addchild docs comments.
$node = $root->addChild($new->getName(), (string) $new, $namespace);
// add any attributes for the new node
foreach($new->attributes() as $attr => $value) {
$node->addAttribute($attr, $value);
}
// get all namespaces, include a blank one
$namespaces = array_merge(array(null), $new->getNameSpaces(true));
// add any child nodes, including optional namespace
foreach($namespaces as $space) {
foreach ($new->children($space) as $child) {
xml_adopt($node, $child, $space);
}
}
}
(edit: example added)
$xml = new SimpleXMLElement(
'<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:media="http://search.yahoo.com/mrss/">
<channel></channel></rss>');
$item = new SimpleXMLElement(
'<item xmlns:media="http://search.yahoo.com/mrss/">
<title>Slide Title</title>
<description>Some description</description>
<link>http://example.com/img/image.jpg</link>
<guid isPermaLink="false">A1234</guid>
<media:content url="http://example.com/img/image.jpg" medium="image" duration="15">
</media:content>
</item>');
$channel = $xml->channel;
xml_adopt($channel, $item);
// output:
// Note that the namespace is (correctly) only preserved on the root element
'<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
<channel>
<item>
<title>Slide Title</title>
<description>Some description</description>
<link>http://example.com/img/image.jpg</link>
<guid isPermaLink="false">A1234</guid>
<media:content url="http://example.com/img/image.jpg" medium="image" duration="15">
</media:content>
</item>
</channel>
</rss>'