0

Note

This question is NOT a duplicate of

or several other related questions


I have a very peculiar use case where I have to filter a multi-dimensional array 'inwards to outwards', implying

"filter the innermost level elements, then it's preceding level elements and so on until coming to filtering the topmost level"


As an (dummy, mock) example, consider this

suppose we have a nested of restaurant-ids (res_ids) grouped by countries (1, 2, & 3) & cities (11, 12, 21, 22, 23, 31)

[
     1 => [
         11 => [
             111 => ['res_id' => 111, 'city_id' => 11, 'country_id' => 1],
             112 => ['res_id' => 112, 'city_id' => 11, 'country_id' => 1],
             113 => ['res_id' => 113, 'city_id' => 11, 'country_id' => 1],
         ],
         12 => [
             121 => ['res_id' => 121, 'city_id' => 12, 'country_id' => 1],
         ],
     ],
     2 => [
         21 => [
             212 => ['res_id' => 212, 'city_id' => 21, 'country_id' => 2],
             214 => ['res_id' => 214, 'city_id' => 21, 'country_id' => 2],
         ],
         22 => [
             221 => ['res_id' => 221, 'city_id' => 22, 'country_id' => 2],
             222 => ['res_id' => 222, 'city_id' => 22, 'country_id' => 2],
             223 => ['res_id' => 223, 'city_id' => 22, 'country_id' => 2],
         ],
     ],
     3 => [
         31 => [
             312 => ['res_id' => 312, 'city_id' => 21, 'country_id' => 2],
             314 => ['res_id' => 314, 'city_id' => 21, 'country_id' => 2],
         ],
     ]
]
  • and we want to remove all restaurants (plus the parent sub-array structure) having even res_ids (keep odd ones)
  • so that resulting output nested array is as follows
    • note that not only individual 'leaf' items depicting res have been filtered, but also higher level city and country items have been filtered if they contained only even res_ids (which we intended to remove)
[
     1 => [
         11 => [
             111 => ['res_id' => 111, 'city_id' => 11, 'country_id' => 1],
             113 => ['res_id' => 113, 'city_id' => 11, 'country_id' => 1],
         ],
         12 => [
             121 => ['res_id' => 121, 'city_id' => 12, 'country_id' => 1],
         ],
     ],
     2 => [
         22 => [
             221 => ['res_id' => 221, 'city_id' => 22, 'country_id' => 2],
             223 => ['res_id' => 223, 'city_id' => 22, 'country_id' => 2],
         ],
     ],
]

actually i myself created above array from a flat array by recursive grouping; but now I have to filter them in groups (which can't be done before grouping)


  • While i can certainly do this using nested loops, I was wondering if we can create a generic function for it (i have other such multi-dimensional filtering use-cases at different depths across my project)
  • Another important thing to note here that given the generic filtering criteria requirement, we would ideally like to be able to have a different filtering criteria per level: custom functions per se.

Any ideas?

y2k-shubham
  • 10,183
  • 11
  • 55
  • 131

2 Answers2

1

You can do this lika a array_filter callback.

$currentKey is not required, but may be handy.

Working example.

function array_filter_clean(array $array, array $callbacks, $currentDepth = 0, $currentKey = '') {
    if (array_key_exists($currentDepth, $callbacks)) { // identify node to apply callback to
        $callback = $callbacks[$currentDepth];
        if (!$callback($currentKey, $array)) { // empty node when callback returns false (or falsy)
            return [];
        }
    }
    
    foreach ($array as $key => &$value) { // &value to modify $array
        if (is_array($value)) {
            $value = array_filter_clean($value, $callbacks, $currentDepth+1, $key); // recurse if array
        }
    }
    
    return array_filter($array); // remove empty nodes (you may want to add "afterCallbacks" here)
}

$callbacksByDepth = [
    /* 2 => function ($key, $value) {
        return $key > 20;
    }, */ // test
    3 => function ($key, $value) {
        return $value['res_id']%2;
    },
];

$output = array_filter_clean($input, $callbacksByDepth);

print_r($output);

I've added comments - in case i forgot to explain something please let me know.

Worth mentioning

This can be done with an extension of RecursiveFilterIterator within a RecursiveIteratorIterator - but the readability of the provided solution is far superior.

Note

Given the case you want to keep every node which contains at least 3 items after you've applied your callback, you will have to extend this funtion (at the last line). You could do exactly the same like above array_key_exists($currentDepth, $callbacksXXX) for another $callbacksAfter array with the same structure. (Or build everything in one array and key your callbacks with before and after - up to you)

SirPilan
  • 4,649
  • 2
  • 13
  • 26
0
  • I came up with following function that accepts a list of callables, each one of which is used for filtering the array at a single level

    • in-line with the original example, filtering is done 'inwards to outwards': first we filter the innermost level, then the one above that and so on (so essentially the nth filter acts on residual output obtained by applying n-1 filters before it)
    • have a look at the unit-tests to understand the behaviour
/**
 * Filters a multi-dimensional array recursively by applying series of filtering function callables, each at a
 * different level. Filtering is done starting from innermost depth and moving outwards.
 * It is assumed that structure / depth of array is consistent throughout (each key grows upto same max depth)
 *
 * Regarding $filter_callables
 *  - this is a series of filtering functions (callables) applied at each level (1st callable is for first /
 *    top-most or outer-most level, next callable is for next level at depth 2 and so on)
 *  - each filter callable function should accept exactly 2 arguments: (1) the value or item and (2) the key of item
 *    as mandated by 'ARRAY_FILTER_USE_BOTH' flag of PHP's array_filter function
 *  - to skip applying filtering at a level, we can pass null (instead of callable) for that position
 *  - no of callables should be less than or equal to depth of array (or else exception will be thrown)
 *
 * see test-cases to understand further (plus detailed explaination)
 * @param array $nested_array Nested array to be filtered resursively
 * @param array $filter_callables List of callables to be used as 'filter' functions at each 'depth' level
 * @return array Recursively filtered array
 */
public static function filterByFnRecursive(array $nested_array, array $filter_callables): array {
    if (empty($nested_array) || empty($filter_callables)) {
        // base case: if array is empty (empty array was passed) or no more callables left to be applied, return
        return $nested_array;
    } else {
        // retrieve first callable (meant for this level)
        $filterer = array_shift($filter_callables);

        if (!empty($filter_callables)) {
            // if there are more callables, recursively apply them on items of current array
            $modified_nested_array = array_map(static function (array $item) use ($filter_callables): array {
                return self::filterByFnRecursive($item, $filter_callables);
            }, $nested_array);
        } else {
            // otherwise keep the current array intact
            $modified_nested_array = $nested_array;
        }

        if (empty($filterer)) {
            // if callable is NULL, return array (at current level) unmodified
            // this is provided to allow skipping filtering at any level (by passing null callable)
            return $modified_nested_array;
        } else {
            // otherwise filter the items at current level
            return array_filter($modified_nested_array, $filterer, ARRAY_FILTER_USE_BOTH);
        }
    }
}

Do checkout this gist for bigger collection of array utility functions along with unit-tests

y2k-shubham
  • 10,183
  • 11
  • 55
  • 131