8
\$\begingroup\$

I've recently written a function that allows me to take a flat array and convert it into a multidimensional array by a specific key in the array.

public function groupArray($arr, $group, $preserveGroupKey = false, $preserveSubArrays = false) {
    $temp = array();
    foreach($arr as $key => $value) {
        $groupValue = $value[$group];
        if(!$preserveGroupKey)
        {
            unset($arr[$key][$group]);
        }
        if(!array_key_exists($groupValue, $temp)) {
            $temp[$groupValue] = array();
        }

        if(!$preserveSubArrays){
            $data = count($arr[$key]) == 1? array_pop($arr[$key]) : $arr[$key];
        } else {
            $data = $arr[$key];
        }
        $temp[$groupValue][] = $data;
    }
    return $temp;
}

Breakdown

function groupArray($arr, $group, $preserveGroupKey = false, $preserveSubArrays = false)

This function accepts 2 to 4 parameters.

  1. The flat array you want to group (array)
  2. The key you want to group by (string/int)
  3. Option to preserve the group key in the output of each sub array (Boolean)
  4. Option to preserve sub arrays. If only 1 key exists in each sub array, the function will store just the single value for each row instead of an array (Boolean)

The first parameter is the array itself, the second parameter is the key that you want to group by, and the 3rd (optional) parameter is a boolean that tells the function if you want to preserve the group key in the sub arrays.

Examples

Let's use this dataset for examples:

$arr = [
    0 => [
        "group" => "group1",
        "name" => "Bob",
    ],
    1 => [
        "group" => "group1",
        "name" => "Randy",
    ],
    2 => [
        "group" => "group1",
        "name" => "Susan",
    ],
    3 => [
        "group" => "group2",
        "name" => "Larry",
    ],
    4 => [
        "group" => "group2",
        "name" => "David",
    ],
    5 => [
        "group" => "group3",
        "name" => "Perry",
    ],
];

groupArray($arr, "group");

Will return

Array
(
    [group1] => Array
        (
            [0] => Bob
            [1] => Randy
            [2] => Susan
        )

    [group2] => Array
        (
            [0] => Larry
            [1] => David
        )

    [group3] => Array
        (
            [0] => Perry
        )

)

groupArray($arr, "group", true);

will return

Array
(
    [group1] => Array
        (
            [0] => Array
                (
                    [group] => group1
                    [name] => Bob
                )

            [1] => Array
                (
                    [group] => group1
                    [name] => Randy
                )

            [2] => Array
                (
                    [group] => group1
                    [name] => Susan
                )

        )

    [group2] => Array
        (
            [0] => Array
                (
                    [group] => group2
                    [name] => Larry
                )

            [1] => Array
                (
                    [group] => group2
                    [name] => David
                )

        )

    [group3] => Array
        (
            [0] => Array
                (
                    [group] => group3
                    [name] => Perry
                )

        )

)

groupArray($arr, "group", false, true);

Will return

Array
(
    [group1] => Array
        (
            [0] => Array
                (
                    [name] => Bob
                )

            [1] => Array
                (
                    [name] => Randy
                )

            [2] => Array
                (
                    [name] => Susan
                )

        )

    [group2] => Array
        (
            [0] => Array
                (
                    [name] => Larry
                )

            [1] => Array
                (
                    [name] => David
                )

        )

    [group3] => Array
        (
            [0] => Array
                (
                    [name] => Perry
                )

        )

)
\$\endgroup\$
1
  • 1
    \$\begingroup\$ It would be sweet if it supports multiple grouping keys. \$\endgroup\$ Commented Mar 31, 2021 at 19:43

2 Answers 2

6
\$\begingroup\$

Here are a few considerations:

  • You might rename $value to $subarray to be more literal about the data that it holds.

  • Because there is no need to modify the subarray, you can access the lone value by calling current() instead of array_pop().

  • You can combine your nested conditional logic into one and eliminate the declaration of $data by assigning directly to $temp[$groupValue][]

Just before the return:

if (!$preserveSubArrays && count($arr[$key]) == 1) {
    $temp[$groupValue][] = current($arr[$key]);
} else {
    $temp[$groupValue][] = $arr[$key];
}

I guess I should also point out that your function makes a few assumptions about the data being passed to it. There are a few ways to elicit Notices/Warnings from this function by passing an uncooperative array of data or grouping value. This may be a "tinfoil hat" observation for the scope of your question, but I thought I'd acknowledge it.

Generally, it looks likes a good cover-all function for the many StackOverflow questions that seek what this function can provide.

p.s. In a previous comment that I have since deleted, I suggested replacing unset() with another function. This was before I fully understood what this function was doing. I toyed with an approach that used array_diff_key(), but there was no advantage. unset() is sure to be the most efficient way to modify the subarray (when necessary).


Years later "re-review":

  • Declare the argument and return data types to make your method a bit more strict and easier to maintain.
  • Use square brace syntax for declaring an array -- it is just more concise and consistent with other square brace syntax used for pushing elements.
  • if(!array_key_exists($groupValue, $temp)) { $temp[$groupValue] = array(); } is unnecessary because in php you don't need to instantiate an array before you push elements into it using [] syntax. BUT, it makes good sense to check if a given row actually contains the grouping column -- else you will be trying to access an undefined key.
  • Obey PSR-12 coding recommendations.
  • Endeavor to remove all single-use variables unless they improve the readability/comprehensibility of your script.
  • When accessing the potentially unset grouping values, reliably find it using the undisturbed original array (not the copy being iterated).

Suggested rewrite:

public function groupArray(
    array $arr,
    string|int $group,
    bool $preserveGroupKey = false,
    bool $preserveSubArrays = false
): array {
    $result = [];
    foreach ($arr as $key => $row) {
        if (!key_exists($group, $row)) {
            throw new Exception("Could not group by $group on row with key $key");
        }
        if (!$preserveGroupKey) {
            unset($row[$group]);
        }
        $result[$arr[$key][$group]][] = !$preserveSubArrays && count($row) === 1
            ? current($row)
            : $row;
    }
    return $result;
}
\$\endgroup\$
0
3
\$\begingroup\$

Just in case you are looking for such a functionality for your database wrapper, note that it already exists in PDO, which can group results for you right off the query, thanks to the PDO::FETCH_GROUP constant that can be used in fetchAll() function.

So you can get your arrays out of the box. For the first one you could combine this mode with PDO::FETCH_COLUMN:

$data = $db->run("SELECT `group`, name FROM users")
           ->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_COLUMN);

whereas for the second one it would be just

$data = $db->run("SELECT `group`, name FROM users")->fetchAll(PDO::FETCH_GROUP);

Note that such a verbose notation is better then anonymous function parameters for readability.

Put yourself in place of someone who would be reading your code. For example, imagine you are reading a like this

combineArray($arr, "group", false, true);

-- would you be able to tell what does this particular call do?

So it's at least better to define named constants that contain your booleans as values and make this call

 groupArray($arr, "group", GR_PRESERVE_KEYS, GR_PRESERVE_SUBARRAYS);

having GR_DROP_KEY and GR_DISBAND_SUBARRAYS in reserve. Or consider just a single parameter with bitmask arguments, just like in many PHP functions.

\$\endgroup\$
1
  • \$\begingroup\$ Do you have any sources to read about bitmask arguments and named constants? \$\endgroup\$ Commented Oct 27, 2017 at 12:49

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.