How to change XML attributes (and values) with SimpleXMLElement in PHP - php

I got a problem when I set a value/attribute of XML tag.
This is my PHP code:
$f=simplexml_load_file("String.xml");
$f2=$f->imgdir;
foreach($f2 as $imgdir)
{
$st=$imgdir->string;
foreach($st as $str)
{
$fd=$str->attributes();
$fd['value']='New Value';
}
}
$f->asXML("String.xml");
And this is my String.xml file's content:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<imgdir name="Cash.img">
<imgdir name="5000011">
<string name="name" value="Monkey"/>
<string name="desc" value="A lovely effect that shows a baby angel floating around as your protector. Designate a HotKey to turn the effect on/off."/>
</imgdir>
<imgdir name="5010000">
<string name="name" value="Sunny Day"/>
<string name="desc" value="A special effect in which you&apos;ll see a brightly smiling sun floating over you. On the KeyConfig, configure this on a button of your choice to turn the effect on/off."/>
</imgdir>
</imgdir>
How can I change the value attribute of per <string> tag and save the XML file? What should I do with my current code?
Thank for reading my question!

Basics
I suggest you select all <string> nodes with xpath:
$xml = simplexml_load_string($x); // assume XML in $x
$strings = $xml->xpath("//string");
The above statement will select all <string> nodes as SimpleXml elements into an array.
The double slash in //string will take <string> regardless of their position in the tree.
Now it is simple to iterate over $strings and set the new value:
foreach ($strings as $string)
$string["value"] = "new value";
Note that the changes applied to $strings happen in $xml, so:
echo $xml->asXML();
will show the changes.
see it working: https://eval.in/499427
Expanded
Of course, you might want to select only certain <string> nodes, e.g. all having name="name". In this case, you can tell xpath like:
$strings = $xml->xpath("//string[#name='name']");
Note the condition set in [] and #name referring to the attribute name.

Related

Delete attribute in XML

I want to completely remove the size="id" attribute from every <door> element.
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249" size="30"/>
<door id="1041" entry="6523" size="3094"/>
-- and 1000 more....
</doors>
The PHP code:
$xml = new SimpleXMLElement('http://mysite/doors.xml', NULL, TRUE);
$ids_to_delete = array( 1, 1506 );
foreach ($ids_to_delete as $id) {
$result = $xml->xpath( "//door[#size='$id']" );
foreach ( $result as $node ) {
$dom = dom_import_simplexml($node);
$dom->parentNode->removeChild($dom);
}
}
$xml->saveXml();
I get no errors but it does not delete the size attribute. Why?
I get no errors but it does not delete the size attribute. Why?
There are mulitple reasons why it does not delete the size attribute. The one that popped first into my mind was that attributes are no child nodes. Using a method to remove a child does just not fit to remove an attribute.
Each element node has an associated set of attribute nodes; the element is the parent of each of these attribute nodes; however, an attribute node is not a child of its parent element.
From: Attribute Nodes - XML Path Language (XPath), bold by me.
However, you don't see an error here, because the $result you have is an empty array. You just don't select any nodes to remove - neither elements nor attributes - with your xpath. That is because there is no such element you look for:
//door[#size='1']
You're searching for the id in the size attribute: No match.
These are the reasons why you get no errors and it does not delete any size attribute: 1.) you don't delete attributes here, 2.) you don't query any elements to delete attributes from.
How to delete attributes in SimpleXML queried by Xpath?
You can remove the attribute nodes by selecting them with an Xpath query and then unset the SimpleXMLElement self-reference:
// all size attributes of all doors
$result = $xml->xpath("//door/#size");
foreach ($result as $node) {
unset($node[0]);
}
In this example, all attribute nodes are queried by the Xpath expressions that are size attributes of door elements (which is what you ask for in your question) and then those are removed from the XML.
//door/#size
(see Abbreviated Syntax)
Now here the full example:
<?php
/**
* #link https://eval.in/215817
*/
$buffer = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249" size="30"/>
<door id="1041" entry="6523" size="3094"/>
-- and 1000 more....
</doors>
XML;
$xml = new SimpleXMLElement($buffer);
// all size attributes of all doors
$result = $xml->xpath("//door/#size");
foreach ($result as $node) {
unset($node[0]);
}
$xml->saveXml("php://output");
Output (Online Demo):
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249"/>
<door id="1041" entry="6523"/>
-- and 1000 more....
</doors>
You can do your whole query in DOMDocument using DOMXPath, rather than switching between SimpleXML and DOM:
$dom = new DOMDocument;
$dom->load('my_xml_file.xml');
# initialise an XPath object to act on the $dom object
$xp = new DOMXPath( $dom );
# run the query
foreach ($xp->query( "//door[#size]" ) as $door) {
# remove the attribute
$door->removeAttribute('size');
}
print $dom->saveXML();
Output for the input you supplied:
<?xml version="1.0" encoding="UTF-8"?>
<doors>
<door id="1" entry="3249"/>
<door id="1041" entry="6523"/>
</doors>
If you do want only to remove the size attribute for the IDs in your list, you should use the code:
foreach ($ids_to_delete as $id) {
# searches for elements with a matching ID and a size attribute
foreach ($xp->query("//door[#id='$id' and #size]") as $door) {
$door->removeAttribute('size');
}
}
Your code wasn't working for several reasons:
it looks like your XPath was wrong, since your array is called $ids_to_delete and your XPATH is looking for door elements with the size attribute equal to the value from $ids_to_delete;
you're converting the nodes to DOMDocument objects ($dom = dom_import_simplexml($node);) to do the deletion, but $xml->saveXml();, which I presume you printed somehow, is a SimpleXML object;
you need to remove the element attribute; removeChild removes the whole element.

update key value in xml with php

I'm not sure if it's possible or not, but here is what I would like to be able to do.
My xml file structure:
<?xml version="1.0" encoding="utf-8" ?>
<content>
<option name="0">Yes</option>
<option name="1">No</option>
<option name="2">Maybe</option>
</content>
I would like to update only one node at the time, for example:
Replace word "Maybe" with something else, where value == 2. So it needs to search for "option name="2" and replace word Maybe with user input.
You can do that with simplexmland xpath:
$xml = simplexml_load_string($x); // assume XML in $x
// get the node
$node = $xml->xpath("/content/option[#name = '2']");
// change it
$node[0][0] = "Hello!";
see it working: https://eval.in/127855

Selecting between 2 specific elements to get comments between closing and starting tag

I'm currently trying to fetch the data of a simple XML-Sheet which contains data for translation use it's structured like this:
<string name="action_settings">Settings</string><!-- Comment1 -->
<string name="action_error">Something went wrong.</string>
<string name="action_menu">You've got the choice</string><!-- Comment2 -->
Sometimes there are comments after to describe the content a bit more for the translator.
I'd like to get those, while I managed to write a comment, I couldn't manage to get 1 comment reliable ...
My idea is:
If I want to get a comment of "action_setting" for example I use xpath to select this area:
<string name="action_settings">Settings</string>|AREASTART|<!-- Comment1 -->
|AREAEND|<string name="action_error">Something went wrong.</string>
<string name="action_menu">You've got the choice</string><!-- Comment2 -->
I already tried archieving this with this code:
<?php
$doc = new DOMDocument();
$doc->load('strings.xml');
$xpath = new DOMXPath($doc);
//foreach ($xpath->query("//string[#name='debug']/following::comment()[1]") as $comment)
foreach ($xpath->query("/*/string[count(preceding-sibling::string[#name='debug'])=1]/comment()[1]") as $comment)
{
var_dump($comment->textContent." ");
}
?>
As you can see the commented line is simply selecting every comment node after my specific element and picks the first one in row. The problem with this is that I can't make sure that the comment is really after the specific element or just the comment of an element a few lines later.(so if I'd like to get "action_error" it would give me "Comment 2" which belongs to "action_menu)
As you can see I tried already to select this desired area but it simply doesn't return anything when there's a comment.(My source: XPath select all elements between two specific elements)
So I'd be grateful if you would explain me a solution for this problem I'm facing with comments between 2 specific elements.
You could use following-sibling in conjunction with a predicate.
Get the text of the next comment
(following-sibling::string|following-sibling::comment())[1][self::comment()]
Given a context node, for example the string with the name of action_settings:
(following-sibling::string|following-sibling::comment())
Selects all of the string and comment sibling nodes following the context.
[1]
Filters the node set to have all of the nodes' position() is 1: in other words, it reduces the set to only the first node.
[self::comment()]
Filters the node set to have only comment nodes.
In summary, the above will return a node set consisting of either:
A single comment node; the one we are interested in.
An empty node set.
Putting it to use in an example
<?php
$xml = <<<XML
<example>
<string name="action_settings">Settings</string><!-- Comment1 -->
<string name="action_error">Error</string>
<string name="action_menu">Menu</string><!-- Comment2 -->
</example>
XML;
$doc = new DOMDocument();
$doc->loadXML($xml);
$xpath = new DOMXPath($doc);
$next_comment_xpath = 'normalize-space(
(following-sibling::string|following-sibling::comment())[1][self::comment()]
)';
$strings = $xpath->query('//string');
foreach ($strings as $string)
{
$name = $string->getAttribute('name');
$value = $string->nodeValue;
$comment = $xpath->evaluate($next_comment_xpath, $string);
echo "Name: {$name}\n";
echo "Value: {$value}\n";
echo "Comment: {$comment }\n\n";
}
The real work is done by $next_comment_xpath which uses the example location path given above. The normalize-space() is used two cast the node set to a string, for two reasons: firstly, casting to a string gets us the text content of the first node in the set, or an empty string if none and, secondly, it means evaluate() can return a PHP string.
Example output
Name: action_settings
Value: Settings
Comment: Comment1
Name: action_error
Value: Error
Comment:
Name: action_menu
Value: Menu
Comment: Comment2

PHP change xmlns in XML file

I have xml with the following structure:
<?xml version="1.0"?>
<ONIXMessage xmlns="http://test.com/test">
...data...
</ONIXMessage>
I need to change xmlns attribute with my own value. How can I do it? Preferably with DOMDocument class.
I need to change xmlns attribute with my own value. How can I do it? Preferably with DOMDocument class.
This by design is not possible. Every DOMDocument has a single root/document element.
In your example XML that root element is:
{http://test.com/test}ONIXMessage
I write the element name as an expanded-name with the convention to put the namespace URI in front enclosed in angle brackets.
Writing the element name in a form that shows it's entire expanded-name also demonstrates that you do not only want to change the value of an attribute here, but you want to change the namespace URI of a specific element. So you want to change the element name. And probably also any child element name it contains if the child is in the same namespace.
As the xmlns attribute only reflects the namespace URI of the element itself, you can not change it. Once it is set in DOMDocument, you can not change it.
You can replace the whole element, but the namespace of the children is not changed either then. Here an example with an XML similar to yours with only textnode children (which aren't namespaced):
$xml = <<<EOD
<?xml version="1.0"?>
<ONIXMessage xmlns="uri:old">
...data...
</ONIXMessage>
EOD;
$doc = new DOMDocument();
$doc->loadXML($xml);
$newNode = $doc->createElementNS('uri:new', $doc->documentElement->tagName);
$oldNode = $doc->replaceChild($newNode, $doc->documentElement);
foreach(iterator_to_array($oldNode->childNodes, true) as $child) {
$doc->documentElement->appendChild($child);
}
Resulting XML output is:
<?xml version="1.0"?>
<ONIXMessage xmlns="uri:new">
...data...
</ONIXMessage>
Changing the input XML now to something that contains children like
<?xml version="1.0"?>
<ONIXMessage xmlns="uri:old">
<data>
...data...
</data>
</ONIXMessage>
Will then create the following output, take note of the old namespace URI that pops up now again:
<?xml version="1.0"?>
<ONIXMessage xmlns="uri:new">
<default:data xmlns:default="uri:old">
...data...
</default:data>
</ONIXMessage>
As you can see DOMDocument does not provide a functionality to replace namespace URIs for existing elements out of the box. But hopefully with the information provided in this answer so far it is more clear why exactly it is not possible to change that attributes value if it already exists.
The expat based parser in the libxml based PHP extension does allow to "change" existing attribute values regardless if it is an xmlns* attribute or not - because it just parses the data and you can process it on the fly with it.
A working example is:
$xml = <<<EOD
<?xml version="1.0" encoding="utf-8"?>
<ONIXMessage xmlns="uri:old">
<data>
...data...
</data>
</ONIXMessage>
EOD;
$uriReplace = [
'uri:old' => 'uri:new',
];
$parser = xml_parser_create('UTF-8');
xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
xml_set_default_handler($parser, function ($parser, $data) {
echo $data;
});
xml_set_element_handler($parser, function ($parser, $name, $attribs) use ($xml, $uriReplace) {
$selfClosing = '/>' === substr($xml, xml_get_current_byte_index($parser), 2);
echo '<', $name;
foreach ($attribs as $name => $value) {
if (substr($name, 0, 5) === 'xmlns' && isset($uriReplace[$value])) {
$value = $uriReplace[$value];
}
printf(' %s="%s"', $name, htmlspecialchars($value, ENT_COMPAT | ENT_XML1));
}
echo $selfClosing ? '/>' : '>';
}, function ($parser, $name) use ($xml) {
$selfClosing = '/>' === substr($xml, xml_get_current_byte_index($parser) - 2, 2);
if ($selfClosing) return;
echo '</', $name, '>';
});
xml_parse($parser, $xml, true);
xml_parser_free($parser);
The output then has transparently changed the namespace URI from uri:old to uri:new:
<ONIXMessage xmlns="uri:new">
<data>
...data...
</data>
</ONIXMessage>
As this example shows, each XML feature you make use of in your XML needs to be handled with the parser. For example the XML declaration is missing. However these can be added by implementing missing handler classbacks (e.g. for CDATA sections) or by outputting missing output (e.g. for the "missing" XML declaration). I hope this is helpful and shows you an alternative way on how to change even these values that are not intended to change.

Hide XML declaration in files generated using PHP

I was tesing with a simple example of how to display XML in browser using PHP and found this example which works good
<?php
$xml = new DOMDocument("1.0");
$root = $xml->createElement("data");
$xml->appendChild($root);
$id = $xml->createElement("id");
$idText = $xml->createTextNode('1');
$id->appendChild($idText);
$title = $xml->createElement("title");
$titleText = $xml->createTextNode('Valid');
$title->appendChild($titleText);
$book = $xml->createElement("book");
$book->appendChild($id);
$book->appendChild($title);
$root->appendChild($book);
$xml->formatOutput = true;
echo "<xmp>". $xml->saveXML() ."</xmp>";
$xml->save("mybooks.xml") or die("Error");
?>
It produces the following output:
<?xml version="1.0"?>
<data>
<book>
<id>1</id>
<title>Valid</title>
</book>
</data>
Now I have got two questions regarding how the output should look like.
The first line in the xml file '', should not be displayed, that is it should be hidden
How can I display the TextNode in the next line. In total I am exepecting an output in this fashion
<data>
<book>
<id>1</id>
<title>
Valid
</title>
</book>
</data>
Is that possible to get the desired output, if so how can I accomplish that.
Thanks
To skip the XML declaration you can use the result of saveXML on the root node:
$xml_content = $xml->saveXML($root);
file_put_contents("mybooks.xml", $xml_content) or die("cannot save XML");
Please note that saveXML(node) has a different output from saveXML().
First question:
here is my post where all usable threads with answers are listed: How do you exclude the XML prolog from output?
Second question:
I don't know of any PHP function that outputs text nodes like that.
You could:
read xml using DomDocument and save each node as string
iterate trough nodes
detect text nodes and add new lines to xml string manually
At the end you would have the same XML with text node values in new line:
<node>
some text data
</node>

Categories