Get value from xml using the attribute with xpath in php - php

I want to get the value '23452345235' of the parameter with name="userID" from this xml:
<?xml version="1.0" encoding="UTF-8"?>
<callout>
<parameter name="UserID">
23452345235
</parameter>
<parameter name="AccountID">
57674567567
</parameter>
<parameter name="NewUserID">
54745674566
</parameter>
</callout>
I'm using this code:
$xml = simplexml_load_string($data);
$myDataObject = $xml->xpath('//parameter[#name="UserID"]');
var_dump($myDataObject);
And I'm getting this:
array(1) {
[0] =>
class SimpleXMLElement#174 (1) {
public $#attributes =>
array(1) {
'name' =>
string(6) "UserID"
}
}
}
I actually want to get the value of '23452345235' or receive the parameter in order to get this value.
What I'm doing wrong?

Well you can (optionally) put it under a loop. Like this:
$myDataObject = $xml->xpath('//parameter[#name="UserID"]');
foreach($myDataObject as $element) {
echo $element;
}
Or directly:
echo $myDataObject[0];
Actually is quite straightforward, as seen on your var_dump(), its an array, so access it as such.

SimpleXMLElement::xpath() can only return an array of SimpleXMLElement objects, so it generates an element and attaches the fetched attribute to it.
DOMXpath::evaluate() can return scalar values from Xpath expressions:
$dom = new DOMDocument();
$dom->loadXml($xml);
$xpath = new DOMXpath($dom);
var_dump($xpath->evaluate('normalize-space(//parameter[#name="UserID"])'));
Output:
string(11) "23452345235"

Related

SimpleXml::xpath() after simplexml_import_dom() operates on the whole DomDocument and not just the Node

I'm not sure if this is the expected behavior or if I'm doing something wrong:
<?php
$xml = '<?xml version="1.0"?>
<foobar>
<foo>
<nested>
<img src="example1.png"/>
</nested>
</foo>
<foo>
<nested>
<img src="example2.png"/>
</nested>
</foo>
</foobar>';
$dom = new DOMDocument();
$dom->loadXML($xml);
$node = $dom->getElementsByTagName('foo')[0];
$simplexml = simplexml_import_dom($node);
echo $simplexml->asXML() . "\n";
echo " === With // ====\n";
var_dump($simplexml->xpath('//img'));
echo " === With .// ====\n";
var_dump($simplexml->xpath('.//img'));
Even though I only imported a specific DomNode, and asXml() returns only that part, the xpath() still seems to operate on the whole document.
I can prevent that by using .//img, but that seemed rather strange to me.
Result:
<foo>
<nested>
<img src="example1.png"/>
</nested>
</foo>
=== With // ====
array(2) {
[0] =>
class SimpleXMLElement#4 (1) {
public $#attributes =>
array(1) {
'src' =>
string(12) "example1.png"
}
}
[1] =>
class SimpleXMLElement#5 (1) {
public $#attributes =>
array(1) {
'src' =>
string(12) "example2.png"
}
}
}
=== With .// ====
array(1) {
[0] =>
class SimpleXMLElement#5 (1) {
public $#attributes =>
array(1) {
'src' =>
string(12) "example1.png"
}
}
}
It is expected behavior. You're importing an DOM element node into an SimpleXMLElement. This does not modify the XML document in the background - the node keeps its context.
Here are Xpath expressions that go up (parent::, ancestor::) or to siblings (preceding-sibling::, following-sibling::).
Location paths starting with a / are always relative to the document, not the context node. An explicit reference to the current node with the . avoids that trigger. .//img is short for current()/descendant-or-self::img - an alternative would be descendant::img.
However you don't need to convert the DOM node into a SimpleXMLElement to use Xpath.
$document = new DOMDocument();
$document->loadXML($xml);
$xpath = new DOMXpath($document);
foreach ($xpath->evaluate('//foo[1]') as $foo) {
var_dump(
$xpath->evaluate('string(.//img/#src)', $foo)
);
}
Output:
string(12) "example1.png"
//foo[1] fetches the first foo element node in the document. If here is no matching element in the document it will return an empty list. Using foreach allows to avoid an error in that case. It will be iterated once or never.
string(.//img/#src) fetches the src attribute of descendant img elements and casts the first one into a string. If here is no matching node the return value will be and empty string. The second argument to DOMXpath::evaluate() is the context node.

Parse XML Document recursive

I have XML documents containing information of articles, that have a kind of hierarchy:
<?xml version="1.0" encoding="UTF-8"?>
<page>
<elements>
<element>
<type>article</type>
<id>1</id>
<parentContainerID>page</parentContainerID>
<parentContainerType>page</parentContainerType>
</element>
<element>
<type>article</type>
<id>2</id>
<parentContainerID>1</parentContainerID>
<parentContainerType>article</parentContainerType>
</element>
<element>
<type>photo</type>
<id>3</id>
<parentContainerID>2</parentContainerID>
<parentContainerType>article</parentContainerType>
</element>
<... more elements ..>
</elements>
</page>
The element has the node parentContainerID and the node parentContainerType. If parentContainerType == page, this is the master element. The parentContainerID shows what's the element's master. So it should look like: 1 <- 2 <- 3
Now I need to build a new page (html) of this stuff that looks like this:
content of ID 1, content of ID 2, content of ID 3 (the IDs are not ongoing).
I guess this could be done with a recursive function. But I have no idea how to manage this?
Here is no nesting/recursion in the XML. The <element/> nodes are siblings. To build the parent child relations I would suggest looping over the XML and building two arrays. One for the relations and one referencing the elements.
$xml = file_get_contents('php://stdin');
$document = new DOMDocument();
$document->loadXml($xml);
$xpath = new DOMXpath($document);
$relations = [];
$elements = [];
foreach ($xpath->evaluate('//element') as $element) {
$id = (int)$xpath->evaluate('string(id)', $element);
$parentId = (int)$xpath->evaluate('string(parentContainerID)', $element);
$relations[$parentId][] = $id;
$elements[$id] = $element;
}
var_dump($relations);
Output:
array(3) {
[0]=>
array(1) {
[0]=>
int(1)
}
[1]=>
array(1) {
[0]=>
int(2)
}
[2]=>
array(1) {
[0]=>
int(3)
}
}
The relations array now contains the child ids for any parent, elements without a parent are in index 0. This allows you use a recursive function access the elements as a tree.
function traverse(
int $parentId, callable $callback, array $elements, array $relations, $level = -1
) {
if ($elements[$parentId]) {
$callback($elements[$parentId], $parentId, $level);
}
if (isset($relations[$parentId]) && is_array($relations[$parentId])) {
foreach ($relations[$parentId] as $childId) {
traverse($childId, $callback, $elements, $relations, ++$level);
}
}
}
This executes the callback for each node. The proper implementation for this would be a RecursiveIterator but the function should do for the example.
traverse(
0,
function(DOMNode $element, int $id, int $level) use ($xpath) {
echo str_repeat(' ', $level);
echo $id, ": ", $xpath->evaluate('string(type)', $element), "\n";
},
$elements,
$relations
);
Output:
1: article
2: article
3: photo
Notice that the $xpath object is provided as context to the callback. Because the $elements array contains the original nodes, you can use Xpath expression to fetch detailed data from the DOM related to the current element node.

no value return for php xml simpleXMLElement

So far i have not been able to return any value for id. here is my code.
$xml = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<Benchmark id="IE8"></Benchmark>
XML;
$sxml = new SimpleXMLElement($xml);
var_dump($sxml);
$name = (string) $sxml->Benchmark['id'];
echo $name;
this is my var dump
object(SimpleXMLElement)[1]
public '#attributes' =>
array (size=1)
'id' => string 'IE8' (length=3)
I basically just want to return the id value: IE8 in Benchmark
When you create a new SimpleXMLElement, the returned object represents the root element of your XML file.
$sxml is your <Benchmark> element, $sxml->Benchmark doesn't exist.
$name = (string) $sxml['id'];

How to build a XML from an Array in php?

This seams obvious, but what I found the most was how to manipulate existing XML and now I wish to build from ground zero. The source is a database converted into an Array. The root is a single "menu" and all child elements are called "item". The structure is defined by the value of "parent" property and "code" property.
item[0] ("code"=>"first" "somevar"=>"somevalue")
item[1] ("code"=>"second", "parent"=>"first" "somevar"=>"othervalue")
Means item[1] is a child of item[0].
<menu>
<item code="first" somevar="somevalue">
<item code="second" somevar="othervalue" />
</item>
</menu>
There will be only two levels of items this time, maybe later I'll expand the capabilities to "n" levels...
I tried with SimpleXML, but it seams is too simple. So I tried with DOMDocument, but I'm stuck creating new elements...
$domMenu = new DOMDocument();
$domMenu->createElement("menu");
... creating the $domItem as a DOMElement with attributes ...
$domMenu->menu->appendChild($domItem);
This generates an error, it seams "menu" is not seen as an DOMElement. Should I use getElements methods or there is a better way of build this XML?
You did not append the menu element to the DOM. And DOM does not map element names to object properties like SimpleXML. The root element is accessible using the DOMDocument::$documentElement property.
$domMenu = new DOMDocument();
$domMenu->appendChild(
$menuNode = $domMenu->createElement("menu")
);
... creating the $domItem as a DOMElement with attributes ...
$menuNode->appendChild($domItem);
In you case I would suggest using xpath to find the parent node for the itemNode and if not found let the function call itself (recursion) to append the parent element first. If here is not parent item, append the node to the document element.
$data = [
["code"=>"second", "parent"=>"first", "somevar"=>"othervalue"],
["code"=>"first", "somevar"=>"somevalue"]
];
function appendItem($xpath, $items, $item) {
// create the new item node
$itemNode = $xpath->document->createElement('item');
$itemNode->setAttribute('code', $item['code']);
$itemNode->setAttribute('somevar', $item['somevar']);
$parentCode = isset($item['parent']) ? $item['parent'] : NULL;
// does it have a parent and exists this parent in the $items array
if (isset($parentCode) && isset($items[$parentCode])) {
// fetch the existing parent
$nodes = $xpath->evaluate('//item[#code = "'.$parentCode.'"]');
if ($nodes->length > 0) {
$parentNode = $nodes->item(0);
} else {
// parent node not found create it
$parentNode = appendItem($xpath, $items, $items[$parentCode]);
}
} else {
$parentNode = $xpath->document->documentElement;
}
$parentNode->appendChild($itemNode);
return $itemNode;
}
$dom = new DOMDocument();
$xpath = new DOMXpath($dom);
$dom->appendChild(
$dom->createElement("menu")
);
// build an indexed list using the "code" values
$items = [];
foreach ($data as $item) {
$items[$item['code']] = $item;
}
foreach ($items as $item) {
// check if the item has already been added
if ($xpath->evaluate('count(//item[#code = "'.$item['code'].'"])') == 0) {
// add it
appendItem($xpath, $items, $item);
}
}
$dom->formatOutput = TRUE;
echo $dom->saveXml();
Output:
<?xml version="1.0"?>
<menu>
<item code="first" somevar="somevalue">
<item code="second" somevar="othervalue"/>
</item>
</menu>
$xml = new SimpleXMLElement('<menu/>');
array_walk_recursive($array, array ($xml, 'addChild'));
print $xml->asXML();

PHP SimpleXML recursive function to list children and attributes

I need some help on the SimpleXML calls for a recursive function that lists the elements name and attributes. Making a XML config file system but each script will have it's own config file as well as a new naming convention. So what I need is an easy way to map out all the elements that have attributes, so like in example 1 I need a simple way to call all the processes but I don't know how to do this without hard coding the elements name is the function call. Is there a way to recursively call a function to match a child element name? I did see the xpath functionality but I don't see how to use this for attributes.
Also does the XML in the examples look correct? can I structure my XML like this?
Example 1:
<application>
<processes>
<process id="123" name="run batch A" />
<process id="122" name="run batch B" />
<process id="129" name="run batch C" />
</processes>
<connections>
<databases>
<database usr="test" pss="test" hst="test" dbn="test" />
</databases>
<shells>
<ssh usr="test" pss="test" hst="test-2" />
<ssh usr="test" pss="test" hst="test-1" />
</shells>
</connections>
</application>
Example 2:
<config>
<queues>
<queue id="1" name="test" />
<queue id="2" name="production" />
<queue id="3" name="error" />
</queues>
</config>
Pseudo code:
// Would return matching process id
getProcess($process_id) {
return the process attributes as array that are in the XML
}
// Would return matching DBN (database name)
getDatabase($database_name) {
return the database attributes as array that are in the XML
}
// Would return matching SSH Host
getSSHHost($ssh_host) {
return the ssh attributes as array that are in the XML
}
// Would return matching SSH User
getSSHUser($ssh_user) {
return the ssh attributes as array that are in the XML
}
// Would return matching Queue
getQueue($queue_id) {
return the queue attributes as array that are in the XML
}
EDIT:
Can I pass two parms? on the first method you have suggested #Gordon
I just got it, thnx, see below
public function findProcessById($id, $name)
{
$attr = false;
$el = $this->xml->xpath("//process[#id='$id'][#name='$name']"); // How do I also filter by the name?
if($el && count($el) === 1) {
$attr = (array) $el[0]->attributes();
$attr = $attr['#attributes'];
}
return $attr;
}
The XML looks good to me. The only thing I wouldn't do is making name an attribute in process, because it contains spaces and should be a textnode then (in my opinion). But since SimpleXml does not complain about it, I guess it boils down to personal preference.
I'd likely approach this with a DataFinder class, encapsulating XPath queries, e.g.
class XmlFinder
{
protected $xml;
public function __construct($xml)
{
$this->xml = new SimpleXMLElement($xml);
}
public function findProcessById($id)
{
$attr = false;
$el = $this->xml->xpath("//process[#id='$id']");
if($el && count($el) === 1) {
$attr = (array) $el[0]->attributes();
$attr = $attr['#attributes'];
}
return $attr;
}
// ... other methods ...
}
and then use it with
$finder = new XmlFinder($xml);
print_r( $finder->findProcessById(122) );
Output:
Array
(
[id] => 122
[name] => run batch B
)
XPath tutorial:
http://www.w3schools.com/XPath/default.asp
An alternative would be to use SimpleXmlIterator to iterate over the elements. Iterators can be decorated with other Iterators, so you can do:
class XmlFilterIterator extends FilterIterator
{
protected $filterElement;
public function setFilterElement($name)
{
$this->filterElement = $name;
}
public function accept()
{
return ($this->current()->getName() === $this->filterElement);
}
}
$sxi = new XmlFilterIterator(
new RecursiveIteratorIterator(
new SimpleXmlIterator($xml)));
$sxi->setFilterElement('process');
foreach($sxi as $el) {
var_dump( $el ); // will only give process elements
}
You would have to add some more methods to have the filter work for attributes, but this is a rather trivial task.
Introduction to SplIterators:
http://www.phpro.org/tutorials/Introduction-to-SPL.html

Categories