I am trying to make a function that changes part of an XML using XPath. I used part of someone else post:
/*********************************************************************
Function to replace part of an XML
**********************************************************************/
function replacePartofXML($element, $methodName, $methodValue, $xml, $newPartofXML)
{
$xpathstring = "//" . $element . "[#$methodName = \"$methodValue\"]";
$xml->xpath($xpathstring);
//$domToChange = dom_import_simplexml($xml->xpath($xpathstring));
$domToChange = dom_import_simplexml($xml);
$domReplace = dom_import_simplexml($newPartofXML);
$nodeImport = $domToChange->ownerDocument->importNode($domReplace, TRUE);
$domToChange->parentNode->replaceChild($nodeImport, $domToChange);
return($xml);
}
What I want to do is return the appended XML. I can't use dom_import_simplexml($xml->node->node) as my XML has many repeating element (but they have different ID reason why I am trying to use xpath)
The commented line does not work either as xpath returns an array and dom_import_simplexml is cannot import arrays.
Thanks for you input
You can take the first element returned by xpath() in case you believe the target element is unique (no-element-returned checking omitted) :
$domToChange = dom_import_simplexml($xml->xpath($xpathstring)[0]);
or iterate through the return value of xpath() and replace one by one.
Related
I have a dom element of which i want to find if exists a specific sub-element.
My node is like this:
<properties>
<property name="a-random-neme" option="option2" >value1</property>
<property name="another-random-name">V2</property>
<property name="yet-another-random-name" option="option5" >K3</property>
</properties>
in php it is referenced by a dom object
$properties_node;
In another part of php code I want to check if a datum I'm going to add already exists
$datum = [ 'name'=>'yet-another-random-name', 'value'=>'K3'];
//NOTE: If other attributes exists I want to keep them
$prop=$dom->createElement('property',$datum['value']);
$prop->setAttribute('name', $datum['name']);
if(prop_list_contains($properties-node,$prop,['name']))
$properties_node->appendChild($prop);
else
echo "not adding element, found\n";
now I want to make
/**
#param $properties_node reference to the existing dom object
#param $prop the new element I want to add
#param $required_matches an array containing the name of the attributes that must match
#return matching element if match is found, false otherweise
*/
function prop_list_contains(&$properties_node,$prop,array $required_matches){
// here I have no Idea how to parse the document I have
return false
}
Desiderata:
not adding element, found
The easiest way I can think if is to use XPath to check if the node already exists.
Assuming that you will only use 1 element to match on (more is possible, but much more complicated). This first extracts the value from the new node and then uses XPath to check if a matching value already exists in the current data.
The main thing about this process is to ensure you use the correct context for the search. This is effectively what to search in, first this uses the new element, then the current one to check it.
function prop_list_contains(DOMXPath $xp, $properties_node, $prop,
array $required_matches){
// Extract value from new node
$compare = $xp->evaluate('string(#'.$required_matches[0].')', $prop);
// Check for the value in the existing data
$xpath = 'boolean(./property[#'. $required_matches[0] . ' = "' . $compare . '"])';
return ( $xp->evaluate($xpath, $properties_node) );
}
This will also mean you need to create the XPath object to pass in...
$xp = new DOMXPath($dom);
this saves creating it each time.
Also as this returns true if the node exists, you need to change your test to use !...
if( ! prop_list_contains($xp, $properties_node,$prop,['name'])) {
$properties_node->appendChild($prop);
}
else {
echo "not adding element, found\n";
}
Everything is about PHP.
I think example will do the best.
I want the node to be <shippingCost>23</shippingCost>. But instead, xml outputs me <shippingcost>23</shippingcost>. I want to connect to an private external API, which only accepts the first form.
I was trying to use DomDocument and SimpleXMLElement classes in PHP, no of them could output me xml code without case-folding to lowercase. I was searching for some options too in both, but no of them was about lowercasing.
$input_xml = new DOMDocument("1.0","utf-8");
$super_root = addChild($input_xml,$input_xml,'orderExport','');
// ^ this gives me <orderexport></orderexport>
...
function addChild($doc,$node,$marker,$value){
$temp = $doc->createElement($marker);
$temp->appendChild($doc->createTextNode($value));
$node->appendChild($temp);
return $temp;
}
...
addChild($input_xml,$shipping_info,'shippingEmail',$mail);
...
$output = $input_xml->saveXml()
I expect to get for example
<camelCase>123</camelCase>
tag, but i get
<camelcase>123</camelcase>
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.
I need to sort the following XML (foreach ProgramList) based on the value of it's child MajorDescription
<ArrayOfProgramList xmlns="http://schemas.datacontract.org/2004/07/Taca.Resources" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<ProgramList>
<AreaOfInterests xmlns:a="http://schemas.datacontract.org/2004/07/Taca">
<a:AreaOfInterest>
<a:Interest>ABORIGINAL STUDIES</a:Interest>
</a:AreaOfInterest>
</AreaOfInterests>
<Coop>true</Coop>
<MajorDescription>ABORIGINAL COMMUNITY AND SOCIAL DEVELOPMENT</MajorDescription>
<Program>ACSD</Program>
<ProgramLocations>
<ProgramLocation>
<Campus>Barrie</Campus>
</ProgramLocation>
</ProgramLocations>
<Term>201210</Term>
</ProgramList>
<ProgramList>
<AreaOfInterests xmlns:a="http://schemas.datacontract.org/2004/07/Taca">
<a:AreaOfInterest>
<a:Interest>GRADUATE CERTIFICATE STUDIES</a:Interest>
</a:AreaOfInterest>
<a:AreaOfInterest>
<a:Interest>HEALTH AND WELLNESS STUDIES</a:Interest>
</a:AreaOfInterest>
</AreaOfInterests>
<Coop>false</Coop>
<MajorDescription>ADVANCED CARE PARAMEDIC</MajorDescription>
<Program>PARM</Program>
<ProgramLocations>
<ProgramLocation>
<Campus>Barrie</Campus>
</ProgramLocation>
</ProgramLocations>
<Term>201210</Term>
</ProgramList>
</ArrayOfProgramList>
I'm trying to do it with SimpleDOM as I've read thats the easiest way to sort XML on other SO questions.
I've tried using:
foreach($listxml->sortedXPath('//ArrayOfProgramList/ProgramList','//ArrayOfProgramList/ProgramList/MajorDescription') as $program){ ... }
and various other similar 'sort' values such as '#MajorDescription', '/MajorDescription' and '.' as suggested here How does one use SimpleDOM sortedXPath to sort on node value? but everything returns an empty array when I check it with var_dump()
I think the problem is that I need to sort on the value of a child node - is this possible? The foreach needs to be on ProgramList as I need to output the values of all the child nodes within ProgramList on each iteration.
Any suggestions? I don't have to use SimpleDOM, I'm open to any method that works - currently I'm iterating through an array of A-Z, and for each letter, iterating the ProgramList, comparing the first letter of MajorDescription to the current letter and outputting if it matches - this is obviously not ideal and only sorts the first letter...
You can try to put all the ProgramList elements into an array and sort it according to a custom function. The code should look like this:
function cmp($a, $b)
{
return strcmp($a->MajorDescription[0],$b->MajorDescription[0])
}
$arr = $listxml->xpath("//ArrayOfProgramList/ProgramList");
usort($arr,"cmp");
There are two problems with your original code. The first is that your XML uses a default namespace, and by design, XPath doesn't support default namespaces so you have to look for namespaced node (e.g. //foo:bar, not //bar) to find them. If you cannot register a prefix for this namespace (for example, if you cannot modify the source XML) you can match namespaced nodes using the wildcard //* combined with a predicate that matches the node's namespace and/or local name.
$nsPredicate = '[namespace-uri() = "http://schemas.datacontract.org/2004/07/Taca.Resources"]';
$query = '//*[local-name() = "ArrayOfProgramList"]' . $nsPredicate
. '/*[local-name() = "ProgramList"]' . $nsPredicate;
$orderBy = '*[local-name() = "MajorDescription"]' . $nsPredicate;
foreach ($listxml->sortedXPath($query, $orderBy) as $program)
{
echo $program->asXML(),"\n";
}
The other problem is with your sort criterion. It should be written from the target node's context.
I need to get the HTML contents of answer in this bit of XML:
<qa>
<question>Who are you?</question>
<answer>Who who, <strong>who who</strong>, <em>me</em></answer>
</qa>
So I want to get the string "Who who, <strong>who who</strong>, <em>me</em>".
If I have the answer as a SimpleXMLElement, I can call asXML() to get "<answer>Who who, <strong>who who</strong>, <em>me</em></answer>", but how to get the inner XML of an element without the element itself wrapped around it?
I'd prefer ways that don't involve string functions, but if that's the only way, so be it.
function SimpleXMLElement_innerXML($xml)
{
$innerXML= '';
foreach (dom_import_simplexml($xml)->childNodes as $child)
{
$innerXML .= $child->ownerDocument->saveXML( $child );
}
return $innerXML;
};
This works (although it seems really lame):
echo (string)$qa->answer;
To the best of my knowledge, there is not built-in way to get that. I'd recommend trying SimpleDOM, which is a PHP class extending SimpleXMLElement that offers convenience methods for most of the common problems.
include 'SimpleDOM.php';
$qa = simpledom_load_string(
'<qa>
<question>Who are you?</question>
<answer>Who who, <strong>who who</strong>, <em>me</em></answer>
</qa>'
);
echo $qa->answer->innerXML();
Otherwise, I see two ways of doing that. The first would be to convert your SimpleXMLElement to a DOMNode then loop over its childNodes to build the XML. The other would be to call asXML() then use string functions to remove the root node. Attention though, asXML() may sometimes return markup that is actually outside of the node it was called from, such as XML prolog or Processing Instructions.
most straightforward solution is to implement custom get innerXML with simple XML:
function simplexml_innerXML($node)
{
$content="";
foreach($node->children() as $child)
$content .= $child->asXml();
return $content;
}
In your code, replace $body_content = $el->asXml(); with $body_content = simplexml_innerXML($el);
However, you could also switch to another API that offers distinction between innerXML (what you are looking for) and outerXML (what you get for now). Microsoft Dom libary offers this distinction but unfortunately PHP DOM doesn't.
I found that PHP XMLReader API offers this distintion. See readInnerXML(). Though this API has quite a different approach to processing XML. Try it.
Finally, I would stress that XML is not meant to extract data as subtrees but rather as value. That's why you running into trouble finding the right API. It would be more 'standard' to store HTML subtree as a value (and escape all tags) rather than XML subtree. Also beware that some HTML synthax are not always XML compatible ( i.e. vs , ). Anyway in practice, you approach is definitely more convenient for editing the xml file.
I would have extend the SimpleXmlElement class:
class MyXmlElement extends SimpleXMLElement{
final public function innerXML(){
$tag = $this->getName();
$value = $this->__toString();
if('' === $value){
return null;
}
return preg_replace('!<'. $tag .'(?:[^>]*)>(.*)</'. $tag .'>!Ums', '$1', $this->asXml());
}
}
and then use it like this:
echo $qa->answer->innerXML();
<?php
function getInnerXml($xml_text) {
//strip the first element
//check if the strip tag is empty also
$xml_text = trim($xml_text);
$s1 = strpos($xml_text,">");
$s2 = trim(substr($xml_text,0,$s1)); //get the head with ">" and trim (note that string is indexed from 0)
if ($s2[strlen($s2)-1]=="/") //tag is empty
return "";
$s3 = strrpos($xml_text,"<"); //get last closing "<"
return substr($xml_text,$s1+1,$s3-$s1-1);
}
var_dump(getInnerXml("<xml />"));
var_dump(getInnerXml("<xml / >faf < / xml>"));
var_dump(getInnerXml("<xml >< / xml>"));
var_dump(getInnerXml("<xml>faf < / xml>"));
var_dump(getInnerXml("<xml > faf < / xml>"));
?>
After I search for a while, I got no satisfy solution. So I wrote my own function.
This function will get exact the innerXml content (including white-space, of course).
To use it, pass the result of the function asXML(), like this getInnerXml($e->asXML()). This function work for elements with many prefixes as well (as my case, as I could not find any current methods that do conversion on all child node of different prefixes).
Output:
string '' (length=0)
string '' (length=0)
string '' (length=0)
string 'faf ' (length=4)
string ' faf ' (length=6)
function get_inner_xml(SimpleXMLElement $SimpleXMLElement)
{
$element_name = $SimpleXMLElement->getName();
$inner_xml = $SimpleXMLElement->asXML();
$inner_xml = str_replace('<'.$element_name.'>', '', $inner_xml);
$inner_xml = str_replace('</'.$element_name.'>', '', $inner_xml);
$inner_xml = trim($inner_xml);
return $inner_xml;
}
If you don't want to strip CDATA section, comment out lines 6-8.
function innerXML($i){
$text=$i->asXML();
$sp=strpos($text,">");
$ep=strrpos($text,"<");
$text=trim(($sp!==false && $sp<=$ep)?substr($text,$sp+1,$ep-$sp-1):'');
$sp=strpos($text,'<![CDATA[');
$ep=strrpos($text,"]]>");
$text=trim(($sp==0 && $ep==strlen($text)-3)?substr($text,$sp+9,-3):$text);
return($text);
}
You can just use this function :)
function innerXML( $node )
{
$name = $node->getName();
return preg_replace( '/((<'.$name.'[^>]*>)|(<\/'.$name.'>))/UD', "", $node->asXML() );
}
Here is a very fast solution i created:
function InnerHTML($Text)
{
return SubStr($Text, ($PosStart = strpos($Text,'>')+1), strpos($Text,'<',-1)-1-$PosStart);
}
echo InnerHTML($yourXML->qa->answer->asXML());
using regex you could do this
preg_match(’/<answer(.*)?>(.*)?<\/answer>/’, $xml, $match);
$result=$match[0];
print_r($result);