Strange behaviour with xdebug and removing child node in SimpleXMLElement - php

I have a xml document with multidimensional structure:
<?xml version="1.0" encoding="UTF-8"?>
<main>
<products>
<product id="87" state="active">
<options>
<option id="99" state="active">
<item id="33" value="somevalue" />
<item id="35" value="somevalue2" />
</option>
<option id="12" state="deleted">
<item id="56" value="somevalue" />
<item id="34" value="somevalue2" />
</option>
</options>
<reports>
<report type="json">
<field id="123" state="active" />
<field id="234" state="deleted" />
<field id="238" state="active" />
<field id="568" state="deleted" />
</report>
</reports>
</product>
</products>
</main>
In the PHP backend I've written methods to detect items with "deleted" status and remove them.
Here is PHP part:
public function loadAndModify() {
$xml = simplexml_load_file($this->request->file('import_xml'));
$this->processXml($xml);
}
/**
* #param $element
*
* #return bool
*/
private function shouldRemove($element): bool
{
return ($element['state'] == SomeClass::STATE_DELETED);
}
/**
* #param $xml
*
* #return void
*/
private function processXml(&$xml): void
{
if ($xml->children()->count() > 0) {
foreach ($xml->children() as $child) {
if ($this->shouldRemove($child)) {
// this code works as expected with or without xdebug
//$node = dom_import_simplexml($child);
//$node->parentNode->removeChild($node);
// this code will work only with xdebug when breakpoint is set
unset($child[0]);
continue;
// end
} else {
$this->processXml($child);
}
}
}
}
I solve my problem by converting simpleXMLElement to DOMElement.
However it seems that PHP has some bug when I use unset with xdebug. When I add breakpoint to line with unset and go to next step in the debugger and then resume application - there is no problem. But when breakpoint is active and I just clicked resume application it cause error:
Uncaught ErrorException: Trying to get property of non-object in
\project\vendor\symfony\var-dumper\Cloner\AbstractCloner.php
If someone else had this error please explain why this is happened in this case.
Thanks.

As discussed in the comments under this previous answer, the problem you are encountering is that you are manipulating an object (in this case, the return value of $xml->children()) which you are iterating over (with a foreach loop).
Internally, the SimpleXMLElement object has a list of child items it is going to present, in turn, to the iterator code in foreach. When you delete the current child item, you necessarily change the shape of that internal list, so "next item" is not well defined. Deleting other items in the list can also have odd behaviour - for instance, deleting item 1 while inspecting item 2 may cause the iterator to "skip ahead" since item 4 has now moved into the place where item 3 was.
As hakre suggests in the comments linked above, the most robust solution is to copy the original list of items into an array, which can be achieved using iterator_to_array. Passing false as the second argument throws away the keys, which is important with SimpleXML because because it uses the tag name as the key, and there can be only one value for each key in the array.
foreach ( iterator_to_array($xml->children(), false) as $child) {
// Carry on as you were
}
The only thing to be aware of with this is that iterator_to_array will go through the whole list before returning, so if you have a large list and want to break out of the loop early, or stream output, this may be problematic.

Related

parsing XML file using PHP (cs-s75: David Malan's)

I am making a PHP project for a Pizza Shop [This is project-0 in David Malan's course CS-S75 Building Dynamic Websites]. And the Code that I have to write must be eXtensible. That is, if the pizza shop's owner wants to add a new category, he should be able to do that pretty easily and my PHP code must accommodate those changes in the XML file without writing any new code.
For my code to be extensible though, I need some methods for filtering the XML data.
For instance inside the root node <menu>, I have child nodes item that have attributes like
<item name="Pizzas">
<category name="Onions">
</category>
</item>
<item name="Salads">
<category name = "Garden">
</category>
</item>
and there are ten item tags in total.
What I want to do is this: if the user wants to purchase the salads, I would want to filter the XML DOM tree the following way:
// $_POST['selected'] has a value of 'Salads' stored in it
$selected = $_POST['selected']
$dom = simple_xml_loadfile("menu.xml")
foreach ($dom -> xpath("menu/item[#name = $selected ]" as $item))
{
echo $item -> category['name'].'<br />';
}
And it should print Garden and any other item that is subsequently added to the Salads category.The problem occurs with the menu/item[#name = $selected ] because this is probably not a proper method for comparing the attribute (Note that attribute comparison like this in XML requires single equal sign and not double equal).And obviously menu/item[#name = $_POST['selected']] doesn't work either.
What works is #name = "Salads" and of course this kills the whole purpose of the extensiblity of XML and dynamism of PHP.
Please help!
Let's get all category nodes that belong to a parent node that has a name attribute of your choosing:
Also note that the function name is simplexml_load_file and not simple_xml_loadfile
foreach ($dom->xpath('item[#name="' . $selected . '"]/category') as $item)
{
echo $item->attributes()->{'name'}. PHP_EOL;
}
Also note the usage of single vs. double quotes to enclose the attribute value.
For reference, this is the xml structure I used for testing:
<menu>
<item name="Pizzas">
<category name="Onions"></category>
</item>
<item name="Salads">
<category name = "Garden"></category>
<category name = "Cesar"></category>
<category name = "Onion and Tomato"></category>
</item>
</menu>

Zend SOAP: Change the default array element name "item" to class name of complex type in WSDL

this question might be asked, but it is so hard to search for, I just can not find anything about it. Plus it is not easy to ask.
I'm using Zend SOAP's autodiscover to re-create our old SOAP interface (because of switching to micro services and re-working everything).
So far it's working like a charm. But I have one problem in recreating the SOAP response of some services when using lists/arrays.
The old response XML of a SOAP request looked like this. It contains two <SMSEntry>s in the <SMSEntries> list.
<SOAP-ENV:Envelope>
<SOAP-ENV:Body>
<ns1:getSMSByTimeSpanResult>
<AmountOfEntries>2</AmountOfEntries>
<SMSEntries>
<SMSEntry></SMSEntry>
<SMSEntry></SMSEntry>
</SMSEntries>
</ns1:getSMSByTimeSpanResult>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
But the recreated response looks like this. It contains two <item>s of type SMSEntry in the <SMSentries> list.
<SOAP-ENV:Envelope>
<SOAP-ENV:Body>
<ns1:getSMSByTimeSpanResponse>
<return xsi:type="ns1:getSMSByTimeSpanResponse">
<AmountOfEntries xsi:type="xsd:int">2</AmountOfEntries>
<SMSEntries SOAP-ENC:arrayType="ns1:SMSEntry[2]" xsi:type="ns1:ArrayOfSMSEntry">
<item xsi:type="ns1:SMSEntry"></item>
<item xsi:type="ns1:SMSEntry"></item>
</SMSEntries>
<DataEx xsi:nil="true"/>
</return>
</ns1:getSMSByTimeSpanResponse>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>
I have no control of the clients. They might be checking for a SMSEntry with comparing the string. I want to use the class name SMSEntry for the XML-tag name.
Second, I would like to leave out the additional, wrapping everything, <return> tag.
I am using the autodiscover like this:
use Zend\Soap\AutoDiscover;
use Zend\Soap\Wsdl\ComplexTypeStrategy\ArrayOfTypeComplex;
$autoDiscover = new AutoDiscover(new ArrayOfTypeComplex());
$autoDiscover->setClass(new Standard($sm));
The getSMSByTimeSpanResponse is defined like this:
Standard.php
/**
* Class getSMSByTimeSpanResponse
*
* #package LgxServiceManager\Service
*/
class getSMSByTimeSpanResponse
{
/**
* #var int
*/
public $AmountOfEntries;
/**
* #var \LgxServiceManager\Service\SMSEntry[]
*/
public $SMSEntries;
}
/**
* Class SMSEntry
*
* #package LgxServiceManager\Service
*/
class SMSEntry
{
}
Does anybody have any idea on how to this?
I found some code in the library\Zend\Soap\Wsdl\ComplexTypeStrategy\ArrayOfTypeSequence.php:122
Where the _addSequenceType() method is setting an attribute hardcoded:
$element->setAttribute('name', 'item');
But this is in type "Sequence" not type "Complex" like I'm using...
Thank you in advance,
Philipp
\EDIT
oh man... I just discovered another structure which I don't know how to create with Zend SOAP's autodiscover...
<mainMember1>SERIALNUMBER</mainMember1>
<mainMember2>NAMEOFPRODUCT</mainMember2>
<mainMember3>000000-000-0</mainMember3>
<Rules>
<RuleEntry>
<singleValue>allow</singleValue>
<ResourceList>
<Name>generic</Name>
<ResourceEntry>[...]</ResourceEntry>
<ResourceEntry>[...]</ResourceEntry>
<ResourceEntry>[...]</ResourceEntry>
</ResourceList>
<ResourceList>
<Name>default</Name>
<ResourceEntry>[...]</ResourceEntry>
<ResourceEntry>[...]</ResourceEntry>
<ResourceEntry>[...]</ResourceEntry>
</ResourceList>
</RuleEntry>
</Rules>
As you can see, there is a <singleValue> inside the <RuleEntry> but multiple <ResourceList>s. The same structure is used inside the resource lists: One <Name> and multiple <ResourceEntry>...
Is this even possible to handle with autodiscover?
I know this is an old thread, but I am trying to help followers that have the same problem I was facing.
Nowadays we use Laminas, but the principle is the same.
another way to solve the original problem is to do a little change to the
vendor\Laminas\Laminas-Soap\src\Wsdl\ComplexTypeStrategy\ArrayOfTypeSequence.php:124
with a code like this:
// Paulo
// $element->setAttribute('name', 'item'); // Original Code
$element->setAttribute('name', substr($childType, 4));
// Paulo End
Hope this helps everyone
For anyone still having above problem:
Default response of stdClass() like this:
$array = ['lets', 'test', 'it'];
$response = new stdClass();
$response->results = $array;
Will be like:
<return xsi:type="SOAP-ENC:Struct">
<results SOAP-ENC:arrayType="xsd:string[3]" xsi:type="SOAP-ENC:Array">
<item xsi:type="xsd:string">lets</item>
<item xsi:type="xsd:string">test</item>
<item xsi:type="xsd:string">it</item>
</results>
</return>
What we can do - we can change array to ArrayObject():
$array = ['lets', 'test', 'it'];
$response = new stdClass();
$response->results = new ArrayObject();
foreach($array as $item) {
$response->results->append($item);
}
which return:
<return xsi:type="SOAP-ENC:Struct">
<results xsi:type="SOAP-ENC:Struct">
<BOGUS xsi:type="xsd:string">lets</BOGUS>
<BOGUS xsi:type="xsd:string">test</BOGUS>
<BOGUS xsi:type="xsd:string">it</BOGUS>
</results>
</return>
And finally the icing on the cake:
$array = ['lets', 'test', 'it'];
$response = new stdClass();
$response->results = new ArrayObject();
foreach($array as $item) {
$soapVar = new SoapVar($item,XSD_STRING,NULL,NULL,'result');
$response->results->append($soapVar);
}
It will return:
<return xsi:type="SOAP-ENC:Struct">
<results xsi:type="SOAP-ENC:Struct">
<result xsi:type="xsd:string">lets</result>
<result xsi:type="xsd:string">test</result>
<result xsi:type="xsd:string">it</result>
</results>
</return>
As you can see the fifth argument tells what will be the key of xml element. You need to be aware of second argument too, becaouse it tells which is the type of variable. You can find more here:
https://secure.php.net/manual/en/class.soapvar.php

SimpleXml attributes not showing for element with no children

I have the following XML:
<?xml version="1.0"?>
<STATUS_LIST>
<ORDER_STATUS SORDER_CODE="SO001" ASSOCIATED_REF="001">
<INVOICES>
<INVOICE INVOICE_CODE="???">SOMETHING</INVOICE>
</INVOICES>
</ORDER_STATUS>
</STATUS_LIST>
When I run the following code:
$statuses = simplexml_load_string($result); //Where $result is my XML
if (!empty($statuses))
{
foreach ($statuses as $status)
{
foreach ($status->INVOICES as $invoice)
{
echo (string)$invoice->attributes()->INVOICE_CODE;
}
}
}
I step through this code and I can see the attributes against ORDER_STATUS but I can't see the attributes against INVOICE. I can however see the value SOMETHING against invoice.
Any idea what could cause this?
Update
After some testing, I can get the attributes to show if I add an element into the INVOICE element, so if I use this xml instead it will work:
<?xml version="1.0"?>
<STATUS_LIST>
<ORDER_STATUS SORDER_CODE="SO001" ASSOCIATED_REF="001">
<INVOICES>
<INVOICE INVOICE_CODE="???"><TEST>tester</TEST></INVOICE>
</INVOICES>
</ORDER_STATUS>
</STATUS_LIST>
So it has to have an element inside to pick up the attributes!?
According to this past question, "SimpleXML doesn't allow attributes and text on the same element." It's pretty ridiculous, and I couldn't find any official coverage of that fact, but it seems true. Lame. It's valid XML. I know Perl SimpleXML reads it fine.
Your problem has nothing to do with the element having no content, you simply have your loops defined slightly wrong.
When you write foreach ($status->INVOICES as $invoice), SimpleXML will loop over every child of the $status element which is called INVOICES; in this case there will always be exactly one such element. But what you actually wanted is to loop over all the children of that element - the individual INVOICE nodes.
To do that you can use one of the following:
foreach ($status->INVOICES->children() as $invoice) (loop over all child nodes of the first, and in this case only, INVOICES element)
foreach ($status->INVOICES[0]->children() as $invoice) (the same, but being more explicit about selecting the first INVOICES node)
foreach ($status->INVOICES[0] as $invoice) (actually does the same again: because you've specifically selected one node, and then asked for a loop, SimpleXML assumes you want its children; this is why foreach($statuses as $status) works as the outer loop)
foreach ($status->INVOICES->INVOICE as $invoice) (loop over only child nodes called INVOICE, which happens to be all of them)
Personally, I would rewrite your sample loop as below:
foreach ($statuses->ORDER_STATUS as $status)
{
foreach ($status->INVOICES->INVOICE as $invoice)
{
echo (string)$invoice['INVOICE_CODE'];
}
}
Here's a live demo to prove that that works.

Read and take value of XML attributes

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

simplexml editing CDATA node

I have an xml file,
I want to open it, edit certain CDATA node with the values from $_POST input and save it as same file,
I've read some online documentation and ended up here,
someone please suggest a nice way of doing this...
regardsh
SimpleXML does not make CDATA elements accessible by default. You can either tell simplexml to skip them (default) or to read them (see: read cdata from a rss feed). If you read them, they are standard text values, so they get merged with other textnodes.
More control is offered by the Document Object ModelDocs, which offers a DOMCdataSection which extends from DOMText, the standard text node model.
Even though this is a different PHP library (DOM vs. SimpleXML), both are compatible to each other. For example a SimpleXMLElement can be converted into a DOMElement by using the dom_import_simplexml function.
If you post some code what you've done so far it should be easy to figure out how to access the CDATA sections you want to modify. Please provide as well some demo XML data so the example is more speaking.
Since I had the same issue just recently, I wanted to let people also see some code, because the linked examples can only add new CDATA sections, but do not remove the old ones. So "my" solutions is merged from the mentioned code example plus deleting the old CDATA node.
// get DOM node
$node = dom_import_simplexml($mySimpleXmlElement);
// remove existing CDATA ($node->childNodes->item(1) does not seem to work)
foreach($node->childNodes as $child) {
if ($child->nodeType == XML_CDATA_SECTION_NODE) {
$node->removeChild($child);
}
}
// add new CDATA
$no = $node->ownerDocument;
$node->appendChild($no->createCDATASection($myNewContent));
// print result
echo $xml->asXML();
I suggest you use this http://www.php.net/manual/en/class.domdocument.php
You can extend class SimpleXMLElement with simples function to do this
class ExSimpleXMLElement extends SimpleXMLElement {
/**
* Add CDATA text in a node
* #param string $cdata_text The CDATA value to add
*/
private function addCData($cdata_text) {
$node = dom_import_simplexml($this);
$no = $node->ownerDocument;
$node->appendChild($no->createCDATASection($cdata_text));
}
/**
* Create a child with CDATA value
* #param string $name The name of the child element to add.
* #param string $cdata_text The CDATA value of the child element.
*/
public function addChildCData($name, $cdata_text) {
$child = $this->addChild($name);
$child->addCData($cdata_text);
return $child;
}
/**
* Modify a value with CDATA value
* #param string $name The name of the node element to modify.
* #param string $cdata_text The CDATA value of the node element.
*/
public function valueChildCData($name, $cdata_text) {
$name->addCData($cdata_text);
return $name;
}
}
usage:
$xml_string = <<<XML
<root>
<item id="foo"/>
</root>
XML;
$xml5 = simplexml_load_string($xml_string, 'ExSimpleXMLElement');
$xml5->valueChildCData($xml5->item, 'mysupertext');
echo $xml5->asXML();
$xml6 = simplexml_load_string($xml_string, 'ExSimpleXMLElement');
$xml6->item->addChildCData('mylittlechild', 'thepunishment');
echo $xml6->asXML();
result:
<?xml version="1.0"?>
<root>
<item id="foo"><![CDATA[mysupertext]]></item>
</root>
<?xml version="1.0"?>
<root>
<item id="foo">
<mylittlechild><![CDATA[thepunishment]]></mylittlechild>
</item>
</root>

Categories