3
\$\begingroup\$

I'd been using this form for a while now with the functional methods map, filter, reduce:

// For example...
$result = array_reduce(array_map(function($value) {
    return explode('-', $value);
}, $input), function($carry, $item) {
    return array_merge($carry, $item);
}, array());

The array_reduce() part, where I need to collapse depth in the result. So I was thinking about this, and this is what I came up with. It actually works as a callback to array_reduce() too:

/**
 * Flatten a multi-dimensional array to a single-level array,
 * sorting the entries in order to depth first.
 * 
 * @param array $carry Array to initialize, will be the subject if the only argument passed.
 * @param mixed $subject Optional Array or value that has data in potential multi-dimensions.
 * 
 * @return array
 */
function array_flatten(?array $carry = [], $subject = null): array
{
    return array_reduce((array) ($subject ?? $carry), function(array $carry, $item) {
        return is_array($item)
            ? array_flatten($carry, $item) 
            : array_merge($carry, (array) $item);
    }, $subject !== null ? (array) $carry : []); // Don't merge onto the $subject...
}

https://3v4l.org/J1NeJ

Thing is, the way it works, I think it literally goes one level further than there are levels to traverse. The reason I think this is because the array_merge() is getting passed a "boxed" element. So the way I'm adding to the array is to box/unbox an array wrapper so it can be merged.

That seems like the most amount of extra effort for one little thing; it happens to every non-array it comes across. Questioning whether I should use it.

It can be called like array_reduce():

array_reduce([1,2,[3,[4]],5], 'array_flatten');

Or more likely:

array_flatten([1,2,[3,[4]],5]);

Is this something I should avoid?

\$\endgroup\$

3 Answers 3

3
\$\begingroup\$

PHP already has a native function for this task.

array_walk_recursive() traverses your "leaf nodes" for you in one step:

Code: (Demo)

//$multi=[1,2,[3,[4]],5];
$multi=[array('A', 
        array('B',
            array('CC','CCCC','CCCCCC'), 
            array('DDD','EFG','HIJ','LMNOP')
        )
    ), 
    array('2'), 
    'b',
    array(array(),array(),array())];
array_walk_recursive($multi,function($v)use(&$flat){$flat[]=$v;});
var_export($flat);

Output:

array (
  0 => 'A',
  1 => 'B',
  2 => 'CC',
  3 => 'CCCC',
  4 => 'CCCCCC',
  5 => 'DDD',
  6 => 'EFG',
  7 => 'HIJ',
  8 => 'LMNOP',
  9 => '2',
  10 => 'b',
)
\$\endgroup\$
2
2
\$\begingroup\$

I don't see the benefit of being able to call array_flatten() from array_reduce(). If there is an important use, please let me know as I'd like to be educated on this point. My code to follow cannot be called like this: array_reduce([1,2,[3,[4]],5], 'array_flatten'); as yours was designed to do.

I have made a few modifications and have a php<7 friendly version to offer... more specifically, I've omitted the type (and nullable) declarations and null coalescing operator so that PHP<7 versions can use the snippet. I'm not suggesting that your type declarations are a bad idea, I only mean to make the function more usable to a wider audience.

Code: (Demo)

$multi=[['A',['B',['CC','CCCC','CCCCCC'],['DDD','EFG','HIJ','LMNOP']]],['2'],'b',[[],[],[]]];

function array_flatten($array,$accumulator=[]){
    return array_reduce($array,function($carry,$item){
        return is_array($item) ? array_flatten($item,$carry) : array_merge($carry,[$item]);
    }, $accumulator);
}
var_export(array_flatten($multi));
\$\endgroup\$
1
  • \$\begingroup\$ There isn't a benefit, it's just that it fits the same signature. That was for the recursion. \$\endgroup\$ Commented Oct 2, 2017 at 13:30
1
\$\begingroup\$

If you're just working on arrays this might satisfy your functional urge for selecting out of a deep data structure:

function recursive_select(
    $data,
    callable $callback,
    $flag = RecursiveIteratorIterator::SELF_FIRST,
    $preserve_keys = FALSE
) {
    return iterator_to_array(new CallbackFilterIterator(
        new RecursiveIteratorIterator(
            new RecursiveArrayIterator($data),
            $flag
        ), 
        $callback
    ), $preserve_keys);
}

Usage here: https://3v4l.org/TDIvu

$multi=[['A',['B',['CC','CCCC','CCCCCC'],['DDD','EFG','HIJ','LMNOP']]],['2'],'b',[[],[],[]]];
$array = array(1,2,[3,4],5,'six',array(array('78',9)),10);


$multi_output = recursive_select($multi, function ($value, $key, $iterator) : bool {
  return is_string($value);
});

$array_output = recursive_select($array, function ($value, $key, $iterator) : bool {
    return is_numeric($value) || is_string($value);
});

var_dump($multi_output, $array_output);

Output:

Array
(
    [0] => A
    [1] => B
    [2] => CC
    [3] => CCCC
    [4] => CCCCCC
    [5] => DDD
    [6] => EFG
    [7] => HIJ
    [8] => LMNOP
    [9] => 2
    [10] => b
)
Array
(
    [0] => 1
    [1] => 2
    [2] => 3
    [3] => 4
    [4] => 5
    [5] => six
    [6] => 78
    [7] => 9
    [8] => 10
)

If you need something that works like that on objects and arrays I've got a version for that, its not a quite a one liner though because behaviour changed subtly in RecursiveArrayIterator between PHP 5 and 7 despite what the PHP documentation says on this :|

The $iterator parameter has a method on it called getDepth() which can be used for fetching from a particular level within the comparison function

\$\endgroup\$
3
  • \$\begingroup\$ Sorry for the late response. I like it, with the only quibble I have being that the $callback requires a signature that embeds a requirement the function itself doesn't have, which makes it feel weird to me. So it really feels like syntactic glue. \$\endgroup\$ Commented Dec 3, 2017 at 14:38
  • \$\begingroup\$ The signature of the callable of recursive_select is compatible with array_filter rather than array_reduce, if you wanted to accumulate I'd suggest filtering with recursive_select and then accumulating after, filtering and accumulating in the same loop with array_reduce is closer to common foreach behaviour imo, where as the result of array_filter/recursive_select forces you to name your selection, which means it's easy to do multiple operations on it, whilst inside array_reduce accumulation and filtering is tightly coupled and hard to pass outside the function in a non accumulated fashion \$\endgroup\$
    – arcanine
    Commented Dec 11, 2017 at 13:24
  • \$\begingroup\$ That said, there can always be other things to consider like performance , or you may know you will not need access to the filtered data ever again, context trumps all if you're not ignorant to the trade offs being made, personally I err on the side of profiling first and being ready for code mutations that will need to occur in the future \$\endgroup\$
    – arcanine
    Commented Dec 11, 2017 at 13:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.