I am trying to call an XPath on a SimpleXMLElement, but I always get empty array.
This is the XML:
<Produkt>
.....
<Darstellung>
<Info Art="Kurztext" Wert="some info!"/>
<Info ... />
</Darstellung>
</Produkt>
I tried this:
$shortDescription = $Produkt->xpath('//Darstellung/Info[#Art="Kurztext"]/#Wert');
$configurableProduct['short_description'] = (string)$shortDescription[0];
And this:
$configurableProduct['short_description'] = $Produkt->xpath('//Darstellung/Info[#Art="Kurztext"]/#Wert');
And also without // in the beginning of the XPath expression or with /*. But when I dump, I see that the array returned from the xpath() function is always empty.
The problem is not in unregistered namespace, because the XML file is not using namespaces. I am kinda out of ideas already... (tried some more things in the syntax, but don't remember them already).
P.S. Yes, I am sure $Produkt is a SimpleXMLElement object and I have checked that.
this code
$xmlStr = '<Produkt>
<Darstellung>
<Info Art="Kurztext" Wert="some info!"/>
<Info />
</Darstellung>
</Produkt>';
$xml = simplexml_load_string($xmlStr);
$ret = $xml->xpath('//Darstellung/Info[#Art="Kurztext"]/#Wert');
var_dump((string)$ret[0]);
returns
string(10) "some info!"
Yes, $ret[0] is XMLObject, so you should cast it to string.
Related
In answering a previous question I found the following behaviour which I can't understand. The following code shows the issue...
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
$data = <<< XML
<?xml version="1.0" standalone="yes"?>
<Base>
<Data>
<Value></Value>
</Data>
</Base>
XML;
$xml = simplexml_load_string($data);
foreach ( $xml->Data->Value as $value ) {
$value = 1;
}
echo $xml->asXML().PHP_EOL;
foreach ( $xml->Data as $value ) {
$value->Value = 1;
}
echo $xml->asXML().PHP_EOL;
I would expect the output at each point to be the same, but the output is...
<?xml version="1.0" standalone="yes"?>
<Base>
<Data>
<Value/>
</Data>
</Base>
<?xml version="1.0" standalone="yes"?>
<Base>
<Data>
<Value>1</Value>
</Data>
</Base>
So this seems to indicate that the first loop which directly accesses the <Value> element, doesn't set the value and yet the second loop which indirectly accesses it works OK.
What is the difference?
The difference is nothing to do with the loops, or with references, but with what exactly = means in each case.
The first version can be simplified to this:
$value = $xml->Data->Value;
$value = 1;
This is a straight-forward assignment to a variable, first of one value, and then of another. There's no interaction between the old value and the new one, so $xml is not changed.
The second case can be written like this:
$data = $xml->Data;
$data->Value = 1;
// Or just $xml->Data->Value = 1;
Here, we are assigning not to a normal variable, but to an object property, and the trick is that the object can intercept that assignment, and do something special with it. In this case, it triggers SimpleXML to send the value to the libxml representation of the XML document in memory. It is as though you had run a method call like $data->setValueOfChild('Value', 1);.
Note that if we instead wrote this:
$value =& $xml->Data->Value;
$value = 1;
Now the first assignment sets $value to be a reference, and the second assigns 1 to that reference. This is enough to write the value to an actual object property, but does not trigger the interception SimpleXML needs.
However, there is one additional trick we can use in this particular case: as well as intercepting property access, the SimpleXMLElement class intercepts array access so that you can write $foo->NameThatOccursMoreThanOnce[3] and $some_element['Attribute']. So it turns out we can write this:
$value = $xml->Data->Value;
$value[0] = 1;
Here, $value is a SimpleXMLElement object, which can intercept the $value[0] = 1 as something like $value->setValueOfItem(0, 1).
In this case, the object holds the collection of all elements called <Value> from inside the <Data> element; but conveniently, even if the object has already been narrowed down to one item, [0] just refers back to the same element, so this works too:
$value = $xml->Data->Value[0];
$value[0] = 1;
Finally, a quick note that your own objects can implement this magic behaviour too! The property access can be implemented using the __get, __set, and __unset magic methods, and the array access can be implemented using the ArrayAccess interface.
If I use the following php code to convert an xml to json:
<?php
header("Content-Type:text/json");
$resultXML = "
<QUERY>
<Company>fcsf</Company>
<Details>
fgrtgrthtyfgvb
</Details>
</QUERY>
";
$sxml = simplexml_load_string($resultXML);
echo json_encode($sxml);
?>
I get
{"Company":"fcsf","Details":"\n fgrtgrthtyfgvb\n "}
However, If I use CDATA in the Details element as follows:
<?php
header("Content-Type:text/json");
$resultXML = "
<QUERY>
<Company>fcsf</Company>
<Details><![CDATA[
fgrtgrthtyfgvb]]>
</Details>
</QUERY>
";
$sxml = simplexml_load_string($resultXML);
echo json_encode($sxml);
?>
I get the following
{"Company":"fcsf","Details":{}}
In this case the Details element is blank. Any idea why Details is blank and how to correct this?
This is not a problem with the JSON encoding – var_dump($sxml->Details) shows you that SimpleXML already messed it up before, as you will only get
object(SimpleXMLElement)#2 (0) {
}
– an “empty” SimpleXMLElement, the CDATA content is already missing there.
And after we figured that out, googling for “simplexml cdata” leads us straight to the first user comment on the manual page on SimpleXML Functions, that has the solution:
If you are having trouble accessing CDATA in your simplexml document, you don't need to str_replace/preg_replace the CDATA out before loading it with simplexml.
You can do this instead, and all your CDATA contents will be merged into the element contents as strings.
$xml = simplexml_load_file($xmlfile, 'SimpleXMLElement', LIBXML_NOCDATA);
So, use
$sxml = simplexml_load_string($resultXML, 'SimpleXMLElement', LIBXML_NOCDATA);
in your code, and you’ll get
{"Company":"fcsf","Details":"\n fgrtgrthtyfgvb\n "}
after JSON-encoding it.
I've been searching for a while on this and haven't had much luck. I've found plenty of resources showing how to echo data from dynamic XML, but I'm a PHP novice, and nothing I've written seems to grab and print exactly what I want, though from everything I've heard, it should be relatively easy. The source XML (located at 192.168.0.15:8080/requests/status.xml) is as follows:
<root>
<fullscreen>0</fullscreen>
<volume>97</volume>
<repeat>false</repeat>
<version>2.0.5 Twoflower</version>
<random>true</random>
<audiodelay>0</audiodelay>
<apiversion>3</apiversion>
<videoeffects>
<hue>0</hue>
<saturation>1</saturation>
<contrast>1</contrast>
<brightness>1</brightness>
<gamma>1</gamma>
</videoeffects>
<state>playing</state>
<loop>true</loop>
<time>37</time>
<position>0.22050105035305</position>
<rate>1</rate>
<length>168</length>
<subtitledelay>0</subtitledelay>
<equalizer/>
<information>
<category name="meta">
<info name="description">
000003EC 00000253 00000D98 000007C0 00009C57 00004E37 000068EB 00003DC5 00015F90 00011187
</info>
<info name="date">2003</info>
<info name="artwork_url"> file://brentonshp04/music%24/Music/Hackett%2C%20Steve/Guitar%20Noir%20%26%20There%20Are%20Many%20Sides%20to%20the%20Night%20Disc%202/Folder.jpg
</info>
<info name="artist">Steve Hackett</info>
<info name="publisher">Recall</info>
<info name="album">Guitar Noir & There Are Many Sides to the Night Disc 2
</info>
<info name="track_number">5</info>
<info name="title">Beja Flor [Live]</info>
<info name="genre">Rock</info>
<info name="filename">Beja Flor [Live]</info>
</category>
<category name="Stream 0">
<info name="Bitrate">128 kb/s</info>
<info name="Type">Audio</info>
<info name="Channels">Stereo</info>
<info name="Sample rate">44100 Hz</info>
<info name="Codec">MPEG Audio layer 1/2/3 (mpga)</info>
</category>
</information>
<stats>
<lostabuffers>0</lostabuffers>
<readpackets>568</readpackets>
<lostpictures>0</lostpictures>
<demuxreadbytes>580544</demuxreadbytes>
<demuxbitrate>0.015997290611267</demuxbitrate>
<playedabuffers>0</playedabuffers>
<demuxcorrupted>0</demuxcorrupted>
<sendbitrate>0</sendbitrate>
<sentbytes>0</sentbytes>
<displayedpictures>0</displayedpictures>
<demuxreadpackets>0</demuxreadpackets>
<sentpackets>0</sentpackets>
<inputbitrate>0.016695899888873</inputbitrate>
<demuxdiscontinuity>0</demuxdiscontinuity>
<averagedemuxbitrate>0</averagedemuxbitrate>
<decodedvideo>0</decodedvideo>
<averageinputbitrate>0</averageinputbitrate>
<readbytes>581844</readbytes>
<decodedaudio>0</decodedaudio>
</stats>
</root>
What I'm trying to write is a simple PHP script that echoes the artist's name (In this example Steve Hackett). Actually I'd like it to echo the artist, song and album, but I'm confident that if I'm shown how to retrieve one, I can figure out the rest on my own.
What little of my script which actually seems to work goes as follows. I've tried more than what's below, but I left out the bits that I know for a fact aren't working.
<?PHP
$file = file_get_contents('http://192.168.0.15:8080/requests/status.xml');
$sxe = new SimpleXMLElement($file);
foreach($sxe->...
echo "Artist: "...
?>
I think I need to use foreach and echo, but I can't figure out how to do it in a way that will print what's between those info brackets.
I'm sorry if I've left anything out. I'm not only new to PHP, but I'm new to StackOverflow too. I've referenced this site in other projects, and it's always been incredibly helpful, so thanks in advance for your patience and help!
////////Finished Working Script - Thanks to Stefano and all who helped!
<?PHP
$file = file_get_contents('http://192.168.0.15:8080/requests/status.xml');
$sxe = new SimpleXMLElement($file);
$artist_xpath = $sxe->xpath('//info[#name="artist"]');
$album_xpath = $sxe->xpath('//info[#name="album"]');
$title_xpath = $sxe->xpath('//info[#name="title"]');
$artist = (string) $artist_xpath[0];
$album = (string) $album_xpath[0];
$title = (string) $title_xpath[0];
echo "<B>Artist: </B>".$artist."</br>";
echo "<B>Title: </B>".$title."</br>";
echo "<B>Album: </B>".$album."</br>";
?>
Instead of using a for loop, you can obtain the same result with XPath:
// Extraction splitted across two lines for clarity
$artist_xpath = $sxe->xpath('//info[#name="artist"]');
$artist = (string) $artist_xpath[0];
echo $artist;
You will have to adjust the xpath expression (i.e. change #name=... appropriately), but you get the idea. Also notice that [0] is necessary because xpath will return an array of matches (and you only need the first) and the cast (string) is used to extract text contained in the node.
Besides, your XML is invalid and will be rejected by the parser because of the literal & appearing in the <info name="album"> tag.
If you look at your code again, you are missing a function that turns the first result of the xpath expression into a string of a SimpleXMLElement (casting).
One way to write this once is to extend from SimpleXMLElement:
class BetterXMLElement extends SimpleXMLElement
{
public function xpathString($expression) {
list($result) = $this->xpath($expression);
return (string) $result;
}
}
You then create the more specific SimpleXMLElement like you did use the less specific before:
$file = file_get_contents('http://192.168.0.15:8080/requests/status.xml');
$sxe = new BetterXMLElement($file);
And then you benefit in your following code:
$artist = $sxe->xpathString('//info[#name="artist"]');
$album = $sxe->xpathString('//info[#name="album"]');
$title = $sxe->xpathString('//info[#name="title"]');
echo "<B>Artist: </B>".$artist."</br>";
echo "<B>Title: </B>".$title."</br>";
echo "<B>Album: </B>".$album."</br>";
This spares you some repeated code. This means as well less places you can make an error in :)
Sure you can further on optimize this by allowing to pass an array of multiple xpath queries and returning all values named then. But that is something you need to write your own according to your specific needs. So use what you learn in programming to make programming more easy :)
If you want some more suggestions, here is another, very detailed example using DOMDocument, the sister-library of SimpleXML. It is quite advanced but might give you some good inspiration, I think something similar is possible with SimpleXML as well and this is probably what you're looking for in the end:
Extracting data from HTML using PHP and xPath
I am having trouble getting the text "TestTwo" where the parent has an attribute with Name=SN from the XML below. I am able to get the SimpleXMLElement, but cannot figure out how to get the child.
What is the best way to return the text "TestTwo"?
Thank you!
$xml = simplexml_load_string($XMLBELOW);
$xml->registerXPathNamespace('s','urn:oasis:names:tc:SAML:2.0:assertion');
$result = $xml->xpath("//s:Attribute[#Name='sn']");
var_dump( $result);
$XMLBELOW = <<<XML
<?xml version="1.0" ?>
<saml2:Assertion ID="SAML-4324423" IssueInstant="2012-09-24T17:49:39Z" Version="2.0" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
<saml2:Issuer>
Test
</saml2:Issuer>
<saml2:Subject>
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified" NameQualifier="Test">
Tester
</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData NotBefore="2012-09-24T17:48:39Z" NotOnOrAfter="2012-09-24T17:51:39Z"/>
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="2012-09-24T17:48:39Z" NotOnOrAfter="2012-09-24T17:51:39Z"/>
<saml2:AuthnStatement AuthnInstant="2012-09-24T17:49:39Z" SessionNotOnOrAfter="2012-09-24T17:51:39Z">
<saml2:SubjectLocality Address="105.57.487.48"/>
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>
urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified
</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
<saml2:AttributeStatement>
<saml2:Attribute Name="sn" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml2:AttributeValue>
TestTwo
</saml2:AttributeValue>
</saml2:Attribute>
<saml2:Attribute Name="departmentNumber" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">
<saml2:AttributeValue>
OPERS
</saml2:AttributeValue>
</saml2:Attribute>
</saml2:AttributeStatement>
</saml2:Assertion>
XML;
How about this?
$result = $xml->xpath("//s:Attribute[#Name='sn']/*");
var_dump( $result[0]->__toString() ); // or just `echo $result[0];`
The updated expression will get you all children of your target element (you can narrow the result set further, if you'd like).
As xpath() method returns an array, you need to take some of its elements, and just call 'toString' on these objects to get its text. Here I've done with the first one.
I'm aware of how to drill down into the nodes of an xml document as described here:
http://www.php.net/manual/en/simplexml.examples-basic.php
but am at a loss on how to extract the value in the following example
$xmlStr = '<Error>Hello world. There is an Error</Error>';
$xml = simplexml_load_string($xmlStr);
simplexml_load_string returns an object of type SimpleXMLElement whose properties will have the data of the XML string.
In your case there is no opening <xml> and closing </xml> tags, which every valid XML should have.
If these were present then to get the data between <Error> tags you can do:
$xmlStr = '<xml><Error>Hello world. There is an Error</Error></xml>';
$xml = simplexml_load_string($xmlStr);
echo $xml->Error; // prints "Hello world. There is an Error"
What do you know. The value of the tag is just:
$error = $xml;
Thanks for looking :)