Getting nested set model into a <ul> but hiding "closed" subtrees - php

Based on Getting a modified preorder tree traversal model (nested set) into a <ul>
One of answers gave right code to display full tree. What i need is to always show first level (depth=0) and siblings+childrens for active list item. Goal is to expand visible part of tree when user selects list item which is parent for more list items.
So, if i got this list:
1. item
2. item
2.1. item
2.2. item
2.2.1. item
2.2.2. item
2.2.3. item
2.3. item
2.4. item
2.4.1. item
2.4.2. item
3. item
4. item
4.1. item
4.2. item
4.2.1. item
4.2.2. item
5. item
and if current list item is "2.", list should look like that:
1. item
2. item // this needs class .selected
2.1. item
2.2. item
2.3. item
2.4. item
3. item
4. item
5. item
and if current list item is "2.2.", list should look like that:
1. item
2. item // this needs class .selected
2.1. item
2.2. item // this needs class .selected
2.2.1. item
2.2.2. item
2.2.3. item
2.3. item
2.4. item
3. item
4. item
5. item
Below there is an example code which works well for me to display full tree. I also added lft/rgt/current which will be needed to solve my issue.
<?php
function MyRenderTree ( $tree = array(array('name'=>'','depth'=>'', 'lft'=>'','rgt'=>'')) , $current=false){
$current_depth = 0;
$counter = 0;
$result = '<ul>';
foreach($tree as $node){
$node_depth = $node['depth'];
$node_name = $node['name'];
$node_id = $node['category_id'];
if($node_depth == $current_depth){
if($counter > 0) $result .= '</li>';
}
elseif($node_depth > $current_depth){
$result .= '<ul>';
$current_depth = $current_depth + ($node_depth - $current_depth);
}
elseif($node_depth < $current_depth){
$result .= str_repeat('</li></ul>',$current_depth - $node_depth).'</li>';
$current_depth = $current_depth - ($current_depth - $node_depth);
}
$result .= '<li id="c'.$node_id.'"';
$result .= $node_depth < 2 ?' class="open"':'';
$result .= '>'.$node_name.'';
++$counter;
}
$result .= str_repeat('</li></ul>',$node_depth).'</li>';
$result .= '</ul>';
return $result;
}
// "$current" may contain category_id, lft, rgt for active list item
print MyRenderTree($categories,$current);
?>

As you already managed to sort the sequence, why not just output as needed?
As some leafs need to appear closed, so the iterator should be able to skip children of non-selected nodes.
Doing so lead me to an idea to solve the problem of terminating the output tree (output = parsing). What to do if the last valid node in the sequence is at a higher depth than 0? I appended a NULL terminator for that. So still open levels can be closed before the loop finishes.
Additionally the iterator overloads nodes to offer common methods on them, like comparing against the currently selected element.
The MyRenderTree function (Demo/Full code)
Edit: The Demo Codepad has problems, here is the source-code: Gist
Getting nested set model into a but hiding “closed” subtrees
function MyRenderTree($tree = array(array('name'=>'','depth'=>'', 'lft'=>'','rgt'=>'')) , $current=false)
{
$sequence = new SequenceTreeIterator($tree);
echo '<ul>';
$hasChildren = FALSE;
foreach($sequence as $node)
{
if ($close = $sequence->getCloseLevels())
{
echo str_repeat('</ul></li>', $close);
$hasChildren = FALSE;
}
if (!$node && $hasChildren)
{
echo '</li>', "\n";
}
if (!$node) break; # terminator
$hasChildren = $node->hasChildren();
$isSelected = $node->isSupersetOf($current);
$classes = array();
$isSelected && ($classes[] = 'selected') && $hasChildren && $classes[] = 'open';
$node->isSame($current) && $classes[] = 'current';
printf('<li class="%s">%s', implode(' ', $classes), $node['name']);
if ($hasChildren)
if ($isSelected)
echo '<ul>';
else
$sequence->skipChildren()
;
else
echo '</li>'
;
}
echo '</ul>';
}
This can be solved as well in a single foreach and some variables, however I think for re-useablilty, the implementation based on the SPL Iterators is better.

Instead of using PHP script for handling tree navigation, Jquery can be used.
Once the tree is generated rest of the things will handled on client itself, it will also save server requests.
See Sample 2 and 3
http://jquery.bassistance.de/treeview/demo/
http://docs.jquery.com/Plugins/Treeview
It may help as per your requirement.

The function expect the $tree is order by the 'left'.
I have modified your function to selected items based on the 'left' and 'right' value. Hope it's what you are after.
The modified function:
function MyRenderTree($tree = array(array('name' => '', 'depth' => '', 'lft' => '', 'rgt' => '')), $current=false)
{
$current_depth = 0;
$counter = 0;
$found = false;
$nextSibling = false;
$result = '<ul>';
foreach ($tree as $node) {
$node_depth = $node['depth'];
$node_name = $node['name'];
$node_id = 1;//$node['category_id'];
if ($current !== false) {
if ($node_depth ==0) {
if ($node['lft'] <= $current['lft'] && $node['rgt'] >= $current['rgt']) {
// selected root item
$root = $node;
}
} else if (!isset($root)) {
// skip all items that are not under the selected root
continue;
} else {
// when selected root is found
$isInRange = ($root['lft'] <= $node['lft'] && $root['rgt'] >= $node['rgt']);
if (!$isInRange) {
// skip all of the items that are not in range of the selected root
continue;
} else if (isset($current['lft']) && $node['lft'] == $current['lft']) {
// selected item reached
$found = true;
$current = $node;
} else if ($nextSibling !== false && $nextSibling['depth'] < $node['depth']) {
// if we have siblings after the selected item
// skip any other childerns in the same range or the selected root item
continue;
} else if ($found && $node_depth == $node['depth']) {
// siblings after the selected item
$nextSibling = $node;
}
}
} else if ($node_depth > 0) {
// show root items only if no childern is selected
continue;
}
if ($node_depth == $current_depth) {
if ($counter > 0)
$result .= '</li>';
}
elseif ($node_depth > $current_depth) {
$result .= '<ul>';
$current_depth = $current_depth + ($node_depth - $current_depth);
} elseif ($node_depth < $current_depth) {
$result .= str_repeat('</li></ul>', $current_depth - $node_depth) . '</li>';
$current_depth = $current_depth - ($current_depth - $node_depth);
}
$result .= '<li id="c' . $node_id . '" ';
$result .= $node_depth < 2 ?' class="open"':'';
$result .= '>' . $node_name .'(' . $node['lft'] . '-' . $node['rgt'] . ')' . '';
++$counter;
}
unset($found);
unset($nextSibling);
$result .= str_repeat('</li></ul>', $node_depth) . '</li>';
$result .= '</ul>';
return $result;
}
Usage:
$categories = array(
array('name' => '1. item',
'depth' => '0',
'lft' => '1',
'rgt' => '2'),
array('name' => '2. item',
'depth' => '0',
'lft' => '3',
'rgt' => '22'),
array('name' => '2.1 item',
'depth' => '1',
'lft' => '4',
'rgt' => '5'),
array('name' => '2.2 item',
'depth' => '1',
'lft' => '6',
'rgt' => '13'),
array('name' => '2.2.1 item',
'depth' => '2',
'lft' => '7',
'rgt' => '8'),
array('name' => '2.2.2 item',
'depth' => '2',
'lft' => '9',
'rgt' => '10'),
array('name' => '2.2.3 item',
'depth' => '2',
'lft' => '11',
'rgt' => '12'),
array('name' => '2.3 item',
'depth' => '1',
'lft' => '14',
'rgt' => '15'),
array('name' => '2.4 item',
'depth' => '1',
'lft' => '16',
'rgt' => '21'),
array('name' => '2.4.1 item',
'depth' => '2',
'lft' => '17',
'rgt' => '18'),
array('name' => '2.4.2 item',
'depth' => '2',
'lft' => '19',
'rgt' => '20'),
array('name' => '3. item',
'depth' => '0',
'lft' => '23',
'rgt' => '24'),
array('name' => '4. item',
'depth' => '0',
'lft' => '25',
'rgt' => '34'),
array('name' => '4.1 item',
'depth' => '1',
'lft' => '26',
'rgt' => '27'),
array('name' => '4.2 item',
'depth' => '1',
'lft' => '28',
'rgt' => '33'),
array('name' => '4.2.1 item',
'depth' => '2',
'lft' => '29',
'rgt' => '30'),
array('name' => '4.2.2 item',
'depth' => '2',
'lft' => '31',
'rgt' => '32',
'category_id' => 5),
array('name' => '5. item',
'depth' => '0',
'lft' => '35',
'rgt' => '36'),
);
$current = array('lft' => '9', 'rgt' => '10');
print MyRenderTree($categories, $current);

http://www.jstree.com/ is a jQuery plugin which will handle this for you far more elegantly and quickly than trying to do a PHP based solution.
Check out http://www.jstree.com/demo for a live demo and instruction on how tom implement.

Based on answer by satrun77. I created a helper for symfony + doctrine + nestedset (http://www.doctrine-project.org/projects/orm/1.2/docs/manual/hierarchical-data/en):
function render_tree_html_list($nodes, Doctrine_Record $current_node, $render = true) {
$html = '';
$current_node_level = $current_node->getLevel();
$counter = 0;
$found = false;
$nextSibling = false;
foreach ($nodes as $i => $node):
$node_level = $node->getLevel();
$node_name = $node->getTitulo();
$node_id = $node->getId();
if ($current_node !== false) {
if ($node_level == 0) {
if ($node->getLft() <= $current_node->getLft() && $node->getRgt() >= $current_node->getRgt()) {
// selected root item
$root = $node;
}
} else if (!isset($root)) {
// skip all items that are not under the selected root
continue;
} else {
// when selected root is found
$isInRange = ($root->getLft() <= $node->getLft() && $root->getRgt() >= $node->getRgt());
if (!$isInRange) {
// skip all of the items that are not in range of the selected root
continue;
} else if ($current_node->getLft() && $node->getLft() == $current_node->getLft()) {
// selected item reached
$found = true;
$current_node = $node;
} else if ($nextSibling !== false && $nextSibling->getLevel() < $node->getLevel()) {
// if we have siblings after the selected item
// skip any other childerns in the same range or the selected root item
continue;
} else if ($found && $node_level == $node->getLevel()) {
// siblings after the selected item
$nextSibling = $node;
}
}
} else if ($node_level > 0) {
// show root items only if no childern is selected
continue;
}
if ($node_level == $current_node_level) {
if ($counter > 0)
$html .= '</li>';
}
elseif ($node_level > $current_node_level) {
$html .= '<ol>';
$current_node_level = $current_node_level + ($node_level - $current_node_level);
} elseif ($node_level < $current_node_level) {
$html .= str_repeat('</li></ol>', $current_node_level - $node_level) . '</li>';
$current_node_level = $current_node_level - ($current_node_level - $node_level);
}
$html .= sprintf('<li node="%d" class="%s"><div>%s</div>',
$node_id,
(isset($nodes[$i + 1]) && $nodes[$i + 1]->getLevel() > $node_level) ? "node" : "leaf",
$node->getLevel() > 0 ? link_to($node->getTitulo(), 'cms_categoria_edit', $node) : $node->getTitulo()
);
++$counter;
endforeach;
$html .= str_repeat('</li></ol>', $node_level) . '</li>';
$html = '<ol class="sortable">'. $html .'</ol>';
return $render ? print($html) : $html;
}
Extra tags: tree, node

This method checks to see if the node is a parent of the selected node, the selected node, or depth=0. Only iterations for nodes which meet one of these conditions add list items to the result string. All of the nodes get either the selected class, open class or both. Otherwise, it is your code.
$current_depth = 0;
$counter = 0;
$result = '<ul>';
foreach($tree as $node){
$node_depth = $node['depth'];
$node_name = $node['name'];
$node_id = $node['category_id'];
$selected = false;
if( $node['lft'] <= current['lft'] && $node['rgt'] >= $current['rgt'] ) $selected=true
if ($node_depth == 0 || $selected == true)
{
if($node_depth == $current_depth)
{
if($counter > 0) $result .= '</li>';
}
elseif($node_depth > $current_depth)
{
$result .= '<ul>';
$current_depth = $current_depth + ($node_depth - $current_depth);
}
elseif($node_depth < $current_depth)
{
$result .= str_repeat('</li></ul>',$current_depth - $node_depth).'</li>';
$current_depth = $current_depth - ($current_depth - $node_depth);
}
$result .= '<li id="c'.$node_id.'"';
$result .= ' class="';
$result .= $node_depth < 2 ?' open':' ';
$result .= $select == true ?' selected':' ';
$result .= '"';
$result .= '>'.$node_name.'';
++$counter;
}
}
$result .= str_repeat('</li></ul>',$node_depth).'</li>';
$result .= '</ul>';
return $result;
}
// "$current" may contain category_id, lft, rgt for active list item
print MyRenderTree($categories,$current);
?>

Just wanted to provide a OOP, cleaner version, which should make it easier to add any sort of logic apart from the selected one.
It works properly with the array structure posted by #satrun77.
class Node
{
var $name;
var $category;
var $depth;
var $lft;
var $rgt;
var $selected;
var $nodes = array();
public function __construct( $name, $category, $depth, $lft, $rgt, $selected = false )
{
$this->name = $name;
$this->category = $category;
$this->depth = $depth;
$this->lft = $lft;
$this->rgt = $rgt;
$this->selected = $selected;
}
public function addNode( Node $node )
{
array_push( $this->nodes, $node );
}
public function render()
{
$renderedNodes = '';
if ( $this->isSelected() ) {
$renderedNodes = $this->renderNodes();
}
return sprintf( '<li id="c%s">%s%s</li>', $this->category, $this->name, $renderedNodes );
}
protected function renderNodes()
{
$renderedNodes = '';
foreach ( $this->nodes as $node )
{
$renderedNodes .= $node->render();
}
return sprintf( '<ul>%s</ul>', $renderedNodes );
}
/** Return TRUE if this node or any subnode is selected */
protected function isSelected()
{
return ( $this->selected || $this->hasSelectedNode() );
}
/** Return TRUE if a subnode is selected */
protected function hasSelectedNode()
{
foreach ( $this->nodes as $node )
{
if ( $node->isSelected() )
{
return TRUE;
}
}
return FALSE;
}
}
class RootNode extends Node
{
public function __construct() {}
public function render()
{
return $this->renderNodes();
}
}
function MyRenderTree( $tree, $current )
{
/** Convert the $tree array to a real tree structure based on the Node class */
$nodeStack = array();
$rootNode = new RootNode();
$nodeStack[-1] = $rootNode;
foreach ( $tree as $category => $rawNode )
{
$node = new Node( $rawNode['name'], $category, $rawNode['depth'], $rawNode['lft'], $rawNode['rgt'], $rawNode['lft'] == $current['lft'] );
$nodeStack[($node->depth -1)]->addNode( $node );
$nodeStack[$node->depth] = $node;
end( $nodeStack );
}
/** Render the tree and return the output */
return $rootNode->render();
}

isn't it the best solution. why there are so many classes, objects bla bla.. ?
this simple function is perfect and flexible in everyways.
DEMO
$categories = array(
array('id'=>1,'name'=>'test1','parent'=>0),
array('id'=>2,'name'=>'test2','parent'=>0),
array('id'=>3,'name'=>'test3','parent'=>1),
array('id'=>4,'name'=>'test4','parent'=>2),
array('id'=>5,'name'=>'test5','parent'=>1),
array('id'=>6,'name'=>'test6','parent'=>4),
array('id'=>7,'name'=>'test7','parent'=>6),
array('id'=>8,'name'=>'test7','parent'=>3)
);
$cats = array();
foreach($categories as &$category)
$cats[$category['parent']][] = $category;
unset($categories);
$selected = 6; // selected id;
echo standartCategory($cats,$selected);
function standartCategory(&$categories,$selected = '',$parent = 0 /*MAIN CATEGORY*/)
{
if (!isset($categories[$parent])) return array('',0);
$html = '';
$haveSelected = 0;
foreach($categories[$parent] as $category) {
list($childHtml,$isVisible) = standartCategory($categories,$selected,$category["id"]);
$isSelected = $category['id']===$selected;
if (! ($isVisible | $isSelected)) { // this if to prevent output
$html .= '<li>'.$category['name'].'</li>';
continue;
}
$haveSelected |= $isVisible | $isSelected;
$html .= '<li>'.$category['name'].$childHtml.'</li>';
}
return $parent ? array('<ul>'.$html.'</ul>',$haveSelected) : '<ul>'.$html.'</ul>';
}

Related

Parsing returns an empty value

I make a parser of items from DotA 2 user inventory in the Steam service. Every time I try to parse user data, I get an empty value:
{"success":true,"items":[]}, but there are items in my Steam inventory.
My function to parse items:
public function loadMyInventory() {
if(Auth::guest()) return ['success' => false];
$prices = json_decode(Storage::get('prices.txt'), true);
$response = json_decode(file_get_contents('https://steamcommunity.com/inventory/'.$this->user->steamid64.'/570/2?l=russian&count=5000'), true);
if(time() < (Session::get('InvUPD') + 5)) {
return [
'success' => false,
'msg' => 'Error, repeat in '.(Session::get('InvUPD') - time() + 5).' сек.',
'status' => 'error'
];
}
//return $response;
$inventory = [];
foreach($response['assets'] as $item) {
$find = 0;
foreach($response['descriptions'] as $descriptions) {
if($find == 0) {
if(($descriptions['classid'] == $item['classid']) && ($descriptions['instanceid'] == $item['instanceid'])) {
$find++;
# If we find the price of an item, then move on.
if(isset($prices[$descriptions['market_hash_name']])) {
# Search data
$price = $prices[$descriptions['market_hash_name']]*$this->config->curs;
$class = false;
$text = false;
if($price <= $this->config->min_dep_sum) {
$price = 0;
$text = 'Cheap';
$class = 'minPrice';
}
if(($descriptions['tradable'] == 0) || ($descriptions['marketable'] == 0)) {
$price = 0;
$class = 'minPrice';
$text = 'Not tradable';
}
# Adding to Array
$inventory[] = [
'name' => $descriptions['market_name'],
'price' => floor($price),
'color' => $this->getRarity($descriptions['tags']),
'tradable' => $descriptions['tradable'],
'class' => $class,
'text' => $text,
'classid' => $item['classid'],
'assetid' => $item['assetid'],
'instanceid' => $item['instanceid']
];
}
}
}
}
}
Session::put('InvUPD', (time() + 5));
return [
'success' => true,
'items' => $inventory
];
}
But should return approximately the following value:
{"success":true,"items":[{"classid":"2274725521","instanceid":"57949762","assetid":"18235196074","market_hash_name":"Full-Bore Bonanza","price":26}]}
Where my mistake?
First of all, you are iterating on descriptions for every assets, which is assets*descriptions iteration, it's quite a lot, but you can optimize this.
let's loop once for descriptions and assign classid and instanceid as object key.
$assets = $response["assets"];
$descriptions = $response["descriptions"];
$newDescriptions=[];
foreach($descriptions as $d){
$newDescriptions[$d["classid"]][$d["instanceid"]] = $d;
}
this will give as the ability to not loop over description each time, we can access the description of certain asset directly $newDescriptions[$classid][$instanceid]]
foreach($assets as $a){
if(isset($newDescriptions[$a["classid"]]) && isset($newDescriptions[$a["classid"]][$a["instanceid"]])){
$assetDescription = $newDescriptions[$a["classid"]][$a["instanceid"]];
$inventory = [];
if(isset($prices[$assetDescription["market_hash_name"]])){
$price = $prices[$assetDescription['market_hash_name']]["price"]*$this->config->curs;
$class = false;
$text = false;
if($price <= $this->config->min_dep_sum) {
$price = 0;
$text = 'Cheap';
$class = 'minPrice';
}
if(($assetDescription['tradable'] == 0) || ($assetDescription['marketable'] == 0)) {
$price = 0;
$class = 'minPrice';
$text = 'Not tradable';
}
$inventory["priceFound"][] = [
'name' => $assetDescription['market_name'],
'price' => floor($price),
'color' => $this->getRarity($assetDescription['tags']),
'tradable' => $assetDescription['tradable'],
'class' => $class,
'text' => $text,
'classid' => $a['classid'],
'assetid' => $a['assetid'],
'instanceid' => $a['instanceid']
];
}else{
$inventory["priceNotFound"][] = $assetDescription["market_hash_name"];
}
}
}
About your mistake:
are you Sure your "prices.txt" contains market_hash_name?
I don't see any other issue yet, operationg on the data you have provided in comment, I got print of variable $assetDescription. Please doublecheck variable $prices.

bootstrap nested navbar with php recursive iteration

I try to render my nested Categories in a Bootstrap navbar but I have a problem to get the desired output. UL dropwdown is getting wrapped more times as it should because of my recursive iteration. I do not find a good workaround to have the desired result
function getFeedCategories($db) {
$sql = "SELECT * FROM feeds_categories";
$datas = array();
$childrenTree = [];
$categoryNames = [];
//We fill $childrenTree and $categoryNames from database
if ($db->sql($sql)) {
while ($res = $db->getResult('array')) {
$datas[] = $res;
}
}
foreach ($datas as $row) {
extract($row);
$categoryNames[(string) $id] = $name;
$parent = (string) $parent;
if (!array_key_exists($parent, $childrenTree))
$childrenTree[$parent] = array();
$childrenTree[$parent][] = (string) $id;
}
return renderTreeCategories("0" ,$childrenTree,$categoryNames);
}
//Main recursive function. I'll asume '0' id is the root node
function renderTreeCategories($parent, $childrenTree,$categoryNames ) {
$children = $childrenTree[$parent];
if (count($children) > 0) { //If node has children
$html .= '<li class="dropdown open">';
$html .= '<a data-toggle="dropdown" class="dropdown-toggle" href="#">'.$categoryNames[$parent].' <b class="caret"></b></a>';
$html .= '<ul class="dropdown-menu">';
foreach ($children as $child){
$html .= renderTreeCategories($child,$childrenTree,$categoryNames);
}
$html .= "</ul></li>";
} else{
$html .= '<li>'.$categoryNames[$parent].'</li>';
}
if ($parent != "0"){
//$html .= "</li>";
}
return $html;
}
my $childrenTree array looks as it follows
array ( 0 =>array ( 0 => '1', 1 => '2', 2 => '5', 3 => '8', 4 => '9', 5 => '10', 6 => '11', 7 => '12', 8 => '14', ),
1 => array ( 0 => '3', ),
2 => array ( 0 => '4', ),
5 => array ( 0 => '6', 1 => '7', ),
10 => array ( 0 => '13' )
9 => array ( 0 => '15', 1 => '16', )
)
UPDATE
the code what implenented seems to work but not sure if is the best workaround.
//Main recursive function. I'll asume '0' id is the root node
function renderTreeCategories($parent, $childrenTree,$categoryNames ) {
$children = $childrenTree[$parent];
$has_childs = count($children);
if ($parent != "0") {
if ($has_childs > 0) {
$html .= "<li class='dropdown'>";
$html .= ''.$categoryNames[$parent].' <b class="caret"></b>';
$html .= '<ul class="dropdown-menu">';
} else{
$html .= "<li>";
$html .= '' . $categoryNames[$parent] . '';
}
}
if ($has_childs > 0) { //If node has children
foreach ($children as $child)
$html .= renderTreeCategories($child, $childrenTree,$categoryNames);
$html .= "</ul>";
}
if ($parent != "0") {
$html .= "</li>";
}
return $html;
}

Create html select list from php array?

Hello I find php function that outputs html select list from array.
function buildTree(Array $data, $parent = 0) {
$tree = array();
foreach ($data as $d) {
if ($d['parent'] == $parent) {
$children = buildTree($data, $d['id']);
// set a trivial key
if (!empty($children)) {
$d['_children'] = $children;
}
$tree[] = $d;
}
}
return $tree;
}
$rows = array(
array ('id' => 1, 'name' => 'Test 1', 'parent' => 0),
array ('id' => 2, 'name' => 'Test 1.1', 'parent' => 1),
array ('id' => 3, 'name' => 'Test 1.2', 'parent' => 1),
array ('id' => 4, 'name' => 'Test 1.2.1', 'parent' => 3),
array ('id' => 5, 'name' => 'Test 1.2.2', 'parent' => 3),
array ('id' => 6, 'name' => 'Test 1.2.2.1', 'parent' => 5),
array ('id' => 7, 'name' => 'Test 2', 'parent' => 0),
array ('id' => 8, 'name' => 'Test 2.1', 'parent' => 7),
);
$tree = buildTree($rows);
// print_r($tree);
function printTree($tree, $r = 0, $p = null) {
foreach ($tree as $i => $t) {
$dash = ($t['parent'] == 0) ? '' : str_repeat('-', $r) .' ';
printf("\t<option value='%d'>%s%s</option>\n", $t['id'], $dash, $t['name']);
if ($t['parent'] == $p) {
// reset $r
$r = 0;
}
if(isset($t['_children'])){
printTree($t['_children'], ++$r, $t['parent']);
}
}
}
print("<select>\n");
printTree($tree);
print("</select>");
but I need to rewrite to return result like this:
$select = "<select>";
$select .= printTree($list);
$select .= "</select>";
echo $select;
// or better
return $select;
The problem is with recursion, solution is to fill each option in array, but I don't know how to do that in recursive functions, and also
printf("\t<option value='%d'>%s%s</option>\n", $t['id'], $dash, $t['name']);
prints directly when foreach loop iterate.
Thanks.
So i figure out where was my mistake, that was simply because i fill an array with html option tag eg.
<option value="0">Start</option>
but with php function print_r() i see nothing in array value exepts when i inspect DOM element.
so here is my final solution:
this function fill values in multi dimensional array, to further needs
# edited printTree() function, renamed to toSEL()
# $array - data array like above,
# $r - to correct iterate, $p - parent id,
# $currentID - what id is selected
function toSEL($array, $r = 0, $p = null, $currentID=null){
foreach($array as $value){
$dash = ($value[parent] == 0) ? '' : str_repeat('-', $r) .' ';
if($value[id]==$currentID){
$html[] = '<option value="'.$value[id].'" selected="selected">'.$dash.$value[name].'</option>';
}else{
$html[] = '<option value="'.$value[id].'">'.$dash.$value[name].'</option>';
}
if($value['parent'] == $p){
// reset $r
$r = 0;
}
if(!empty($value[children])){
$html[] = toSEL($value[children], ++$r, $value[parent], $currentID);
}
}
return $html;
}
to convert from multi dimensional array to one dimension
$aNonFlat = toSEL($list, 0, null, $currentID);
$result = array();
array_walk_recursive($aNonFlat,function($v, $k) use (&$result){ $result[] = $v; });
then if needs to output HTML use some simple loop.
$html = '<select>';
foreach($result as $res){
$html .= $res;
}
$html .='</select>';
echo $html;

PHP Recursive array looping and formatting

I have some PHP code I've been working on for a good few days now. I'm trying to generate a formatted list of rules from a flat array. I got help here before on how to turn the flat array into a tree array, but I'm having difficulty writing a recursive function that can go through it and successfully break it down at points at such depths where I'd like the rules to be members of an unordered list from the markup that gets printed.
<?php
$data = array(
'0' => 'Introduction',
'4' => 'General',
'4.1' => 'Chat',
'4.1.1' => 'Do',
'4.1.1.9' => 'This',
'4.1.1.10' => 'That',
'4.1.1.11' => 'Other',
);
$struct = array(
'children' => array()
);
foreach ($data as $ruleID => $content)
{
$parent =& $struct;
foreach (explode('.', $ruleID) as $val)
{
if (!isset($parent['children'][$val]))
{
$parent['children'][$val] = array(
'content' => '',
'children' => array()
);
}
$parent =& $parent['children'][$val];
}
$parent['content'] = $content;
}
$out = '';
$rules = array_pop($struct);
format_rule($rules);
var_dump($rules);
echo $out;
function format_rule($arr, $depth=0)
{
global $out;
echo "depth: $depth\n";
foreach($arr as $key => $val)
{
switch($depth)
{
case 0:
$out .= '<h1>'.$val['content']."</h1><br />\n";
break;
case 1:
$out .= '<h2>'.$val['content']."</h2><br />\n";
break;
case 2:
$out .= '<h3>'.$val['content']."</h3><br />\n";
break;
default:
$out .= '<li>'.$val['content']."</li>\n";
break;
}
if(isset($val['children']) && count($val['children']) > 0)
{
if($depth > 2)
{
$out .= '<ul>';
format_rule($val['children'], ++$depth);
$out .= '</ul>';
}
else
{
format_rule($val['children'], ++$depth);
}
}
}
}
The output at the moment is:
<h1>Introduction</h1><br />
<h1>General</h1><br />
<h2>Chat</h2><br />
<h3>Do</h3><br />
<li>This</li><br />
<li>That</li><br />
<li>Other</li><br />
Which is great, except from my code I'm pretty sure the section under 'Do' should have a <ul> around it.
change your code to :
if($depth >= 2)
note: remember the count starts at 0, not 1.
Try this:
<?php
$data = array(
'0' => 'Introduction',
'4' => 'General',
'4.1' => 'Chat',
'4.1.1' => 'Do',
'4.1.1.9' => 'This',
'4.1.1.10' => 'That',
'4.1.1.11' => 'Other',
);
function get_level($key){
return count(explode(".",$key));
}
function set_tag(&$array,$key,$item,&$ul_started){
$level = get_level($key);
switch($level){
case 1:
case 2:
case 3:
if($ul_started){
$array[$key] = "</ul><h".$level.">".$item."</h".$level."><br>";
$ul_started=false;
}else{
$array[$key] = "<h".$level.">".$item."</h".$level."><br>";
}
break;
default:
if(!$ul_started){
$array[$key] = "<ul><li><strong>".$item."</strong></li><br>";
$ul_started=true;
}else{
$array[$key] = "<li><strong>".$item."</strong></li><br>";
}
break;
}
}
$ul_started = false;
foreach($data as $key=>$item){
set_tag($data,$key,$item,$ul_started);
}
if($ul_started){
$keys = array_keys($data);
$data[$keys[count($data)-1]] .= "</ul>";
}
echo implode("",$data);
?>

How can I build a nested HTML list with an infinite depth from a flat array?

I'm trying to produce a multi-level HTML list from a source array that is formatted like this:
/**
* id = unique id
* parent_id = "id" that this item is directly nested under
* text = the output string
*/
$list = array(
array(
'id' => 1,
'parent_id' => 0,
'text' => 'Level 1',
), array(
'id' => 2,
'parent_id' => 0,
'text' => 'Level 2',
), array(
'id' => 3,
'parent_id' => 2,
'text' => 'Level 2.1',
), array(
'id' => 4,
'parent_id' => 2,
'text' => 'Level 2.2',
), array(
'id' => 5,
'parent_id' => 4,
'text' => 'Level 2.2.1',
), array(
'id' => 6,
'parent_id' => 0,
'text' => 'Level 3',
)
);
The goal is a nested <ul> with an infinite depth. The expected output of the array above is this:
Level 1Level 2Level 2.1Level 2.2Level 2.2.1Level 3
If only the array items had a key called child or something that contained the actual sub-array, it would be easy to recurse though these and get the desired output with a function like this:
function makeList($list)
{
echo '<ul>';
foreach ($list as $item)
{
echo '<li>'.$item['text'];
if (isset($item['child']))
{
makeList($item['child']);
}
echo '</li>';
}
echo '</ul>';
}
Unfortunately that's not the case for me - the format of the source arrays can't be changed. So, long ago I wrote this very nasty function to make it happen, and it only works up to three levels (code is pasted verbatim with original comments). I know it's a long boring read, please bear with me:
function makeArray($links)
{
// Output
$nav = array();
foreach ($links as $k => $v)
{
// If no parent_id is present, we can assume it is a top-level link
if (empty($v['parent_id']))
{
$id = isset($v['id']) ? $v['id'] : $k;
$nav[$id] = $v;
// Remove from original array
unset($links[$k]);
}
}
// Loop through the remaining links again,
// we can assume they all have a parent_id
foreach ($links as $k => $v)
{
// Link's parent_id is in the top level array, so this is a level-2 link
// We already looped through every item so we know they are all accounted for
if (isset($nav[$v['parent_id']]))
{
$id = isset($v['id']) ? $v['id'] : $k;
// Add it to the top level links as a child
$nav[$v['parent_id']]['child'][$id] = $v;
// Set a marker so we know which ones to loop through to add the third level
$nav2[$id] = $v;
// Remove it from the array
unset($links[$k]);
}
}
// Last iteration for the third level
// All other links have been removed from the original array at this point
foreach ($links as $k => $v)
{
$id = isset($v['id']) ? $v['id'] : $k;
// Link's parent_id is in the second level array, so this is a level-3 link
// Orphans will be ignored
if (isset($nav2[$v['parent_id']]))
{
// This part is crazy, just go with it
$nav3 = $nav2[$v['parent_id']]['parent_id'];
$nav[$nav3]['child'][$v['parent_id']]['child'][] = $v;
}
}
return $nav;
}
This makes an array like:
array(
'text' => 'Level 1'
'child' => array(
array(
'text' => 'Level 1.2'
'child' => array(
array(
'text' => 'Level 1.2.1'
'child' => array(
// etc.
),
array(
'text' => 'Level 1.2.2'
'child' => array(
// etc.
),
)
)
)
)
);
Usage:
$nav = makeArray($links);
makeList($nav);
I've spent many spare hours trying to work this out, and the original code which I have given here is still the best solution I've been able to produce.
How can I make this happen without that awful function (which is limited to a depth of 3), and have an infinite number of levels? Is there a more elegant solution to this?
Print:
function printListRecursive(&$list,$parent=0){
$foundSome = false;
for( $i=0,$c=count($list);$i<$c;$i++ ){
if( $list[$i]['parent_id']==$parent ){
if( $foundSome==false ){
echo '<ul>';
$foundSome = true;
}
echo '<li>'.$list[$i]['text'].'</li>';
printListRecursive($list,$list[$i]['id']);
}
}
if( $foundSome ){
echo '</ul>';
}
}
printListRecursive($list);
Create multidimensional array:
function makeListRecursive(&$list,$parent=0){
$result = array();
for( $i=0,$c=count($list);$i<$c;$i++ ){
if( $list[$i]['parent_id']==$parent ){
$list[$i]['childs'] = makeListRecursive($list,$list[$i]['id']);
$result[] = $list[$i];
}
}
return $result;
}
$result = array();
$result = makeListRecursive($list);
echo '<pre>';
var_dump($result);
echo '</pre>';
Tested and working :)
$list = array(...);
$nested = array();
foreach ($list as $item)
{
if ($item['parent_id'] == 0)
{
// Woot, easy - top level
$nested[$item['id']] = $item;
}
else
{
// Not top level, find it's place
process($item, $nested);
}
}
// Recursive function
function process($item, &$arr)
{
if (is_array($arr))
{
foreach ($arr as $key => $parent_item)
{
// Match?
if (isset($parent_item['id']) && $parent_item['id'] == $item['parent_id'])
{
$arr[$key]['children'][$item['id']] = $item;
}
else
{
// Keep looking, recursively
process($item, $arr[$key]);
}
}
}
}
Some methods I recently wrote, maybe some will help, sorry I'm short on time and cannot rewite them to match your needs.
This code is actually a part of Kohana Framework Model, method ->as_array() is used to flat an Database_Result object.
function array_tree($all_nodes){
$tree = array();
foreach($all_nodes as $node){
$tree[$node->id]['fields'] = $node->as_array();
$tree[$node->id]['children'] = array();
if($node->parent_id){
$tree[$node->parent_id]['children'][$node->id] =& $tree[$node->id];
}
}
$return_tree = array();
foreach($tree as $node){
if($node['fields']['depth'] == 0){
$return_tree[$node['fields']['id']] = $node;
}
}
return $return_tree;
}
array_tree() is used to make a tree out of a flat array. The key feature is the =& part ;)
function html_tree($tree_array = null){
if( ! $tree_array){
$tree_array = $this -> array_tree();
}
$html_tree = '<ul>'."\n";
foreach($tree_array as $node){
$html_tree .= $this->html_tree_crawl($node);
}
$html_tree .= '</ul>'."\n";
return $html_tree;
}
function html_tree_crawl($node){
$children = null;
if(count($node['children']) > 0){
$children = '<ul>'."\n";
foreach($node['children'] as $chnode){
$children .= $this->html_tree_crawl($chnode);
}
$children .= '</ul>'."\n";
}
return $this->html_tree_node($node, $children);
}
html_tree_node() is a simple method to display current node and children in HTML.
Example below:
<li id="node-<?= $node['id'] ?>">
<?= $node['title'] ?>
<?= (isset($children) && $children != null) ? $children : '' ?>
</li>

Categories