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.
Related
I've got a function that give the childs of a father sector. So I need to print each one and their childs, and so on.
class sector {
public $id;
public $name;
public $father_sector;
public function bringChilds() : array {
return BD::findChilds();
}
}
The function returns an array of objects or just empty.
I need to print the list of sectors.
I tried using a while, but i do not understand where to stop.
Something like:
print($sector)
foreach($sector->bringChilds() as $child) {
print($child)
foreach($child->bringChilds() as $childer) {
...
}
}
Any help? Thanks and sorry for my english.
You just need recursive calls to a function that takes $sector as an argument.
<?php
function printSectors($sector){
print($sector);
foreach($sector->bringChilds() as $child) {
printSectors($child); // call for child recursively again
}
}
printSectors($sector);
When extending the base Walker class I need to extend the walk() method.
However, calling the parent walk() method yields no results.
These are the approaches I have tried:
public function walk($elements, $max_depth) {
parent::walk($elements, $max_depth);
}
public function walk($elements, $max_depth) {
$parent_class=get_parent_class($this);
$args = array($elements, $max_depth);
call_user_func_array(array($parent_class, 'walk'), $args);
}
It appears to me that as soon as I override the walk() things break.
Should this method return some specific value?
Should I call the parent method differently?
Walker::walk will return the string resulting from the walk operation.
What you will get is a text that has been created using the methods Walker::display_element, Walker::start_lvl, Walker::start_el and so on...
What you will get from the parent method is already HTML code probably hard to modify in the right way in a second time, but if you really want to do that:
public function walk($elements, $max_depth) {
$html = parent::walk($elements, $max_depth);
/* Do something with the HTML output */
return $html;
}
As pointed out in a comment by #TheFallen, the class Walker of Wordpress gives back an output
// Extracted from WordPress\wp-includes\class-wp-walker.php
public function walk( $elements, $max_depth ) {
$args = array_slice(func_get_args(), 2);
$output = '';
//invalid parameter or nothing to walk
if ( $max_depth < -1 || empty( $elements ) ) {
return $output;
}
...
So, if you want to extend the class and overwrite the method, you MUST keep the original behaviour, returning the output too. My suggestion:
class Extended_Walker extends Walker {
public function walk( $elements, $max_depth ) {
$output = parent::walk($elements, $max_depth);
// Your code do things with output here...
return $output;
}
}
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 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');
If I create a new PHP class e.g. to simplify form building (yes I know there are some out there) but I am also trying to learn about classes so pls. be patient - thanks ...
OK I create a new class in the usual way
class newform { class details in here }
add a construct function
public function __construct() { function in here }
I can then call that class again in the usual way
$newform = new newform();
so far so good .... (for me anyhow).
Now I can add some args to the function like so
public function __construct($args) { function in here }
and inside the function "go through" the args - which in my case is an array so written like this
$newform = new newform($args = array('arg1'=>'arg1 val','arg2'=>'arg2 val'));
I can do all that but how do I "add further functions" What I mean here is at the moment I have to declare a new class for every input: i.e.
$newform = new newform($args = array('arg1'=>'arg1 val','arg2'=>'arg2 val'));
$newform->textarea;
$newform = new newform($args = array('arg1'=>'arg1 val','arg2'=>'arg2 val'));
$newform->textinput;
That seems "very" long winded to me and therefore wrong.
How do you do something like this (syntax I know is wromg) where textarea and textinput are created in the class a bit like this (but without the args) $this->textarea = '<textarea></textarea>';
$newform = new newform();
$newform->textarea($args);
$newform->textinput($args);
$newform->textarea($args);
What I mean is what additional function/s do you put into the class to allow you to firstly declare the class ($newform = new newform();) then pass $args to items within the class so you can do "something" like above?
Hope I am explaining myself.
If the arguments in the parameter array are related to individual form elements, move the parameter to a new function instead of the passing it to the class constructor. Like so:
class newform {
public function __construct() { }
public function make_textarea(array $args) {
/* do stuff here */
return $formatted_textarea; // a string like '<textarea></textarea>'
}
public function make_input(array $args) {
/* do stuff here */
return $formatted_input; // a string like '<input />'
}
}
Then in your template:
$form = new newForm;
echo $form->make_textarea(array('arg1' => 'val1', 'arg2' => 'val2'));
echo $form->make_input(array('arg1' => 'val3', 'arg2' => 'val4'));
Note: I'm not doing ($args = array('arg1'=> when calling the method. Assigning the array to a variable is not necessary.
Note: Notice the array type hinting: make_textarea(array $args). That's only there to make sure an array is passed to the method. If anything else is passed to the method--a string for example--a Fatal Error will be thrown.
Update - How to use a private method
class Example {
public function do_something(array $args) {
$result = $this->private_method($args);
return $result;
}
private function private_method(array $args) {
/* do stuff here */
return $formatted_args;
}
}
It isn't long-winded to declare functions for each type of tag you want to generate. There are a finite number of tags, and rather than relying on dynamically intercepting function calls via __call you're better off simply defining the methods.
Move most of the internal implementation for each type of tag can be moved to a private method for generating generic HTML tags. Not all form elements share any internal implementation though; tags like <input type="password" /> and <input type="text" /> are obvious candidates for a shared implementation, while <select> elements will require special handling.
The following should give you an idea. When you build your own, don't forget to escape htmlspecialchars where appropriate:
class Form_helper {
// pass boolean false for $contents to build a self-closing "<input />"-style tag
private function html_tag($name, $contents, array $attributes = array() {
$tag = "<$name";
foreach ($attributes as $key => $value) {
$tag .= " $key=\"$value\"";
}
if ($contents === false) {
// self-closing
$tag .= " />";
} else {
$tag .= ">$contents</$name>";
}
return $tag;
}
public function textarea($contents, array $attributes = array()) {
return $this->html_tag('textarea', $contents, $attributes);
}
public function input(array $attributes = array()) {
return $this->html_tag('input', false, $attributes);
}
public function select(array $options) {
// options contains "value"=>"contents" mappings, for production
// option tags in the form <option value="value">contents</option>
$option_tags = '';
foreach ($options as $value => $content) {
$option_tags .= $this->html_tag('option', $content, array('value' => $value));
}
return $this->html_tag('select', $option_tags);
}
}
First of all, you don't have to do this:
$newform = new newform($args = array('arg1'=>'arg1 val','arg2'=>'arg2 val'));
This will suffice:
$newform = new newform(array('arg1'=>'arg1 val','arg2'=>'arg2 val'));
That is, if you want to pass an array as the first argument. Normally, you would do something like this instead:
class newform {
public function __construct($arg1, $arg2) {
// method body here
}
}
$form = new newform('arg1 val', 'arg2 val');
Now, you must keep in mind that a constructor (__construct) is just like another method. So you can do this:
class newform {
public function __construct() {
// method body here
}
public function textarea($name) {
echo '<textarea name="'.$name.'"></textarea>';
}
public funciton textinput($name) {
echo '<input type="text" name="'.$name.'"/>';
}
}
$form = new newform;
$form->textarea('foo');
$form->textinput('bar');
Outputs:
<textarea name="foo"></textarea>
<input type="text" name="bar"/>
I'm not sure what you mean, but my guts tell me that what you need are so-called magic methods:
magic methods / overloading / manual
Best
Raffael