Read and take value of XML attributes - php

I am stock with this XML problem, I have a XML file that I browse to find values. Everything is working fine, I can read on all the child nodes, but I am stuck on this section. The XML portion containing photos are all the same name of the node, except for an attribute, how can I specify how to browse according to this and take the filename value of each of them
XML
...
<Engine>
<Fuel>Unleaded</Fuel>
<Cylinders>4</Cylinders>
<Induction>Normally aspirated</Induction>
</Engine>
<Photo order="1">
<Filename>http://usedcarpics.s3.amazonaws.com/514SPINELLITOYOTA2/b5092588_2.jpg</Filename>
</Photo>
<Photo order="2">
<Filename>http://usedcarpics.s3.amazonaws.com/514SPINELLITOYOTA2/b5092588_3.jpg</Filename>
</Photo>
<Photo order="3">
<Filename>http://usedcarpics.s3.amazonaws.com/514SPINELLITOYOTA2/b5092588_4.jpg</Filename>
</Photo>
<Photo order="4">
<Filename>http://usedcarpics.s3.amazonaws.com/514SPINELLITOYOTA2/b5092588_5.jpg</Filename>
</Photo>
<Photo order="5">
<Filename>http://usedcarpics.s3.amazonaws.com/514SPINELLITOYOTA2/b5092588_6.jpg</Filename>
...
In my php file, I have this code that help me find the values:
$import->stock_no =(string)$item->Invoice->Vehicle->VehicleStock;
$import->image1 =(string)$item->Invoice->Vehicle->Photo->attributes(order="1")->Filename;
Of course it doesn't work, how can I browse all the photo nodes( I have 8 pictures I need to take the values from)
I want to have $import->image1 = (filename in the attibutes of pohoto 1), sames for image 2, 3, etc.
Thank you.

What you try to achieve is (first of all) possible by using an xpath query. You want to access a child-node based on an attribute value. The better reference questions in SimpleXML are:
Implementing condition in XPath
SimpleXML get element content based on attribute value
It's also since some days when the suggestion was given to extend form SimpleXMLElement to provide a utility function to actually do that with an easy interface:
PHP/XML - how to read multible sub's
simplexml_load_file - redundant element with empty value is converted to new SimpleXMLElement Object
However your case is a little different because of the syntax you suggest:
$xml = simplexml_load_string($buffer, 'MySimpleXMLElement');
echo $xml->Vehicle->Photo->attribute("order", "1")->Filename;
// prints "http://usedcarpics.s3.amazonaws.com/514SPINELLITOYOTA2/b5092588_2.jpg"
Instead of using an ordinary SimpleXMLElement this example uses an extended one named (exemplary) MySimpleXMLElement. It runs an XPath query inside based on the input parameters and based on the parent element it operates on (here being a Photo element):
/**
* Class MySimpleXMLElement
*
* Example of how to magically access named child-nodes based
* on an attribute value of theirs.
*/
class MySimpleXMLElement extends SimpleXMLElement
{
public function attribute($name, $value) {
$nodes = $this->xpath(
sprintf('../%s[#%s = "%s"]', $this->getName(), $name, $value)
);
return $nodes ? $nodes[0] : NULL;
}
}
This new MySimpleXMLElement::attribute() method (sorry attributes() was already in use) is then available on every node. So have fun.
Naturally you can also write it this way:
$xml = simplexml_load_string($buffer);
echo $xml->Vehicle->xpath('Photo[#order="1"]')[0]->Filename;
// prints "http://usedcarpics.s3.amazonaws.com/514SPINELLITOYOTA2/b5092588_2.jpg"
the extended SimpleXMLElement is mainly for convenience reasons. And it's probably more easy to debug in case you're not fluent with Xpath yet.
Last time I extended SimpleXMLElement on Stackoverflow was in the said answer to the "simplexml_load_file - redundant element with empty value is converted to new SimpleXMLElement Object" question.

Try this
<?php
$xml = '<Engine>
<Fuel>Unleaded</Fuel>
<Cylinders>4</Cylinders>
<Induction>Normally aspirated</Induction>
</Engine>
<Photo order="1">
<Filename>http://usedcarpics.s3.amazonaws.com/514SPINELLITOYOTA2/b5092588_2.jpg</Filename>
</Photo>
<Photo order="2">
<Filename>http://usedcarpics.s3.amazonaws.com/514SPINELLITOYOTA2/b5092588_3.jpg</Filename>
</Photo>';
$xml="<Wraper>".$xml."</Wraper>";
$parse=new SimpleXMLElement($xml);
echo "Engine Fuel:".$parse->Engine->Fuel;
echo "<br/>Engine Cylinders:".$parse->Engine->Cylinders;
echo "Photos<br/>";
foreach ($parse->Photo as $photo)
{
echo "<br/>Photo Order: ".$photo->attributes();
echo "<br/>Photo URL: ".$photo->Filename;
echo "<hr/>";
}
?>

sweet and simple with xpath:
$xml = simplexml_load_string($x); // assume XML in $x
$photos = $xml->xpath("//Photo"); // select all Photo nodes and their children in an array
foreach ($photos as $photo)
echo "order: $photo[order], file: $photo->Filename<br />"; // simple output
see it working: http://3v4l.org/SJmEg

Related

PHP: How to force simplexml to use certain datatype for a node

How can one enforce simplexml_load_string( ) to use same data structure at each node point.
$xml = "
<level1>
<level2>
<level3>Hello</level3>
<level3>stackoverflow</level3>
</level2>
<level2>
<level3>My problem</level3>
</level2>
</level1>";
$xmlObj = simplexml_load_string($xml)
var_dump($xmlObj);
Examining the output,
level1 is an object; level2 is an array; level2[0] is an array.
level2[1] is an object, because there's only one child node, which I'll rather have as a single index array.
I'm collecting the xml from user, and there may be 1 or more nodes inside each level2. My sanitisation block is a foreach loop which fails when there's only one node inside level2.
The sanitation block looks something like this
foreach($xmlObj -> level2 as $lvl2){
if($lvl2 -> level3[0] == 'condition'){ doSomething( ); }
}
doSomething() works fine when <level2> always has more than one child node in the xml string. If <level2> has only one child <level3> node, an error about trying to get attribute of a non-object comes up.
var_dump shows that the data type changes from object to array depending on how many nodes are nested within.
I'll prefer a way to ensure <level2> to always be an array regardless of how many children are within. That saves me from editing too much. But any other way out would suffice.
Thanks
It is not an information available in the XML itself. So you will have to add it in your implementation. SimpleXML provides both list and item access to a child elements. If you access it as a list (for example with foreach) it will provide all matching child elements.
$xml = "
<level1>
<level2>
<level3>Hello</level3>
<level3>stackoverflow</level3>
</level2>
<level2>
<level3>My problem</level3>
</level2>
</level1>";
$level1 = new SimpleXMLElement($xml);
$result = [];
foreach($level1->level2 as $level2) {
$data2 = [];
foreach ($level2->level3 as $level3) {
$data2[] = (string)$level3;
}
$result[] = $data2;
}
var_dump($result);
So the trick is to use the SimpleXMLElement instance directly and not convert it into an array. Do not treat the creation of your JSON structure as a generic conversion. Build up a specific output while reading the XML using SimpleXML.

PHP - Unable to parse attribute using SimpleXML

Given the following xml:
<data xmlns:ns2="...">
<versions>
<ns2:version type="HW">E</ns2:version>
<ns2:version type="FW">3160</ns2:version>
<ns2:version type="SW">3.4.1 (777)</ns2:version>
</versions>
...
</data>
I am trying to parse the third attribute ~ns2:version type="SW" but when running the following code I get nothing..
$s = simplexml_load_file('data.xml');
echo $s->versions[2]->{'ns2:version'};
Running this gives the following output:
$s = simplexml_load_file('data.xml');
var_dump($s->versions);
How can I properly get that attribute?
You've got some quite annoying XML to work with there, at least as far as SimpleXML is concerned.
Your version elements are in the ns2 namespace, so in order to loop over them, you need to do something like this:
$s = simplexml_load_string($xml);
foreach ($s->versions[0]->children('ns2', true)->version as $child) {
...
}
The children() method returns all children of the current tag, but only in the default namespace. If you want to access elements in other namespaces, you can pass the local alias and the second argument true.
The more complicated part is that the type attributes is not considered to be part of this same namespace. This means you can't use the standard $element['attribute'] form to access it, since your element and attribute are in different namespaces.
Fortunately, SimpleXML's attributes() method works in the same way as children(), and so to access the attributes in the global namespace, you can pass it an empty string:
$element->attributes('')->type
In full, this is:
$s = simplexml_load_string($xml);
foreach ($s->versions[0]->children('ns2', true)->version as $child) {
echo (string) $child->attributes()->type, PHP_EOL;
}
This will get you the output
HW
FW
SW
To get the third attribute.
$s = simplexml_load_file('data.xml');
$sxe = new SimpleXMLElement($s);
foreach ($sxe as $out_ns) {
$ns = $out_ns->getNamespaces(true);
$child = $out_ns->children($ns['ns2']);
}
echo $child[2];
Out put:
3.4.1 (777)

Looping through SimpleXMLElement to access attributes

I am trying process data retrieved with SimpleXML and am having great difficulty. I have read numerous threads here about this subject, they all LOOK like what I am doing, but mine are not working. Here's what I've got:
<ROOT>
<ROWS COMP_ID="165462">
<ROWS COMP_ID="165463">
</ROOT>
My code:
$xml = simplexml_load_file('10.xml');
foreach( $xml->ROWS as $comp_row ) {
$id = $comp_row->COMP_ID;
}
As I step through this in my debugger, I can see that $id is not set to the string value of COMP_ID, but becomes a SimpleXMLElement itself containing the CLASSNAME object. I've tried many variations of addressing this attribute but none work, including $comp_row->attributes()->COMP_ID and others.
What am I missing?
SimpleXML is an array-like object. Cheat sheet:
Unprefixed child elements as numeric-index or traversable
Does not include prefixed elements (NOTE, I really mean prefixed, not null-namespace! SimpleXMLElement handling of namespaces is a strange and arguably broken.)
first child: $sxe[0]
new SimpleXMLElement with a subset of matching elements: $sxe->ROWS, $sxe->{'ROWS'}
iterate children: foreach ($sxe as $e), $sxe->children()
Text content: (string) $sxe. SimpleXMLElement always returns another SimpleXMLElement, so if you need a string cast it explicitly!
Prefixed child elements:
$sxe->children('http://example.org') returns a new SimpleXMLElement with elements
in the matching namespace, with namespace stripped so you can use it like the previous section.
Attributes in null namespace as key-index:
specific attribute: `$sxe['attribute-name']
all attributes: $sxe->attributes()
$sxe->attributes() returns a special SimpleXMLElement that shows attributes as both child elements and attributes, so both the following work:
$sxe->attributes()->COMP_ID
$a = $sxe->attributes(); $a['COMP_ID'];
Value of an attribute: coerce to string (string) $sxe['attr-name']
Attributes in other namespaces:
all attributes: $sxe->attributes('http://example.org')
specific attribute: $sxe_attrs = $sxe->attributes('http://example.org'); $sxe_attrs['attr-name-without-prefix']
What you want is:
$xml = '<ROOT><ROWS COMP_ID="165462"/><ROWS COMP_ID="165463"/></ROOT>';
$sxe = simplexml_load_string($xml);
foreach($sxe->ROWS as $row) {
$id = (string) $row['COMP_ID'];
}
You're missing...
foreach( $xml->ROWS as $comp_row ) {
foreach ($comp_row->attributes() as $attKey => $attValue) {
// i.e., on first iteration: $attKey = 'COMP_ID', $attValue = '165462'
}
}
PHP Manual: SimpleXMLElement::attributes

How to get values inside <![CDATA[values]] > using php DOM?

How can i get values inside <![CDATA[values]] > using php DOM.
This is few code from my xml.
<Destinations>
<Destination>
<![CDATA[Aghia Paraskevi, Skiatos, Greece]]>
<CountryCode>GR</CountryCode>
</Destination>
<Destination>
<![CDATA[Amettla, Spain]]>
<CountryCode>ES</CountryCode>
</Destination>
<Destination>
<![CDATA[Amoliani, Greece]]>
<CountryCode>GR</CountryCode>
</Destination>
<Destination>
<![CDATA[Boblingen, Germany]]>
<CountryCode>DE</CountryCode>
</Destination>
</Destinations>
Working with PHP DOM is fairly straightforward, and is very similar to Javascript's DOM.
Here are the important classes:
DOMNode — The base class for anything that can be traversed inside an XML/HTML document, including text nodes, comment nodes, and CDATA nodes
DOMElement — The base class for tags.
DOMDocument — The base class for documents. Contains the methods to load/save XML, as well as normal DOM document methods (see below).
There are a few staple methods and properties:
DOMDocument->load() — After creating a new DOMDocument, use this method on that object to load from a file.
DOMDocument->getElementsByTagName() — this method returns a node list of all elements in the document with the given tag name. Then you can iterate (foreach) on this list.
DOMNode->childNodes — A node list of all children of a node. (Remember, a CDATA section is a node!)
DOMNode->nodeType — Get the type of a node. CDATA nodes have type XML_CDATA_SECTION_NODE, which is a constant with the value 4.
DOMNode->textContent — get the text content of any node.
Note: Your CDATA sections are malformed. I don't know why there is an extra ]] in the first one, or an unclosed CDATA section at the end of the line, but I think it should simply be:
<![CDATA[Aghia Paraskevi, Skiatos, Greece]]>
Putting this all together we:
Create a new document object and load the XML
Get all Destination elements by tag name and iterate over the list
Iterate over all child nodes of each Destination element
Check if the node type is XML_CDATA_SECTION_NODE
If it is, echo the textContent of that node.
Code:
$doc = new DOMDocument();
$doc->load('test.xml');
$destinations = $doc->getElementsByTagName("Destination");
foreach ($destinations as $destination) {
foreach($destination->childNodes as $child) {
if ($child->nodeType == XML_CDATA_SECTION_NODE) {
echo $child->textContent . "<br/>";
}
}
}
Result:
Aghia Paraskevi, Skiatos, Greece
Amettla, Spain
Amoliani, Greece
Boblingen, Germany
Use this:
$parseFile = simplexml_load_file($myXML,'SimpleXMLElement', LIBXML_NOCDATA)
and next :
foreach ($parseFile->yourNode as $node ){
etc...
}
Best and easy way
$xml = simplexml_load_string($xmlData, 'SimpleXMLElement', LIBXML_NOCDATA);
$xmlJson = json_encode($xml);
$xmlArr = json_decode($xmlJson, 1); // Returns associative array
Use replace CDATA before parsing PHP DOM element after that you can get the innerXml or innerHtml:
str_replace(array('<\![CDATA[',']]>'), '', $xml);
I use following code.
Its not only read all xml data with
<![CDATA[values]] >
but also convert xml object to php associative array. So we can apply loop on the data.
$xml_file_data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA),true), true);
Hope this will work for you.
function inBetweenOf(string $here, string $there, string $content) : string {
$left_over = strlen(substr($content, strpos($content, $there)));
return substr($content, strpos($content, $here) + strlen($here), -$left_over);
}
Iterate over "Destination" tags and then call inBetweenOf on each iteration.
$doc = inBetweenOf('<![CDATA[', ']]>', $xml);

In SimpleXML, how can I add an existing SimpleXMLElement as a child element?

I have a SimpleXMLElement object $child, and a SimpleXMLElement object $parent.
How can I add $child as a child of $parent? Is there any way of doing this without converting to DOM and back?
The addChild() method only seems to allow me to create a new, empty element, but that doesn't help when the element I want to add $child also has children. I'm thinking I might need recursion here.
Unfortunately SimpleXMLElement does not offer anything to bring two elements together. As #nickf wrote, it's more fitting for reading than for manipulation. However, the sister extension DOMDocument is for editing and you can bring both together via dom_import_simplexml(). And #salathe shows in a related answer how this works for specific SimpleXMLElements.
The following shows how this work with input checking and some more options. I do it with two examples. The first example is a function to insert an XML string:
/**
* Insert XML into a SimpleXMLElement
*
* #param SimpleXMLElement $parent
* #param string $xml
* #param bool $before
* #return bool XML string added
*/
function simplexml_import_xml(SimpleXMLElement $parent, $xml, $before = false)
{
$xml = (string)$xml;
// check if there is something to add
if ($nodata = !strlen($xml) or $parent[0] == NULL) {
return $nodata;
}
// add the XML
$node = dom_import_simplexml($parent);
$fragment = $node->ownerDocument->createDocumentFragment();
$fragment->appendXML($xml);
if ($before) {
return (bool)$node->parentNode->insertBefore($fragment, $node);
}
return (bool)$node->appendChild($fragment);
}
This exemplary function allows to append XML or insert it before a certain element, including the root element. After finding out if there is something to add, it makes use of DOMDocument functions and methods to insert the XML as a document fragment, it is also outlined in How to import XML string in a PHP DOMDocument. The usage example:
$parent = new SimpleXMLElement('<parent/>');
// insert some XML
simplexml_import_xml($parent, "\n <test><this>now</this></test>\n");
// insert some XML before a certain element, here the first <test> element
// that was just added
simplexml_import_xml($parent->test, "<!-- leave a comment -->\n ", $before = true);
// you can place comments above the root element
simplexml_import_xml($parent, "<!-- this works, too -->", $before = true);
// but take care, you can produce invalid XML, too:
// simplexml_add_xml($parent, "<warn><but>take care!</but> you can produce invalid XML, too</warn>", $before = true);
echo $parent->asXML();
This gives the following output:
<?xml version="1.0"?>
<!-- this works, too -->
<parent>
<!-- leave a comment -->
<test><this>now</this></test>
</parent>
The second example is inserting a SimpleXMLElement. It makes use of the first function if needed. It basically checks if there is something to do at all and which kind of element is to be imported. If it is an attribute, it will just add it, if it is an element, it will be serialized into XML and then added to the parent element as XML:
/**
* Insert SimpleXMLElement into SimpleXMLElement
*
* #param SimpleXMLElement $parent
* #param SimpleXMLElement $child
* #param bool $before
* #return bool SimpleXMLElement added
*/
function simplexml_import_simplexml(SimpleXMLElement $parent, SimpleXMLElement $child, $before = false)
{
// check if there is something to add
if ($child[0] == NULL) {
return true;
}
// if it is a list of SimpleXMLElements default to the first one
$child = $child[0];
// insert attribute
if ($child->xpath('.') != array($child)) {
$parent[$child->getName()] = (string)$child;
return true;
}
$xml = $child->asXML();
// remove the XML declaration on document elements
if ($child->xpath('/*') == array($child)) {
$pos = strpos($xml, "\n");
$xml = substr($xml, $pos + 1);
}
return simplexml_import_xml($parent, $xml, $before);
}
This exemplary function does normalize list of elements and attributes like common in Simplexml. You might want to change it to insert multiple SimpleXMLElements at once, but as the usage example shows below, my example does not support that (see the attributes example):
// append the element itself to itself
simplexml_import_simplexml($parent, $parent);
// insert <this> before the first child element (<test>)
simplexml_import_simplexml($parent->children(), $parent->test->this, true);
// add an attribute to the document element
$test = new SimpleXMLElement('<test attribute="value" />');
simplexml_import_simplexml($parent, $test->attributes());
echo $parent->asXML();
This is a continuation of the first usage-example. Therefore the output now is:
<?xml version="1.0"?>
<!-- this works, too -->
<parent attribute="value">
<!-- leave a comment -->
<this>now</this><test><this>now</this></test>
<!-- this works, too -->
<parent>
<!-- leave a comment -->
<test><this>now</this></test>
</parent>
</parent>
I hope this is helpful. You can find the code in a gist and as online demo / PHP version overview.
I know this isn't the most helpful answer, but especially since you're creating/modifying XML, I'd switch over to using the DOM functions. SimpleXML's good for accessing simple documents, but pretty poor at changing them.
If SimpleXML is treating you kindly in all other places and you want to stick with it, you still have the option of jumping over to the DOM functions temporarily to perform what you need to and then jump back again, using dom_import_simplexml() and simplexml_import_dom(). I'm not sure how efficient this is, but it might help you out.
Actually, it's possible (dynamically) if you look carefully on how addChild() is defined. I used this technique to convert any array into XML using recursion and pass-by-reference
addChild() returns SimpleXMLElement of added child.
to add leaf node, use $xml->addChilde($nodeName, $nodeValue).
to add a node which may have subnode or value, use
$xml->addChilde($nodeName), no value is passed to addChild(). This
will result in having a subnode of type SimpleXMLElement! not a
string!
target XML
<root>
<node>xyz</node>
<node>
<node>aaa</node>
<node>bbb</node>
</node>
</root>
Code:
$root = new SimpleXMLElement('<root />');
//add child with name and string value.
$root.addChild('node', 'xyz');
//adds child with name as root of new SimpleXMLElement
$sub = $root->addChild('node');
$sub.addChild('node', 'aaa');
$sub.addChild('node', 'bbb');
Leaving this here as I just stumbled upon this page and found that SimpleXML now supports this functionality through the ::addChild method.
You can use this method to do add any cascading elements as well:
$xml->addChild('parent');
$xml->parent->addChild('child');
$xml->parent->child->addChild('child_id','12345');

Categories