INSERT node into PHP AST with nikic/PHP-Parser - php

Im using https://github.com/nikic/PHP-Parser. What is a good strategy when wanting to INSERT a node in the AST? With the traverser I can UPDATE and DELETE nodes easily using a NodeTraverser class. But how can I "INSERT before" or "INSERT after" a node?
Example: When traversing an AST namespace I want to INSERT a Use statement just before the first non-use statement.
I started working with beforeTraverse and afterTraverse to find indexes of arrays but it seems overly complicated. Any ideas?

It is possible to replace one node with multiple nodes. This only works inside leaveNode and only if the parent structure is an array.
public function leaveNode(Node $node) {
if ($node instanceof Node\Stmt\Return_ && $node->expr !== null) {
// Convert "return foo();" into "$retval = foo(); return $retval;"
$var = new Node\Expr\Variable('retval');
return [
new Node\Stmt\Expression(new Node\Expr\Assign($var, $node->expr)),
new Node\Stmt\Return_($var),
];
}
}
See last section in Modyfing the AST

Related

asXML() not saving changes

I have a complex xml with nested namespaces for which I'm trying to do the following:
1) Open XML File
2) Validate against a XSD Schema
3) Parse it
4) Change nodes (1 at the time, setting them either to null or other variables)
5) Saves changed xml into a new file
5) Ri-validate it against same schema as 2) and make sure an error pops up.
Now, points 1-2-3 and 5-6 are not an issue. The Change + saving into a new xml is.
XML Snippet:
<Movie creationDateTime="2014-05-14T13:42:52Z" endDateTime="2015-05-14T00:00:00Z" providerVersionNum="5" startDateTime="2014-05-14T00:00:00Z" uriId="disney.chlsd.com/MOOT0000000000020902">
<core:Ext>
<ext:MovieExtensions analogueOff="true" mediaId="CGOT0000000000020902">
<ext:assetPart partNum="1">
<ext:SourceUrl>DSNY0000000000020902.mxf</ext:SourceUrl>
<ext:ContentFileSize>46166173874</ext:ContentFileSize>
<ext:ContentCheckSum>4da3e4cafd4f3262d136c519311a7b53</ext:ContentCheckSum>
<ext:SOE>PT09H59M30S00F</ext:SOE>
<ext:SOM>PT10H00M00S00F</ext:SOM>
<ext:EOM>PT10H46M02S11F</ext:EOM>
</ext:assetPart>
<ext:playlistSupportOnly>false</ext:playlistSupportOnly>
</ext:MovieExtensions>
</core:Ext>
<content:AudioType>Stereo</content:AudioType>
<content:FrameRate>25</content:FrameRate>
<content:Codec>H.264</content:Codec>
<content:AVContainer>MXF</content:AVContainer>
<content:Duration>PT00H46M02S</content:Duration>
<content:IsHDContent>false</content:IsHDContent>
</Movie>
I do the parsing on attributes using ($mypix is the XmlSimpleObject where I load the Xml):
$xmlfile = "prova.xml";
$mypix = simplexml_load_file($xmlfile);
[...]
foreach ($mypix->children() as $parent => $child)
{
echo "<br/>Main Node: ".(String)$parent."<br/>";
foreach ($mypix->children()->attributes() as $a => $b)
{
echo "Main attribute: ".(String)$a. " with value: ".(String)$b."<br/>";
if ($a == "endDateTime")
{
echo "Entering node: ".$a." and eliminating: ".$b." <br/>";
$b=NULL;
echo "<br/><pre>";
echo $mypix->asXML("t.xml");
echo "<br/></pre>";
}
}
}
The parsing gives me:
Main Node: Movie
Main attribute: creationDateTime with value: 2014-05-16T14:40:41Z
Main attribute: endDateTime with value: 2015-05-16T00:00:00Z
Entering node: endDateTime and eliminating: 2015-05-16T00:00:00Z
Problem is, when I open t.xml, endDateTime is still a valid tag (definitely not empty).
=========================================================================
Things I've tried:
alternative approach using Xpath:
$namespaces = $mypix->getNameSpaces(true);
$mypix->registerXPathNamespace('ext', 'URN:NNDS:CMS:ADI3:01');
$mypix->registerXPathNamespace('title', 'http://www.cablelabs.com/namespaces/metadata/xsd/title/1');
$mypix->registerXPathNamespace('core', 'http://www.cablelabs.com/namespaces/metadata/xsd/core/1');
echo "<br/><br/>";
// Getting Episode Name
$xtring = ($mypix->xpath('//core:Ext/ext:LocalizableTitleExt/ext:EpisodeName'));
echo "<br/><b>EpisodeName: </b>".$xtring[0]."<br/>";
$xtring[0] = NULL;
echo $mypix->asXML("t.xml"); // Nothing again
Here the xpath query returns a valid value, but changing & writing to a new file fails
2nd try: save to the same file ('prova.xml') instead of 't.xml' (in case I screwed up with SimpleXMlObjects)...nothing...
Any help please?
Setting a variable to null does not remove, destroy, or edit the object that variable used to point to.
You may have seen examples where this is a valid way of "cleaning up" something like a database connection object, because when you remove all references to an object, its destructor will be called. However, this is not the case here, because the object pointed at by $b is still accessible, e.g. from another call to $mypix->children()->attributes().
The other thing you will have seen in examples is assigning a new value to a child element or attribute using syntax like $element->someChild = 'new value'; or $element['someAttribute'] = 'new value';. However, this works because SimpleXML overloads the property access (->) and array element access ([...]), in the same way as implementing __set() and ArrayAccess::offsetSet(), and your code uses neither of those.
There is a way of using the array-access overload to delete or blank an element which you have a variable pointing at directly, which is that the offset [0] points back at the current element. Thus, you can write unset($b[0]); to delete an element or attribute completely; you can also write $b[0] = ''; to blank an element, but with an attribute as here, that leads to a fatal error (which I suspect is a bug).
Note that when you use XPath, you are not actually reaching this self-reference, or an overloaded operator because SimpleXMLElement::xpath returns a plain array, so $xtring[0] is just a normal PHP variable. Since it's an element in that example, you could delete it using the self-reference, by writing unset($xtring[0][0]); or blank it with $xtring[0][0] = '';
However, all that being said, your code can actually be massively simplified in order to avoid any of this being necessary. Let's take it apart line by line:
foreach ($mypix->children() as $parent => $child)
The variable $mypix here is for a larger document than you show in your sample, the sample apparently being just one entry in this loop. Note that $parent => $child here would be more appropriately named $childName => $child.
It's also quite likely that you're only interested in children with a particular name, so the most common form of loop is foreach ($mypix->Movie as $child)
foreach ($mypix->children()->attributes() as $a => $b)
Here you ignore the progress around the outer loop completely, and go back to the whole document. SimpleXML will interpret $mypix->children()->... as $mypix->children()[0]->..., that is only ever look at the first child. You actually want foreach ($child->attributes() ....
if ($a == "endDateTime")
Since you are looking for an attribute with a particular name, you don't actually need to loop over attributes() at all, you can just access it directly as $child['endDateTime']. Note that since we're now using the overloaded [...] operator, we can make use of it to write back to or delete the attribute.
echo $mypix->asXML("t.xml");
SimpleXMLElement::asXML either returns the document as a string or saves to a file, not both. Since in the latter case it returns a boolean, echoing that result isn't likely to be very useful.
You are also calling this function every time around the inner loop, thus saving the same file several times. You only need to do it once, when you've finished making all your modifications.
So, here is how I would write that code:
foreach ( $mypix->Movie as $child )
{
$child['endDateTime'] = null;
// or to remove the attribute completely: unset($child['endDateTime']);
}
$mypix->asXML('t.xml');
Or, for the second example but without XPath (long-winded, but useful if you are changing several things at once, so don't want to "jump" to the deepest descendants in the document). Note the use of ->children($ns_uri) to switch to a different namespace.
// Constants for handier but implementation-independent reference to namespaces
define('XMLNS_EXT', 'URN:NNDS:CMS:ADI3:01');
define('XMLNS_TITLE', 'http://www.cablelabs.com/namespaces/metadata/xsd/title/1');
define('XMLNS_CORE', 'http://www.cablelabs.com/namespaces/metadata/xsd/core/1');
foreach ( $mypix->children() as $child )
{
foreach ( $child->children(XMLNS_CORE)->Ext as $ext )
{
foreach ( $ext->children(XMLNS_EXT)->LocalizableTitleExt as $title )
{
// Delete a child node; note not ->children() as "ext" namespace already selected
unset($title->EpisodeName);
}
}
}
$mypix->asXML("t.xml");

PHP DomElement: See if element is a direct or indirect child of another element

Let's say I have the following XML:
<family>
<father>
<date_of_birth>1971-01-25</date_of_birth>
<first_name>Bob</first_name>
<last_name>Johnson</last_name>
</father>
<mother>
<date_of_birth>1977-09-12</date_of_birth>
<first_name>Mary</first_name>
<last_name>Johnson</last_name>
</mother>
<child>
<date_of_birth>2006-04-21</date_of_birth>
<first_name>Pete</first_name>
<last_name>Johnson</last_name>
</child>
</family>
And I decide I want to use only <date_of_birth> values. I do the following:
$DOBs = $dom->getElementsByTagName("date_of_birth");
Now, my question is, while I'm looping through the values:
foreach ($DOBs as $date)
{
echo $date->nodeValue . "<br>";
}
How can I tell if these values came from a mother, father or child? Is there a way to check and see if the DomElement $date is a child or grandchild of mother or father? Basically I need to know if any of these are within a tag (no matter how deep).
EDIT
For example (please note that the method is_child_of is made up):
foreach ($DOBs as $date)
{
if($date->is_child_of('father'))
{
//This is the father's date of birth.
}
}
I'm looking for something that tells me this <date_of_birth> is inside another element. So if I say:
if($date->is_child_of('family'))
{
}
or
if($date->is_child_of('father'))
{
}
these would both be true (assuming we're dealing with the father's date_of_birth) and the code inside the if statements will fire.
If you're only looking for direct parent, use the parentNode and nodeName attribute of the DOMNode instance.
For indirect parent, other than using XPath, the only thing I know is this function:
function isChildOf(DOMNode $childNode, $parentNodeName)
{
while ($childNode->parentNode)
{
if ($childNode->parentNode->nodeName == $parentNodeName) {
return true;
}
else {
$childNode = $childNode->parentNode;
}
}
return false;
}
Untested, use at your own risk.

How to assign a variable to an address by php

I wana to assign a variable such as 'heloo' to an address such as ->system_settings->settings->hostname and i write a function for.now when i write that address manually this function work correctly and assign 'hello' to that address,but,when i wana to gave address dynamically it doesn't work.
my function :
<?php
write_xml("->system_settings->settings->hostname",'Helloooooooo');
function write_xml($tag_address,$value) {
$xml = simplexml_load_file("test.xml")
or die("Error: Cannot create object");
// $xml->system_settings->settings->hostname = $value;
$xml->$tag_address=$value;
$xml->asXML("test.xml");
}
?>
when i run the command line it works but in dynamical mode it doesn't work and identifies $tag_address in this line $xml->$tag_address=$value; as a string,not as an address.
what should i do?
TNX
The solution is not that easy.
The easiest, but least secure, is to use eval() function so you can write something like this:
eval('$xml'.$tag_address.' = $value;'); // note ' as quotation marks
The most correct way can be that you split your text and create a chained object manually. You can't just refer to all chained elements in one string, but you can do this step-by-step with one element.
For example something like
$splitted_text = explode('->', $tag_address);
$node = $xml;
foreach($splitted_text as $object)
$node = &$node -> {$object};
// at the moment $node = $xml->system_settings->settings->hostname
$node = $value;
$xml->asXML("test.xml");
should work. I haven't tested it, but the idea is that in each iteration you prepare $node variable going deeper into the $xml variable. At the end you modify only the $node, but as objects are only references, so $xml should change accordingly.
This example assumes that there is no -> in the beginning of $tag_address. If it is, there would be a problem with explode() function because this would create empty string as the first element of the $splitted_text array.
So you might need to remove this empty element or apply calling as
write_xml("system_settings->settings->hostname",'Helloooooooo');
without the first ->.
Use XPath to select the node, then update the value. For that, you need the proper systax for your tag address.
write_xml("system_settings/settings/hostname", 'Helloooooooo');
function write_xml($tag_address, $value)
{
$xml = simplexml_load_file('test.xml') or die("Error: Cannot create object");
$nodes = $xml->xpath($tag_address);
$nodes[0][0] = $value;
$xml->asXML('test.xml');
}

How to access first element in iterator?

I'm using a PHP framework that returns SQL results as iteratable objects. Problem is I have a SQL query that that returns one row and I don't want to have to create a foreach-loop to get at the first - and only - element.
So how do I do it?
These don't work:
$obj->item(0)->propName;
$obj->next()->propName;
$obj[0]->propName;
Any ideas?
Assuming by "iterable", you mean that the object implements the Iterator interface, you can use $obj->current() to retrieve the current element, so $obj->current()->propName is probably what you want.
If the iterator pointer has been moved (for example, if it was used in a foreach, which doesn't reset the pointer), then you can call $obj->rewind() to set the pointer back to the first element before you call $obj->current().
There are only two class interfaces that can be traversed: Iterator and IteratorAggregate (any other must implement one of them).
Iterator
First element of Iterator can be obtained as follows:
$iterator->rewind();
if (!$iterator->valid()) {
throw new Exception('There is no any element!');
}
$firstElement = $iterator->current();
If you are sure:
the $iterator was never been traversed by foreach, or if it was but the loop was never stopped with break or return (since PHP 7 this point is irrelevant because foreach does not affect pointer)
there was never called $iterator->next();
you can omit the $iterator->rewind(); from the previous example.
If you are sure the count of elements in $iterator is not zero, you can even omit the condition block testing $iterator->valid().
So if these previous conditions are preserved then what you need is just:
$firstElement = $iterator->current();
IteratorAggregate
IteratorAggergate is actually just an envelope for an Iterator or another IteratorAggregate. Logically that means there is an Iterator at the end.
If you know how many levels deep the Iterator is, just grab it and use as in the very first example:
$iterator = $iteratorAggregate->getIterator();
But if you don't know the deepness, you may use a solution which works also for an Iterator:
$array = iterator_to_array($iteratorAggregate);
// $array is an ordinary array now
if (count($array) === 0) {
throw new Exception('There is no any element!');
}
$firstElement = reset($array);
Unfortunately in case of biig array this is a little overkill because copy of all elements must be created despite we need just one. Besides if the Iterator is an infinite Generator you will run out of memory.
There is one solution that works:
while ($iterator instanceof \IteratorAggregate) {
$iterator = $iterator->getIterator();
}
// $iterator now contains instance of Iterator
I made a simple benchmark for an array of 10000 members and this solution was almost 6 times faster.
So, the universal solution which will work for all cases is:
while ($iterator instanceof \IteratorAggregate) {
$iterator = $iterator->getIterator();
}
$iterator->rewind();
if (!$iterator->valid()) {
throw new Exception('There is no any element!');
}
$firstElement = $iterator->current();
LLAP
For any types of iterables(array, Iterator, IteratorAggregate) you can use this function:
/* public static */ function first(iterable $iterable, $default = null) {
foreach ($iterable as $item){
return $item;
}
return $default;
}
If you prefer to throw an exception if iterable is empty, you can use that version:
/* public static */ function firstOrThrow(iterable $iterable, \Throwable $e) {
foreach ($iterable as $item){
return $item;
}
throw $e;
}
That works both for arrays and iterator objects and also computes only first item in iterators which can improve performance is some cases.
An alternative way of doing this, which I personally find less verbose is to convert the iterator to an array first and then check if the array is empty or not.
$result = iterator_to_array($iterator, true);
if(!empty($result)){
$user = end($result)->id;
}
Pretty useful when you know your iterator will return only one 'iteration'. However, if you are going to have thousands, be cautious that you'll be populating an array with all the data first which might increase your memory usage until the array if empty again.

Access an element's parent with PHP's SimpleXML?

I'm iterating through a set of SimpleXML objects, and I can't figure out how to access each object's parent node. Here's what I want:
$divs = simplexml->xpath("//div");
foreach ($divs as $div)
{
$parent_div = $div->get_parent_node(); // Sadly, there's no such function.
}
Seems like there must be a fairly easy way to do this.
You could run a simple XPath query to get it:
$parent_div = $div->xpath("parent::*");
And as this is Simplexml and it only has element and attribute nodes and a parent node can only be an element and never an attribute, the abbreviated syntax can be used:
$parent_div = $div->xpath("..");
(via: Common Xpath Cheats - SimpleXML Type Cheatsheet (Feb 2013; by hakre) )
$div->get_parent_node(); // Sadly, there's no such function.
Note that you can extend SimpleXML to make it so. For example:
class my_xml extends SimpleXMLElement
{
public function get_parent_node()
{
return current($this->xpath('parent::*'));
}
}
And now all you have to do is modify the code you use to create your SimpleXMLElement in the first place:
$foo = new SimpleXMLElement('<foo/>');
// becomes
$foo = new my_xml('<foo/>');
$foo = simplexml_load_string('<foo/>');
// becomes
$foo = simplexml_load_string('<foo/>', 'my_xml');
$foo = simplexml_load_file('foo.xml');
// becomes
$foo = simplexml_load_file('foo.xml', 'my_xml');
The best part is that SimpleXML will automatically and transparently return my_xml objects for this document, so you don't have to change anything else, which makes your get_parent_node() method chainable:
// returns $grandchild's parent's parent
$grandchild->get_parent_node()->get_parent_node();
If memory serves, an xpath() call returns one or more SimpleXMLElements. If that's the case, then you may be able to use something like:
$div->xpath( '..' );
# or
$div->xpath( 'parent::*' );

Categories