8

I have a tree of categories of the following structure:

[6] => Array
    (
        [id] => 6
        [name] => computers
        [productCount] => 0
        [children] => Array
            (
                [91] => Array
                    (
                        [id] => 91
                        [name] => notebook
                        [productCount] => 5
                        [children] => Array
                            (
                            )
                    )

                [86] => Array
                    (
                        [id] => 86
                        [name] => desktop
                        [productCount] => 0
                        [children] => Array
                            (
                            )
                    )
            )
    )

Beside a subcategory, each category may contain products (like a folder may contain subfolders and just files).

I'm trying to write a recursive function which I want to take this array as reference and strip both leaf categories with [productCount] = 0 and all parent categories that contain such empty nodes. In other words, after processing I want to have only those categories that hold products on any sublevels.

I've wrote some code, now debugging it and it doesn't strip empty nodes. May be I'm not using references properly. Please, help me fix it, if possible.

    function pruneTree( & $node) {
    if ( ! $node['children'] && ! $node['productCount']) {
        unset($node);
    }
    if ( ! empty($node['children'])) {
        foreach ($node['children'] as $key => $child) {
            pruneTree($node['children'][$key]);
        }
    }
    return;
}
sevenWonders
  • 175
  • 1
  • 9

4 Answers4

5

unset deletes only the reference but not the referenced variable:

If a variable that is PASSED BY REFERENCE is unset() inside of a function, only the local variable is destroyed. The variable in the calling environment will retain the same value as before unset() was called.

So you need to pass the parent array and the key to delete that variable:

function pruneTree(&$parent, $key) {
    $node = &$parent[$key];
    if (!$node['children'] && !$node['productCount']) {
        unset($parent[$key]);
    }
    if (!empty($node['children'])) {
        foreach ($node['children'] as $key => &$child) {
            pruneTree($node['children'], $key);
        }
    }
}
Gumbo
  • 643,351
  • 109
  • 780
  • 844
  • Thank you, Gumbo! I've been searching for clues on the "references" page of the manual, and missed the point with `unset` and the scope. – sevenWonders Dec 01 '10 at 12:23
5

You could also change the parameter in the function to take an array of nodes instead of a single node. This changes the recursion slightly, and prevents the need to pass along a key:

function pruneTree(&$nodes) {
    foreach ($nodes as $key => $node) {
        if (!$node['children'] && !$node['productCount']) {
            unset($nodes[$key]);
        } elseif (!empty($node['children'])) {
            pruneTree($nodes[$key]['children']);
            // This line checks if all the children have been pruned away:
            if (empty($nodes[$key]['children'])) {
                unset($nodes[$key]);
            }
        }
    }
}

Also, added a check that ensures that if all child nodes are pruned, the parent (now, leaf) node also gets pruned.

Hope this helps!


Test data:

$data = array(
    6 => array(
        'id' => 6,
        'name' => 'computers',
        'productCount' => 0,
        'children' => array(
            91 => array(
                'id' => 91,
                'name' => 'notebook',
                'productCount' => 5,
                'children' => array()
            ),
            86 => array(
                'id' => 86,
                'name' => 'desktop',
                'productCount' => 0,
                'children' => array()
            )
        )
    )
);

The Call:

pruneTree($data);
echo '<pre>';
print_r($data);
echo '</pre>';
RabidFire
  • 6,280
  • 1
  • 28
  • 24
  • It turns out it's impossible to use `unset($nodes[$key]);` inside a function to modify initial array that is passed by reference, because it will just unset the reference variable within the functions scope. – sevenWonders Dec 01 '10 at 12:14
  • @sevenWonders - I forgot to mention that I tested this script (as well as Gumbo's) and they both work. There's virtually no difference, except that I find the key from within the called function before unsetting. – RabidFire Dec 01 '10 at 13:03
  • @RabidFire - Strangely when I test your function I get an error "Only variables can be passed by reference" at line `pruneTree($nodes[$key]['children']);`. – sevenWonders Dec 01 '10 at 13:23
  • Added the test data and the way I'm calling this. My PHP version is 5.3.0. Maybe it has something to do with that. Dunno, has got me stumped. I can't pass in a variable by reference and unset it from within a function, unless it's an array and I have the key. – RabidFire Dec 01 '10 at 13:42
  • @RabidFire - Oh, I'm so sorry, while testing your code in my application I overlooked the fact that your function takes array of nodes instead of a single node. My bad. Your code works flawlessly! And it's more suitable for my application because the trees for prunning actually come from another array, which I had to iterate in a loop because of my initial recursion design, and your approach simplifies that too. – sevenWonders Dec 01 '10 at 15:25
2

I would do that. Notice the "&" in the foreach.

function pruneTree(&$node)
{
    foreach ($node as $index => &$value) {
        if (empty($value)) {
            unset($node[$index]);
        } elseif (is_array($value)) {
            pruneTree($value);
        }
    }
}
lsd
  • 504
  • 1
  • 4
  • 16
1

I dont know if this is the case, but when i needed to change values recursively in array, i needed to pass & to the foreach value as well.

private function convertXMLPart(&$array) {
        foreach ($array as $rowKey => &$row) {
            if (gettype($row) != 'string') {
                $row = (array)$row;
                if (!empty($row['@attributes'])) {
                    foreach ($row['@attributes'] as $key => $value) {
                        $row[$key] = $value;
                    }
                    unset($row['@attributes']);
                    $array[$rowKey] = $row;
                }
                $this->convertXMLPart($row);
            }
        }
    }
Aiphee
  • 8,904
  • 3
  • 18
  • 16