This question already has answers here:
create array tree from array list [duplicate]
(9 answers)
Closed 5 years ago.
I am at a little bit of a loss as to how to approach this, I suspect foreach is not the right answer, and I am aware of the existence of array_walk() and RecursiveArrayIterator, but I have no real-world experience of using either, so I could do with a bit of a pointer in the right direction. (I am working with PHP 7.1.9 if it makes any difference to the answer).
Source data
I have a single-dimension array that contains a parent/child tree of objects. You can assume the tree has unknown and variable nesting depth. A basic example would look like the following :
$sampleParent=array("id"=>101,"level"=>1,"parent_id"=>1,"name"=>"parent","otherparam"=>"bar");
$sampleChildD1=array("id"=>234,"level"=>2,"parent_id"=>101,"name"=>"level1","otherparam"=>"bar");
$sampleChildD2=array("id"=>499,"level"=>3,"parent_id"=>234,"name"=>"level2","otherparam"=>"bar");
$sampleTree=array($sampleParent,$sampleChildD1,$sampleChildD2);
Desired output
The ultimate goal is to output HTML lists (i.e. <ul><li></li></ul>), one list per parent. Nesting of children achieved by nesting <ul> tags. So for my example above :
<ul>
<li>
parent
</li>
<ul>
<li>
level1
<ul>
<li>
level2
</li>
</ul>
</li>
</ul>
</ul>
You can extend RecursiveArrayIterator:
class AdjacencyListIterator extends RecursiveArrayIterator
{
private $adjacencyList;
public function __construct(
array $adjacencyList,
array $array = null,
$flags = 0
) {
$this->adjacencyList = $adjacencyList;
$array = !is_null($array)
? $array
: array_filter($adjacencyList, function ($node) {
return is_null($node['parent_id']);
});
parent::__construct($array, $flags);
}
private $children;
public function hasChildren()
{
$children = array_filter($this->adjacencyList, function ($node) {
return $node['parent_id'] === $this->current()['id'];
});
if (!empty($children)) {
$this->children = $children;
return true;
}
return false;
}
public function getChildren()
{
return new static($this->adjacencyList, $this->children);
}
}
Then you can traverse this iterator with RecursiveIteratorIterator, or you can extend the former to somewhat semi-automatically decorate the tree with HTML:
class UlRecursiveIteratorIterator extends RecursiveIteratorIterator
{
public function beginIteration()
{
echo '<ul>', PHP_EOL;
}
public function endIteration()
{
echo '</ul>', PHP_EOL;
}
public function beginChildren()
{
echo str_repeat("\t", $this->getDepth()), '<ul>', PHP_EOL;
}
public function endChildren()
{
echo str_repeat("\t", $this->getDepth()), '</ul>', PHP_EOL;
echo str_repeat("\t", $this->getDepth()), '</li>', PHP_EOL;
}
}
Having this two classes you can iterate your tree like this:
$iterator = new UlRecursiveIteratorIterator(
new AdjacencyListIterator($sampleTree),
RecursiveIteratorIterator::SELF_FIRST
);
foreach ($iterator as $leaf) {
echo str_repeat("\t", $iterator->getDepth() + 1);
echo '<li>', '', $leaf['name'], '';
echo $iterator->hasChildren() ? '' : '</li>', PHP_EOL;
}
Here is working demo.
Take a notice, that str_repeat and PHP_EOL used here only for presentation purpose and should be removed in real life code.
May I suggest to do this in an OOP manner?
I'd create an object with the properties and a list of children. If you like, you could also add a link from a child to its parent, as in this sample
class TreeNode {
// string
public $name;
// integer
public $id;
// TreeNode
public $parent;
// TreeNode[]
public $children;
}
With this structure, it should be very straight forward to iterate it using foreach.
I have two classes. In my parent class I have this function:
protected function assign($items, $array)
{
foreach($items as $item)
{
$array[$item] = $item;
}
return $array;
}
I use the second array parameter to determine which array to append to.
Then I call it from my child class like this.
$this -> assign($this -> attributes, $this -> values);
That doesn't work but if I hardcode the name of the array in the parent class to $this -> values then it works as it should. I want to know what I am doing wrong and how to do it correctly.
Thanks
Try the below code
protected function assign($items, &$array)
{
foreach($items as $item)
{
$array[$item] = $item;
}
return $array;
}
I have an object that implements Iterator and holds 2 arrays: "entries" and "pages". Whenever I loop through this object, I want to modify the entries array but I get the error An iterator cannot be used with foreach by reference which I see started in PHP 5.2.
My question is, how can I use the Iterator class to change the value of the looped object while using foreach on it?
My code:
//$flavors = instance of this class:
class PaginatedResultSet implements \Iterator {
private $position = 0;
public $entries = array();
public $pages = array();
//...Iterator methods...
}
//looping
//throws error here
foreach ($flavors as &$flavor) {
$flavor = $flavor->stdClassForApi();
}
The reason for this is that sometimes $flavors will not be an instance of my class and instead will just be a simple array. I want to be able to modify this array easily regardless of the type it is.
I just tried creating an iterator which used:
public function ¤t() {
$element = &$this->array[$this->position];
return $element;
}
But that still did not work.
The best I can recommend is that you implement \ArrayAccess, which will allow you to do this:
foreach ($flavors as $key => $flavor) {
$flavors[$key] = $flavor->stdClassForApi();
}
Using generators:
Updating based on Marks comment on generators, the following will allow you to iterate over the results without needing to implement \Iterator or \ArrayAccess.
class PaginatedResultSet {
public $entries = array();
public function &iterate()
{
foreach ($this->entries as &$v) {
yield $v;
}
}
}
$flavors = new PaginatedResultSet(/* args */);
foreach ($flavors->iterate() as &$flavor) {
$flavor = $flavor->stdClassForApi();
}
This is a feature available in PHP 5.5.
Expanding upon Flosculus' solution, if you don't want to reference the key each time you use the iterated variable, you can assign a reference to it to a new variable in the first line of your foreach.
foreach ($flavors as $key => $f) {
$flavor = &$flavors[$key];
$flavor = $flavor->stdClassForApi();
}
This is functionally identical to using the key on the base object, but helps keep code tidy, and variable names short... If you're into that kind of thing.
If you implemented the iterator functions in your calss, I would suggest to add another method to the class "setCurrent()":
//$flavors = instance of this class:
class PaginatedResultSet implements \Iterator {
private $position = 0;
public $entries = array();
public $pages = array();
/* --- Iterator methods block --- */
private $current;
public function setCurrent($value){
$this->current = $value;
}
public function current(){
return $this->current;
}
//...Other Iterator methods...
}
Then you can just use this function inside the foreach loop:
foreach ($flavors as $flavor) {
$newFlavor = makeNewFlavorFromOldOne($flavor)
$flavors -> setCurrent($newFlavor);
}
If you need this function in other classes, you can also define a new iterator and extend the Iterator interface to contain setCurrent()
i have a model extended from CActiveRecord
let says the name of the class is SomeModel and the object is $foo
$foo = SomeModel::model()->findByPk(1);
Then i created virtual attributes on that model
$foo->setImage('testing.jpg');
When i testing call the property/state it works perfectly:
var_dump($foo->image); // output testing.jpg
But when i do iteration with the model it didn't show the property.
foreach($foo as $key => $value) {
echo $key .' = '. $value."\n";
}
How to make the image property listed when i do iteration?
You can't iterate a model like that. Try this instead:
foreach ( $foo->getAttributes() as $key => $value ) {
// Do stuff
}
I am looking to do almost exactly what this function does in Wordpress:
add_filter('wp_nav_menu_objects', function ($items) {
$hasSub = function ($menu_item_id, &$items) {
foreach ($items as $item) {
if ($item->menu_item_parent && $item->menu_item_parent==$menu_item_id) {
return true;
}
}
return false;
};
foreach ($items as &$item) {
if ($hasSub($item->ID, &$items)) {
$item->classes[] = 'menu-parent-item'; // all elements of field "classes" of a menu item get join together and render to class attribute of <li> element in HTML
}
}
return $items;
});
This handy function attaches a class "menu-parent-item" to the li that contains a sub-menu. What I would like to do is add that same class (or any other usable class of course) to the link beforehand instead. In other words I would end up with this:
<li>
Homepage
<ul class="sub-menu">
<li>
And so forth. Any ideas?
You could always use jQuery
$('ul.sub-menu').parent().addClass('menu-parent-item');
EDIT: well, with your edit, you could just use the prev() method?
$('ul.sub-menu').prev().addClass('menu-parent-item');