SimpleXML: how to get node value with a condition on parse rule? - php

I have a XML file which was generated by an external entity that I want to parse using SimpleXML. My problem is that in the mapping given by the client I have some conditions in it to get the info I want.
For example, the mapping for the client code is something like this: E1ADRM1\PARTNER_Q=OSO\E1ADRE1\EXTEND_D which means that the code for the client is the value of the EXTEND_D tag, which is nested in one of the many PARTNER_Q tags. The one that has the OSO value.
I am starting today to explore SimpleXML, so I have no idea how to get this info.
For what I've read so far, it is pretty simple to get the info of a node, accessing it's properties. If I a single PARTNER_Q and no condition, my $clientCode would be $xml->E1ADRM1->PARTNER_Q->E1ADRE1->EXTEND_D (right?)
Any hint on how can I get the info having that PARTNER_Q=OSO condition in mind?

For future reference, I'll leave here the code with how I've solved this.
I've Based my code on this well written answer that I found.
$root = $xml->IDOC;
$shippingPoints = array();
// Go through the list of OTs
foreach($root->E1EDL20 as $ot) {
// Instanciate empty OT
$otInfo = emptyOt();
$otInfo["refOt"] = trim($ot->VBELN);
// Go through partner to get the wanted items
foreach ($ot->E1ADRM1 as $partner) {
switch ($partner->PARTNER_Q) {
case "OSO":
$otInfo["codeLoader1"] = trim($partner->E1ADRE1->EXTEND_D);
break;
case "OSP":
$otInfo["refShip"] = trim($partner->PARTNER_ID);
// get the shipping info if not already fetched
if(!isset($shippingPoints[$otInfo["refShip"]])) {
$shippingPoints[$otInfo["refShip"]] = Whouses::getWhouseAddressByCode($otInfo["refShip"]);
}
// assign values
$otInfo["brandNameShip"] = $shippingPoints[$otInfo["refShip"]]["name"];
$otInfo["addrShip1"] = $shippingPoints[$otInfo["refShip"]]["address"];
$otInfo["cityShip"] = $shippingPoints[$otInfo["refShip"]]["city"];
$otInfo["zipShip"] = $shippingPoints[$otInfo["refShip"]]["zipcode"];
$otInfo["countryShip"] = $shippingPoints[$otInfo["refShip"]]["country"];
break;
case "WE":
$otInfo["refDeliv"] = trim($partner->PARTNER_ID);
$otInfo["addrDeliv1"] = trim($partner->STREET1);
$otInfo["cityDeliv"] = trim($partner->CITY1);
$otInfo["zipDeliv"] = trim($partner->POSTL_COD1);
$otInfo["countryDeliv"] = trim($partner->COUNTRY1);
default:
// do nothing
break;
}
}
// Go through the dates info to get the wanted items
foreach ($ot->E1EDT13 as $qualf) {
switch ($qualf->QUALF) {
case "006":
$dtDeliv = trim($qualf->NTANF);
$timeDeliv = trim($qualf->NTANZ);
$otInfo["initialDtDeliv"] = convertToOtDateFormat($dtDeliv, $timeDeliv);
break;
case "007":
$initialDtShip = trim($qualf->NTANF);
$timeInitialDtShip = trim($qualf->NTANZ);
$otInfo["initialDtShip"] = convertToOtDateFormat($initialDtShip, $timeInitialDtShip);
break;
default:
// do nothing
break;
}
}
}
Hope this'll help someone!

Related

How to use the Open/Closed Principle to replace switch block that modifies shared state

I am working on a project that requires me to generate a report based on purchased rentals. I have to keep a count for each type of rental and sum the corresponding totals. Currently I am using a switch block to determine which action to take based on the current rental. However, it is my understanding that this violates the Open/Closed Principle, as I will have to modify the switch block every time a new rental is added. I would like to make this meet the OCP, but I am not sure how to go about. Below is my code:
public function generateReport($rentals)
{
$type_1_count = 0;
$type_2_count = 0;
$type_3_count = 0;
$type_1_sum = 0;
$type_2_sum = 0;
$type_3_sum = 0;
foreach ($rentals as $rental) {
switch ($rental->type) {
case 'TYPE_1':
$type_1_count++;
$type_1_sum += $rental->price;
break;
case 'TYPE_2':
$type_2_count++;
$type_2_sum += $rental->price;
break;
case 'TYPE_3':
// some of the rentals include other rentals which must be accounted for
$type_1_count++;
$type_1_sum += $rental->price / 2;
$type_3_count++;
$type_3_sum += $rental->price / 2;
break;
default:
echo 'Rental Not Identified';
}
}
return compact('type_1_count', 'type_2_count', 'type_3_count', 'type_1_sum', 'type_2_sum', 'type_3_sum');
}
I am modifying the shared state variables depending on the selected case. I reviewed many OCP examples but all of them show how to execute an action or return a value, but I need to modify the shared state instead. What would be the best way to refactor this code to be more inline with the OCP?
You can use an associative array. You just need to make sure the variable is set first. Something like this:
$all_rentals = [];
foreach ($rentals as $rental) {
// make sure the variable has a default value
if(!isset($all_rentals[$rental->type."_count"]) {
$all_rentals[$rental->type."_count"] = 0;
$all_rentals[$rental->type."_sum"] = 0;
}
$all_rentals[$rental->type."_count"]++;
$all_rentals[$rental->type."_sum"] += $rental->price;
}
...
This way you can add new values (rental types) without having to modify any existing code

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.

What's the best way to do this?

I am trying to get different sums depending on certain conditions, there needs to be 20 different variables which will be returned at the end of the function. I feel like there should be an easier way to do it? Thanks for any help in advanced.
function getTotalNutrients($id)
{
$applications = $this->getApplications($id);
$nsum = 0;
$psum = 0;
$ksum = 0;
$mgsum = 0;
// ...
foreach( $applications as $pro ) {
$details = $this->getAppliedNutrients($pro->id);
foreach($details as $nutrients){
switch($pro->area){
case "1":
$nsum += ($nutrients->n);
$psum += $nutrients->p;
$ksum += $nutrients->k;
$mgsum += $nutrients->mg;
break;
case "2":
$tnsum += ($nutreients->n);
// ...
break;
case "3":
$fnsum += ($nutreients->n);
//...
break;
case "4":
// ...
break;
case "5":
// ...
break;
}
}
}
return array(
$nsum,
$psum,
$ksum,
$mgsum,
$tnsum,
// etc..
);
}
create an array by adding all values like
$arraySum[] = array();
$arraySum[] = $nsum;
$arraySum[] = $tnsum;
.
.
.
ect
at the end
use
array_sum($arraySum);
If it is not fullfill your requirement. provide your code completely.
You have a case of design patterns here my friend...
Instead of using a large switch (breaks the Open/Closed principle) you should create an interface that calculates the different sums and returns a tupple (set of different values) of your choice.
Then create those AREAS of yours following the interface details. Once all your areas calculators are created, just create each of them and put them in an array based on the id. Then, load the right one using the id and calculate your values returned in a tupple...
Your code will look much tidier!

Passing Object Operators As Strings (PHP)

I'm building a script that takes the contents of several (~13) news feeds and parses the XML data and inserts the records into a database. Since I don't have any control over the structure of the feeds, I need to tailor an object operator for each one to drill down into the structure in order to get the information I need.
The script works just fine if the target node is one step below the root, but if my string contains a second step, it fails ( 'foo' works, but 'foo->bar' fails). I've tried escaping characters and eval(), but I feel like I'm missing something glaringly obvious. Any help would be greatly appreciated.
// Roadmaps for xml navigation
$roadmap[1] = "deal"; // works
$roadmap[2] = "channel->item"; // fails
$roadmap[3] = "deals->deal";
$roadmap[4] = "resource";
$roadmap[5] = "object";
$roadmap[6] = "product";
$roadmap[8] = "channel->deal";
$roadmap[13] = "channel->item";
$roadmap[20] = "product";
$xmlSource = $xmlURL[$fID];
$xml=simplexml_load_file($xmlSource) or die(mysql_error());
if (!(empty($xml))) {
foreach($xml->$roadmap[$fID] as $div) {
include('./_'.$incName.'/feedVars.php');
include('./_includes/masterCategory.php.inc');
$test = sqlVendors($vendorName);
} // end foreach
echo $vUpdated." records updated.<br>";
echo $vInserted." records Inserted.<br><br>";
} else {
echo $xmlSource." returned an empty set!";
} // END IF empty $xml result
While Fosco's solution will work, it is indeed very dirty.
How about using xpath instead of object properties?
$xml->xpath('deals/deal');
PHP isn't going to magically turn your string which includes -> into a second level search.
Quick and dirty hack...
eval("\$node = \"\$xml->" . $roadmap[$fID] . "\";");
foreach($node as $div) {

PHP switch with GET request

I am building a simple admin area for my site and I want the URLs to look somewhat like this:
http://mysite.com/admin/?home
http://mysite.com/admin/?settings
http://mysite.com/admin/?users
But I am not sure how I would retrieve what page is being requested and then show the required page. I tried this in my switch:
switch($_GET[])
{
case 'home':
echo 'admin home';
break;
}
But I get this error:
Fatal error: Cannot use [] for reading in C:\path\to\web\directory\admin\index.php on line 40
Is there any way around this? I want to avoid setting a value to the GET request, like:
http://mysite.com/admin/?action=home
If you know what I mean. Thanks. :)
Use $_SERVER['QUERY_STRING'] – that contains the bits after the ?:
switch($_SERVER['QUERY_STRING']) {
case 'home':
echo 'admin home';
break;
}
You can take this method even further and have URLs like this:
http://mysite.com/admin/?users/user/16/
Just use explode() to split the query string into segments, get the first one and pass the rest as arguments for the method:
$args = explode('/', rtrim($_SERVER['QUERY_STRING'], '/'));
$method = array_shift($args);
switch($method) {
case 'users':
$user_id = $args[2];
doSomething($user_id);
break;
}
This method is popular in many frameworks that employ the MVC pattern. An additional step to get rid of the ? altogether is to use mod_rewrite on Apache servers, but I think that's a bit out of scope for this question.
As well as the ones mentioned, another option would be key($_GET), which would return the first key of the $_GET array which would mean it would work with URLs with other parameters
www.example.com/?home&myvar = 1;
The one issue is that you may want to use reset() on the array first if you have modified the array pointer as key returns the key of the element array pointer is currently pointing to.
The PHP code:
switch($_GET){
case !empty($_GET['home']):
// code here
break;
case !empty($_GET['settings']):
// code here
break;
default:
// code here
break;
}
$_SERVER['QUERY_STRING']
Is not the most "elegant" way to do it but the simplest form to answer your question is..
if (isset($_GET['home'])):
# show index..
elseif (isset($_GET['settings'])):
# settings...
elseif (isset($_GET['users'])):
# user actions..
else:
# default action or not...
endif;
You can make your links "look nicer" by using the $_SERVER['REQUEST_URI'] variable.
This would allow you to use URLs like:
http://mysite.com/admin/home
http://mysite.com/admin/settings
http://mysite.com/admin/users
The PHP code used:
// get the script name (index.php)
$doc_self = trim(end(explode('/', __FILE__)));
/*
* explode the uri segments from the url i.e.:
* http://mysite.com/admin/home
* yields:
* $uri_segs[0] = admin
* $uri_segs[1] = home
*/
// this also lower cases the segments just incase the user puts /ADMIN/Users or something crazy
$uri_segs = array_values(array_filter(explode('/', strtolower($_SERVER["REQUEST_URI"]))));
if($uri_segs[0] === (String)$doc_self)
{
// remove script from uri (index.php)
unset($uri_segs[0]);
}
$uri_segs = array_values($uri_segs);
// $uri_segs[1] would give the segment after /admin/
switch ($uri_segs[1]) {
case 'settings':
$page_name = 'settings';
break;
case 'users':
$page_name = 'users';
break;
// use 'home' if selected or if an unexpected value is given
case 'home':
default:
$page_name = 'home';
break;
}

Categories