I would like to use the RecursiveIteratorIterator class to build a menu.
Actually I'm using Zend_Navigation container to build main menu and breadcrumbs, however it doesn't suit my other needs, so I'm using container with iterator:
$navigation = $this->view->navigation()->getContainer();
$iterator = new RecursiveIteratorIterator( $navigation,
RecursiveIteratorIterator::SELF_FIRST);
Then I like to know, two things - current level and if item has more children.
First one can I achieve by getDepth() method and for second one I tried to use
callHasChildren(), but this one is always returning TRUE, even if item has not more children.
foreach ($iterator as $page) {
echo $iterator->getDepth() . $iterator->callHasChildren()) . '<br />';
}
Any idea how to use this method?
Related
How do i get a b c d e from the below xml markup...
<api:field name="test">
<api:text>a</api:text>
<api:text>b</api:text>
<api:text>c</api:text>
<api:text>d</api:text>
<api:text>e</api:text>
</api:field>
I am trying to use this for loop:
foreach ($xml->xpath('//api:field[#name="test"]') as $item)
{
foreach ($item->children() as $child) {
...
}
}
but i do not know how to access the child nodes which contain no attributes.
I need to get the child values for the parent node "test" specifically so please don't give me $xml->xpath("//api:text"); as an answer. The problem with this answer is that we might see under other parent nodes and i only want to get the child values from a specific parent node. In this case name="test".
There are a couple of ways you can achieve this. Either just extend your xpath expression to return the child nodes themselves:
foreach ($sxml->xpath('/api:field[#name="test"]/api:text') as $item) {
echo (string) $item, PHP_EOL;
}
Alternatively if you did want to use two loops (or it just better fits your use case), you just need to pass the namespace prefix into the children() method:
foreach ($sxml->xpath('/api:field[#name="test"]') as $item) {
foreach ($item->children('api', true) as $child) {
echo (string) $child, PHP_EOL;
}
}
(If the namespace prefix is likely to change, you can use the registerXPathNamespace method to register a persistent one, according to the defined namespace URL.)
Both of these approaches will yield the same result:
a
b
c
d
e
See https://eval.in/954569 for a full example
I'm trying to delete certain nodes from an XML-file using PHP, for some reason it was not working properly and I came to the conclusion that that's probably the case, because PHP is not returning the correct number of nodes, that are in said XML-file.
This is the XML-File:
<?xml version="1.0"?>
<Filenames>
<newFile>
<link>somelink</link>
<ViewLink>someotherlink</ViewLink>
<FileName>test</FileName>
</newFile>
<newFile>
<link>somelink</link>
<ViewLink>someotherlink</ViewLink>
<FileName>test</FileName>
</newFile>
</Filenames>
...and so on (the file is way longer, but I'm always using the same name for nodes etc., you get the point)
Since deleting certain nodes did not work out, I tried just getting the information from the xml-file first and I tried to check how many <newFile> (those are the nodes I want to delete) nodes PHP could find.
In order to do that, I did the following:
<?php
$doc = new DOMDocument();
$doc->load("FilenameList.xml");
$root = $doc->documentElement;
$newFiles = $root->getElementsByTagName("newFile");
echo count($newFiles);
?>
However, that returned "1", even though, obviously, there are way more nodes.
I also tried counting the amount of <newFile>nodes using simpleXML, but again, it did not work. I'm very confused by this, as I used the same syntax in the past and it worked out perfectly.
Another thing I tried was to do this:
$newFiles = $doc->getElementsByTagName("newFile");
Instead of $root = ...
In addition, I also tried using sizeof() instead of count().
Am I missing something or does anyone have an idea about what's going wrong?
The DOMDocument::getElementsByTagName function returns a new instance of class DOMNodeList. Such class had the following signature when the question was made:
DOMNodeList implements Traversable {
/* Properties */
readonly public int $length ;
/* Methods */
DOMNode DOMNodelist::item ( int $index )
}
The class implemented the Traversable interface (so you can foreach () it) but not the Countable interface, which would allow count() to produce something useful.
The solution is the $length property you can see in the class signature:
length
The number of nodes in the list. The range of valid child node indices
is 0 to length - 1 inclusive.
As of PHP/7.2.0 the Countable interface was finally implemented:
DOM:
Implemented FR #74837 (Implement Countable for DomNodeList and
DOMNamedNodeMap).
Current version of the documentation is this:
DOMNodeList implements Traversable , Countable {
/* Properties */
readonly public int $length ;
/* Methods */
public int count ( void )
DOMNode item ( int $index )
}
… and your original code should finally work.
$doc = new DOMDocument;
$doc->loadXML("FilenameList.xml");
$newFiles = $doc->getElementsByTagName('newFile');
foreach ($newFiles as $newFile) {
$newFile++;
}
echo $newFile;
you forgot the foreach loop. Check this http://php.net/manual/en/domdocument.getelementsbytagname.php
I have a class called Collection which stores objects of same type.
Collection implements array interfaces: Iterator, ArrayAccess, SeekableIterator, and Countable.
I'd like to pass a Collection object as the array argument to the array_map function. But this fails with the error
PHP Warning: array_map(): Argument #2 should be an array
Can I achieve this by implementing other/more interfaces, so that Collection objects are seen as arrays?
The array_map() function doesn't support a Traversable as its array argument, so you would have to perform a conversion step:
array_map($fn, iterator_to_array($myCollection));
Besides iterating over the collection twice, it also yield an array that will not be used afterwards.
Another way is to write your own map function:
function map(callable $fn)
{
$result = array();
foreach ($this as $item) {
$result[] = $fn($item);
}
return $result;
}
Update
Judging by your use-case it seems that you're not even interested in the result of the map operation; therefore it makes more sense to use iterator_apply().
iterator_apply($myCollection, function($obj) {
$obj->method1();
$obj->method2();
return true;
});
array_map wants, as the name suggests, arrays. It's not called iterator_map after all. ;)
Apart from iterator_to_array(), which produces a potentially large temporary array, there's no trick to make iterable objects work with array_map.
The Functional PHP library has a map implementation which works on any iterable collection.
If you're not interested in creating a new array that is a function mapped over the original array, you could just use a foreach loop (because you implement Iterator).
foreach($item in $myCollection) {
$item->method1();
$item->method2();
}
if you actually want to use map, then I think you'll have to implement your own. I would suggest making it a method on Collection, eg:
$mutatedCollection = $myCollection->map(function($item) {
/* do some stuff to $item */
return $item;
});
I would ask yourself if you really want to use map or do you really just mean foreach
I came up with the following solution:
//lets say you have this iterator
$iterator = new ArrayIterator(array(1, 2, 3));
//and want to append the callback output to the following variable
$out = [];
//use iterator to apply the callback to every element of the iterator
iterator_apply(
$iterator,
function($iterator, &$out) {
$current = $iterator->current();
$out[] = $current*2;
return true;
},
array($iterator, &$out) //arguments for the callback
);
print_r($out);
This way, you can generate an array without iterating twice as you would to with the approach like:
$iterator = new ArrayIterator(array(1,2,3));
$array = iterator_to_array($iterator); //first iteration
$output = array_map(function() {}, $array); //second iteration
Good luck!
I just stumbled upon this question and I managed to cast the collection to an array to make it work:
array_map($cb, (array) $collection);
disclaimer For the original question this might not be a suitable option but I found the question while looking to solve a problem which I solved with this solution. I would recommend using a custom iterator map where possible/viable.
another option is to do something like this:
foreach($collection as &$item) {
$item = $cb($item);
}
which will mutate the underlying collection.
EDIT:
It has been pointed out that casting to an array can have unwanted side effects. It would be better to add a method to your collection to return the array from the iterator, and traverse that, or otherwise add a map method which accepts a callback and run a loop on the underlying iterator.
I currently have a code snippet where for each category, it would find the sub-categories:
$categories = array_map(
function($child)
{
$child['children'] =
$this->getChildren(
$child['id'],
!empty($this->request->get['language_id']) ?
$this->request->get['language_id'] : 1
);
return $child;
}, $categories);
getChildren() would recursively get the children of one category:
private function getChildren($parent_id, $language_id) {
$this->load->model('official/category');
$children =
$this->model_official_category->getCategoriesByParentId(
$parent_id,
$language_id
);
// For each child, find the children.
foreach ($children as $child) {
$child['children'] = $this->getChildren(
$child['id'],
$language_id
);
}
return $children;
}
Currently, using my lambda function within the array_map(), only the sub-category's children would be retrieve, so if each sub-category has its own sub-sub-category, it would not be saved into its children.
How could I show the sub-sub-category given the sub-category we have?
What I wanted to do with my code was to take a parent, get its children, and then treat each of those children as a parent and get its children recursively, however my JSON output does not reflect that. Only the parent has children - the children has no children (despite my database having them).
The problem is that your recursion foreach loop assigns the children that it retrieves to a copy of the child data, rather than the child data itself.
To resolve this you could use foreach loop that references the child data, like so:
foreach ($children as &$child) {
However, due to a number of reasons related to how foreach is implemented internally in PHP (more info if you're interested), it would be considerably more memory efficient to use a for loop instead, as this will avoid quite a few copy-on-write copies of the child data:
for ($i = 0; isset($children[$i]); $i++) {
$children[$i]['children'] = $this->getChildren(
$children[$i]['id'],
$language_id
);
}
This is one place where using objects instead of arrays to represent the child data might be a good idea, because objects are always passed by reference (kind of) and the behaviour would be more like what you were expecting initially.
I've got this notice:
ArrayIterator::next(): Array was modified outside object and internal position is no longer valid in /var/www...
which is produced by this code, at the begining of the foreach loop. Together with the notice, the foreach loop starts iterating all over again. In other words, the internal position is reset whenever this thing happens. But acording to php manual, ArrayObject is using ArrayIterator by default.
And manual says this about ArrayIterator
This iterator allows to unset and modify values and keys while iterating over Arrays and Objects.
Am I missing something here? I found some bugreports about ArratIterator, but not this kind. Is it a bug or is it my bad?
version: PHP Version 5.3.10-1ubuntu3.4
<?php
//file 1:
// no namespace
abstract class holder extends \ArrayObject{
// abstract function init();
public function __construct($init){
parent::__construct($init, 1);
}
}?>
<?php
//file 2:
namespace troops;
class holder extends \holder{
public function __construct(){
parent::__construct($this->init());
}
private function init(){
return array( /*... some data from db ...*/ );
}
public function saveData(){
foreach($this as $k => $v){
$this->save($v);
if($v->number_of_items==0) {
unset($k);
// $this->offsetUnset($k); // tryed both
}
}
}
}
?>
ArrayObject implements IteratorAggregate which means it has a method called getIterator() which returns an iterator.
php's foreach loop will automatically retrieve an iterator by calling the getIterator() method for you to retrieve an iterator to iterate over. This is convenient, but you need to get a reference to this iterator in order to call the offsetUnset() method on the iterator itself. The key thing here is you must call the iterators offsetUnset() method, not the ArrayObjects offsetUnset() method.
$ao = new ArrayObject();
$ao[] = 9;
$iter = $ao->getIterator();
foreach ($iter as $k => $v)
$iter->offsetUnset($k); // no error
The underlying ArrayObject that the iterator is iterating over will be mutated, so if you have more than one active iterator at the same time over the same Arrayobject, you'll still encounter the same error.
The rationale for this is likely so that the iterators can be memory efficient and not have to copy the underlying ArrayObject, because a copy is the only simple solution to dealing with the complexity of deciding what the current iterator position should be when things are added to or deleted from the underlying array.
In case you remove the last record from array than the next foreach loop can fail (internaly calling $this->next() )
Forexample when i have array of one item and I will unset it, than next foreach fails.
So it helped me to test the validy and break in this case the next loop.
deleteOffset = true;
foreach ($iterator as $key => $value)
{
if ($deleteOffset && $iterator->offsetExists($key) )
{
$iterator->offsetUnset($key);
}
//if remove last record than the foreach ( $this->next() ) fails so
//we have to break in this case the next ->next call
//after any ->offsetUnset calls
if (!$iterator->valid()) break;
}
This took me so long! I used the recipe on this page to (unsuccessfully) clear-out the contents of an ArrayObject's storage, then I found this little diddy..
$this->exchangeArray(array());
magic!