removing elements from xml with php - unorthodox xml - php

I created an ios app that parses an xml document. If a user logs in, their information will be added to the xml file. I would like to be able to remove a user if they are logging out or cancelling their logins. Essentially, I need to figure out how to delete an xml object (a bartender, in this case) that looks like this:
<Bars>
<Bar>
<bar_id>0</bar_id>
<Bartenders>
<Bartender>
<imageURL>unique URL</imageURL>
<shift>20:30</shift>
</Bartender>
</Bartenders>
</Bar>
<Bar>
<bar_id>1</bar_id>
<Bartenders>
<Bartender>
<imageURL>aURL</imageURL>
<shift>a shift</shift>
</Bartender>
<Bartender>
<imageURL>aURL</imageURL>
<shift>a shift</shift>
</Bartender>
</Bartenders>
</Bar>
For example, I want to remove a user based on a unique imageURL... I will also know the bar_id. This is the php that I have so far, but I am completely open to suggestions on doing it a different way
$newimageURL = $row['imageURL'];
$newBar_ID = $row['Bar_ID'];
$xmlUrl = "Bars.xml"; // XML
$xmlStr = file_get_contents($xmlUrl);
$xml = new SimpleXMLElement($xmlStr);
$bartenders = $xml->xpath('//Bartenders');
// something needs to happen here to remove the child
$xml->asXML('Bars.xml');
I was told that I could use something of this sort:
$bartenders->removeChild($bartenders[$newBar_ID]);
Or xpath but I am not sure how to get the path right to the correct bartender with just the unique imageXML. I know I should have planned/designed this better, but I am crunched for time and this will have to do.
Sorry I am so bad with php...
Thanks for your help.

There is a very nice script at http://php.net/manual/fr/ref.simplexml.php that works perfectly fine
function removeNode($xml, $path, $multi='one')
{
$result = $xml->xpath($path);
# for wrong $path
if (!isset($result[0])) return false;
switch ($multi) {
case 'all':
$errlevel = error_reporting(E_ALL & ~E_WARNING);
foreach ($result as $r) unset ($r[0]);
error_reporting($errlevel);
return true;
case 'child':
unset($result[0][0]);
return true;
case 'one':
if (count($result[0]->children())==0 && count($result)==1) {
unset($result[0][0]);
return true;
}
default:
return false;
}
}
Please click on the link for more information

Related

PHP return value after XML exploration

I got a PHP array with a lot of XML users-file URL :
$tab_users[0]=john.xml
$tab_users[1]=chris.xml
$tab_users[n...]=phil.xml
For each user a <zoom> tag is filled or not, depending if user filled it up or not:
john.xml = <zoom>Some content here</zoom>
chris.xml = <zoom/>
phil.xml = <zoom/>
I'm trying to explore the users datas and display the first filled <zoom> tag, but randomized: each time you reload the page the <div id="zoom"> content is different.
$rand=rand(0,$n); // $n is the number of users
$datas_zoom=zoom($n,$rand);
My PHP function
function zoom($n,$rand) {
global $tab_users;
$datas_user=new SimpleXMLElement($tab_users[$rand],null,true);
$tag=$datas_user->xpath('/user');
//if zoom found
if($tag[0]->zoom !='') {
$txt_zoom=$tag[0]->zoom;
}
... some other taff here
// no "zoom" value found
if ($txt_zoom =='') {
echo 'RAND='.$rand.' XML='.$tab_users[$rand].'<br />';
$datas_zoom=zoom($r,$n,$rand); } // random zoom fct again and again till...
}
else {
echo 'ZOOM='.$txt_zoom.'<br />';
return $txt_zoom; // we got it!
}
}
echo '<br />Return='.$datas_zoom;
The prob is: when by chance the first XML explored contains a "zoom" information the function returns it, but if not nothing returns... An exemple of results when the first one is by chance the good one:
// for RAND=0, XML=john.xml
ZOOM=Anything here
Return=Some content here // we're lucky
Unlucky:
RAND=1 XML=chris.xml
RAND=2 XML=phil.xml
// the for RAND=0 and XML=john.xml
ZOOM=Anything here
// content founded but Return is empty
Return=
What's wrong?
I suggest importing the values into a database table, generating a single local file or something like that. So that you don't have to open and parse all the XML files for each request.
Reading multiple files is a lot slower then reading a single file. And using a database even the random logic can be moved to SQL.
You're are currently using SimpleXML, but fetching a single value from an XML document is actually easier with DOM. SimpleXMLElement::xpath() only supports Xpath expression that return a node list, but DOMXpath::evaluate() can return the scalar value directly:
$document = new DOMDocument();
$document->load($xmlFile);
$xpath = new DOMXpath($document);
$zoomValue = $xpath->evaluate('string(//zoom[1])');
//zoom[1] will fetch the first zoom element node in a node list. Casting the list into a string will return the text content of the first node or an empty string if the list was empty (no node found).
For the sake of this example assume that you generated an XML like this
<zooms>
<zoom user="u1">z1</zoom>
<zoom user="u2">z2</zoom>
</zooms>
In this case you can use Xpath to fetch all zoom nodes and get a random node from the list.
$document = new DOMDocument();
$document->loadXml($xml);
$xpath = new DOMXpath($document);
$zooms = $xpath->evaluate('//zoom');
$zoom = $zooms->item(mt_rand(0, $zooms->length - 1));
var_dump(
[
'user' => $zoom->getAttribute('user'),
'zoom' => $zoom->textContent
]
);
Your main issue is that you are not returning any value when there is no zoom found.
$datas_zoom=zoom($r,$n,$rand); // no return keyword here!
When you're using recursion, you usually want to "chain" return values on and on, till you find the one you need. $datas_zoom is not a global variable and it will not "leak out" outside of your function. Please read the php's variable scope documentation for more info.
Then again, you're calling zoom function with three arguments ($r,$n,$rand) while the function can only handle two ($n and $rand). Also the $r is undiefined, $n is not used at all and you are most likely trying to use the same $rand value again and again, which obviously cannot work.
Also note that there are too many closing braces in your code.
I think the best approach for your problem will be to shuffle the array and then to use it like FIFO without recursion (which should be slightly faster):
function zoom($tab_users) {
// shuffle an array once
shuffle($tab_users);
// init variable
$txt_zoom = null;
// repeat until zoom is found or there
// are no more elements in array
do {
$rand = array_pop($tab_users);
$datas_user = new SimpleXMLElement($rand, null, true);
$tag=$datas_user->xpath('/user');
//if zoom found
if($tag[0]->zoom !='') {
$txt_zoom=$tag[0]->zoom;
}
} while(!$txt_zoom && !empty($tab_users));
return $txt_zoom;
}
$datas_zoom = zoom($tab_users); // your zoom is here!
Please read more about php scopes, php functions and recursion.
There's no reason for recursion. A simple loop would do.
$datas_user=new SimpleXMLElement($tab_users[$rand],null,true);
$tag=$datas_user->xpath('/user');
$max = $tag->length;
while(true) {
$test_index = rand(0, $max);
if ($tag[$test_index]->zoom != "") {
break;
}
}
Of course, you might want to add a bit more logic to handle the case where NO zooms have text set, in which case the above would be an infinite loop.

How to clean xml request before sending to web service

Good day i am trying to send an xml to a web service but would need to clean the xml before sending. So far I have tried different ways and now been stuck for a while.
I capture the data from a form and post it to my php file to process. If the user doesnt enter any data in the length/width/height then i would like to clean my xml and remove empty element so it can pass validation on the server where sending xml request too.
Here below is a snippet of the data cpatured from my post and build the xml file accordingly but what if the dimensions were omitted? Could I also clean other elements that are empty?
$xmlRequest = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<mailing-scenario xmlns="http://www.mysite.com/ws/ship/rate-v2">
<customer-number>{$mailedBy}</customer-number>
<parcel-characteristics>
<weight>{$weight}</weight>
<dimensions>
<length>{$length}</length>
<width>{$width}</width>
<height>{$height}</height>
</dimensions>
</parcel-characteristics>
<origin-postal-code>{$originPostalCode}</origin-postal-code>
<destination>
<domestic>
<postal-code>{$postalCode}</postal-code>
</domestic>
</destination>
</mailing-scenario>
XML;
$xmlRequest = phpquery::newDocument();
$xp = new DOMXPath($xmlRequest->getDOMDocument());
foreach($xp->query('//*[not(node()) or normalize-space() = ""]') as $node) {
$node->parentNode->removeChild($node);
}
Okay here an example with just simple dom. Maybe some points first, you'll have decide what to do if no customer number is given or a negative weight, ... .
So you have to clean the XML but sometimes cleaning it will make either the request invalid or the user might get some result he did not expect. For instance he might put 1kg as weight you remove the kg because the weight is set in g and a string there is just wrong. If you don't tell the user he might yell at you!
And also just because all nodes are valid does not mean the request is correct, as there might be some missing nodes, so you also have to check for the requirements!
One last word to efficency, if you can get all these fields from the user without XML because the user just sends one parcel at a time. Do it like that and just check if that data is correct.
If you have to use XML, put still just send one package at a time you can just fetch the data check the validity and reconstruct the validated XML.
I would just use this example if I know that these XML requests might be really extensive and/or have a complex format.
function cleanXML($data){
// ok the data is string! So get your node.
$node = simplexml_load_string($data);
// now we can iterate throught all child nodes:
foreach($node->children() as $child){
//so here we got the childrens
// All child nodes of the root should be mailing scenarios
if($child->getName() == "mailing-scenario"){
//otherwise we check if mailing scenario is valid
if(!validateMScenario($child)){
//This node seems not so valid
//you have to decide what to do now!
}
}
else{
//Here we remove all nodes that are different
unset($child[0]);
echo "Error: Needed to remove Node";
}
}
// Just give them their cleaned XML!
return $node->asXML();
}
function validateMScenario($ms){
// These var's safe if the requirements are fullfilled
$is_customer_number_set = 0
$is_weight_set = 0
$is_package_set = 0
// Again iterate over nodes
foreach($ms->children as $child){
//check for customer number
if($child->getName() == "customerNumber"){
if($is_customer_number_set == 1){
echo "You just need one customer number I guess?!"
return -1
}
value = (string) $child;
// Check if customer number is existing
if(strlen(value) == 0 || !is_int(value) || intval(value) == -1){
echo "Dude you need a number!";
return -1
}
$is_customer_number_set = 0;
}
else if($node->getName() == "parcel-characteristics"){
//Ok I hope it should be pretty clear what to do here!
//...
}
else{
//Remove node again?
}
}
// All requirements fullfilled?
return ($is_customer_number_set && $is_weight_set && $is_package_set);
}

Use PHP to load XML Data into Oracle

I'm fairly new to php although I've been programming for a couple years.
I'm working on a project and the end goal is to load certain elements of an xml file into an oracle table on a nightly basis. I have a script which runs nightly and saves a the file on my local machine. I've searched endlessly for answers but have been unsuccessful.
Here is an aggregated example of the xml file.
<?xml version="1.0" encoding="UTF-8" ?>
<Report account="7869" start_time="2012-02-23T00:00:00+00:00" end_time="2012-02-23T15:27:59+00:00" user="twilson" more_sessions="false">
<Session id="ID742247692" realTimeID="4306650378">
<Visitor id="5390643113837">
<ip>128.XXX.XX.XX</ip>
<agent>MSIE 8.0</agent>
</Visitor>
</Session>
<Session id="ID742247695" realTimeID="4306650379">
<Visitor id="7110455516320">
<ip>173.XX.XX.XXX</ip>
<agent>Chrome 17.0.963.56</agent>
</Visitor>
</Session>
</Report>
One thing to note is that the xml file will contain several objects which I will need to load into my table and the above example would just be for two rows of data. I'm familiar with the whole process of connecting and loading data into oracle and have setup similar scripts which perform ETL of txt. and csv. files using php. Unfortunately for me in this case the data is stored in xml. The approach I've taken when loading a csv. file is to load the data into an array and proceed from there.
I'm pretty certain that I can use something similar and perhaps create variable for each or something similar but am not really too sure how to do that with an xml. file.
$xml = simplexml_load_file('C:/Dev/report.xml');
echo $xml->Report->Session->Visitor->agent;
In the above code i'm trying to just return the agent associated with each visitor. This returns an error 'Trying to get property of non-object in C:\PHP\chatTest.php on line 11'
The end result would be for me to load the data into a table similar to the example I provided would be to load two rows into my table which would look similar to below however I think I can handle that if i'm able to get the data into an array or something similar.
IP|AGENT
128.XXX.XX.XX MSIE 8.0
173.XX.XX.XXX Chrome 17.0.963.56
Any help would be greatly appreciated.
Revised Code:
$doc = new DOMDocument();
$doc->load( 'C:/Dev/report.xml' );
$sessions = $doc->getElementsByTagName( "Session" );
foreach( $sessions as $session )
{
$visitors = $session->getElementsByTagName( "Visitor" );
foreach( $visitors as $visitor )
$sessionid = $session->getAttribute( 'realTimeID' );
{
$ips = $visitor->getElementsByTagName( "ip" );
$ip = $ips->item(0)->nodeValue;
$agents = $visitor->getElementsByTagName( "agent" );
$agent = $ips->item(0)->nodeValue;
echo "$sessionid- $ip- $agent\n";
}}
?>
The -> operator in PHP means that you are trying to invoke a field or method on an object. Since Report is not a method within $xml, you are receiving the error that you are trying to invoke a property on a non-object.
You can try something like this (don't know if it works, didn't test it and haven't written PHP for a long time, but you can google it):
$doc = new DOMDocument();
$doc->loadXML($content);
foreach ($doc->getElementsByTagName('Session') as $node)
{
$agent = $node->getElementsByTagName('Visitor')->item(0)->getElementsByTagName('agent')->item(0)->nodeValue;
}
edit:
Adding stuff to an array in PHP is easy as this:
$arr = array();
$arr[] = "some data";
$arr[] = "some more data";
The PHP arrays should be seen as a list, since they can be resized on the fly.
I was able to figure this out using simplexml_load_file rather than the DOM approach. Although DOM works after modifying the Leon's suggestion the approach below is what I would suggest.
$xml_object = simplexml_load_file('C:/Dev/report.xml');
foreach($xml_object->Session as $session) {
foreach($session->Visitor as $visitor) {
$ip = $visitor->ip;
$agent = $visitor->agent;
}
echo $ip.','.$agent."\n";
}

How to compare similar XMLs with PHPUnit?

So let's say I want to compare two DOMDocument objects. They have the same content but order and formatting might be off. For example, first one outputs this XML:
<responses>
<response id="12">
<foo>bar</foo>
<lorem>ipsum</lorem>
<sit>dolor</sit>
</response></responses>
Other one outputs:
<responses>
<response id="12">
<lorem>ipsum</lorem><sit>dolor</sit>
<foo>bar</foo>
</response>
</responses>
As you can see, they contain the same XML structure but some elements might be in different order and formatting is completely random.
If I do:
$this->assertEquals();
The test will of course fail. I don't want to test just XML structure but also contents.
Any ideas?
This seems to have solved the problem:
https://phpunit.de/manual/current/en/appendixes.assertions.html#appendixes.assertions.assertXmlStringEqualsXmlString
Which version of PHPUnit is this? I'm pretty sure recent versions all support DomDocument comparisons.
Short version: Use the $doc->preserveWhiteSpace setting to remove the whitespace, and then use $doc->C14N() to strip comments and get a string you can compare.
OK, here's a script you can play with, note that the EOD; lines cannot have any trailing or leading whitespace.
$x1 = <<<EOD
<responses>
<response id="12">
<foo>bar</foo>
<lorem>ipsum</lorem>
<sit>dolor</sit>
<!--This is a comment -->
</response></responses>
EOD;
$x2 = <<<EOD
<responses>
<response id="12">
<lorem>ipsum</lorem><sit>dolor</sit>
<foo>bar</foo>
<!--This is another comment -->
</response>
</responses>
EOD;
// The next block is part of the same file, I'm just making this formatting-break so that the StackOverflow syntax-highlighting system doesn't choke.
$USE_C14N = true; // Try false, just to see the difference.
$d1 = new DOMDocument(1.0);
$d2 = new DOMDocument(1.0);
$d1->preserveWhiteSpace = false;
$d2->preserveWhiteSpace = false;
$d1->formatOutput = false; // Only useful for "pretty" output with saveXML()
$d2->formatOutput = false; // Only useful for "pretty" output with saveXML()
$d1->loadXML($x1); // Must be done AFTER preserveWhiteSpace and formatOutput are set
$d2->loadXML($x2); // Must be done AFTER preserveWhiteSpace and formatOutput are set
if($USE_C14N){
$s1 = $d1->C14N(true, false);
$s2 = $d2->C14N(true, false);
} else {
$s1 = $d1->saveXML();
$s2 = $d2->saveXML();
}
echo $s1 . "\n";
echo $s2 . "\n";
Output with $USE_C14N=true;
<responses><response id="12"><foo>bar</foo><lorem>ipsum</lorem><sit>dolor</sit></response></responses>
<responses><response id="12"><lorem>ipsum</lorem><sit>dolor</sit><foo>bar</foo></response></responses>
Output with $USE_C14N=false;
<?xml version="1.0"?>
<responses><response id="12"><foo>bar</foo><lorem>ipsum</lorem><sit>dolor</sit><!--This is a comment --></response></responses>
<?xml version="1.0"?>
<responses><response id="12"><lorem>ipsum</lorem><sit>dolor</sit><foo>bar</foo><!--This is another comment --></response></responses>
Note that $doc->C14N() might be slower, but I think it seems likely that stripping out comments is desirable. Note that all of this also assumes that whitespace in your XML isn't important, and there are probably some use-cases where that assumption isn't right...
I suggest you turn the XML into DOMDocuments and then use assertEquals with those. It's already supported by PHPUnit - However that might not cover all your needs already.
You can re-format the documents and re-load them as well, see PHP XML how to output nice format:
$doc->preserveWhiteSpace = false;
$doc->formatOutput = true;
Another idea is to sort then the children by their tagname - no idea if that has been done before.
You can use PHPUnit's assertXmlFileEqualsXmlFile(), assertXmlStringEqualsXmlFile() and assertXmlStringEqualsXmlString() functions; yet, they do not give informations on what's different, they only let the test fail with
Failed asserting that two DOM documents are equal.
So you might want to use PHP's XMLDiff PECL extension, or write your own recursive comparison function. If time matters, I'd recommend to not use DOM but SimpleXML instead because of the simpler API.
I've been playing with some of the notions presented here and figured I might as well post my end result. One of the things I wanted to be able to do was to compare the results of two nodes or two documents. (technically, this one can compare either or so long as the first child of a similar document is being compared to another)
Basically if I send in a DomDocument, it clones it using a $clone->loadXml($obj->saveXml) but if it's a node sent in, it does a $clone->importNode($obj);
The order of the if's becomes important because DomDocument is also a instance of DomNode.
/**
* #param \DOMDocument|\DOMNode $n1
* #param \DOMDocument|\DOMNode $n2
* #return bool
* #throws \Exception for invalid data
*/
function compareNode($n1, $n2)
{
$nd1 = new \DOMDocument('1.0', "UTF-8");
if ($n1 instanceof \DOMDocument) {
$nd1 = $n1->cloneNode(true);
$nd1->preserveWhiteSpace = false;
$nd1->formatOutput = false;
$nd1->loadXML($n1->saveXML());
} elseif ($n1 instanceof \DOMNode) {
$nd1->preserveWhiteSpace = false;
$nd1->formatOutput = false;
$nd1->importNode($n1);
} else {
throw new \Exception(__METHOD__ . " node 1 is invalid");
}
$nd2 = new \DOMDocument('1.0', "UTF-8");
if ($n2 instanceof \DOMDocument) {
$nd2 = $n2->cloneNode(true);
$nd2->preserveWhiteSpace = false;
$nd2->formatOutput = false;
$nd2->loadXML($n2->saveXML());
} elseif ($n1 instanceof \DOMNode) {
$nd2->preserveWhiteSpace = false;
$nd2->formatOutput = false;
$nd2->importNode($n2);
} else {
throw new \Exception(__METHOD__ . " node 2 is invalid");
}
return ($nd1->C14N(true, false) == $nd2->C14N(true, false));
}
Use the following assertion:
$this->assertXmlStringEqualsXmlString($expected, $actual);

PHP XML. IP validation

My Xml looks like this example:
<?xml version="1.0" encoding="UTF-8"?>
<Allvotes>
<vote score="2" ip="116.971.203.221"/>
<vote score="5" ip="32.97.233.5"/>
<vote score="3" ip="212.977.233.225"/>
<vote score="5" ip="2.80.233.225"/>
</Allvotes>
When on my flash website (AS2), somebody press "vote" button, script in PHP getting his IP... What I want is run specyfic function, depends on his IP exist in xml file or not.
If his IP already exist, PHP send message: "ALREADY VOTED!", when IP doesn't exist in XML, then I want to run function which store his vote score and IP in xml.
So far I know that this PHP script not works:
$dom = new DomDocument('1.0', 'UTF-8');
$myXML = "votes.xml";
$s="";
if ($_POST['todo']=="vote"){
$ip=$_SERVER['REMOTE_ADDR'];
$dom->load($myXML);
$allVotes= $dom->getElementsByTagName('vote');
foreach ($allVotes as $vote){
if ($vote->getAttribute('ip')==$ip){
$s.="&msg= Already Voted";
echo $s;
break;
}else{
doOtherStuff
}
}
}
The problem is that this loop fire "doOtherStuff" function when IP is not in first node...
Is there any magic trick to do that?
Why your code does not work
To answer the immediate question: you need to defer the "already voted?" test until you have iterated over all the records:
$alreadyVoted = false;
foreach ($allVotes as $vote){
if ($vote->getAttribute('ip')==$ip){
$alreadyVoted = true;
break;
}
}
if($alreadyVoted) {
$s.="&msg= Already Voted";
echo $s;
}
else {
// other stuff
}
Why you should not do it this way
Storing your data in XML like this is a really inefficient way of doing things. You should move the data store to a database (MySql is typically easiest to set up and work with from PHP).

Categories