Athari / YaLinqo

Yet Another LINQ to Objects for PHP [Simplified BSD]
https://athari.github.io/YaLinqo
BSD 2-Clause "Simplified" License
439 stars 39 forks source link

GroupBy Deep works for groupBy('$v["A"]', '$v["B"]') but crashes for groupBy('$v["A"]', '$v["B"]', '$v["C"]') #19

Closed josoroma-zz closed 8 years ago

josoroma-zz commented 8 years ago

Hi!

Beautiful package, but I have probably an easy roadblock to solve since this morning, please let me explain my scenario, I am gathering the following array result (Role/Area/Permissions) from a Laravel Builder Query:

array
  0 => 
    array
      'RoleName' => string 'Root'
      'AreaName' => string 'Global'
      'PermissionName' => string 'Post'
  1 => 
    array (size=3)
      'RoleName' => string 'Root'
      'AreaName' => string 'Global'
      'PermissionName' => string 'Delete'
  3 => 
    array (size=3)
      'RoleName' => string 'Editor'
      'AreaName' => string 'Europe'
      'PermissionName' => string 'Index'
  4 => 
    array (size=3)
      'RoleName' => string 'Editor'
      'AreaName' => string 'Europe'
      'PermissionName' => string 'Post'

and I was wondering how to produce the following array result using your groupBy method:

Root
    Global
        Post
        Delete
        ...

...

Editor
    Europe
        Index
        Area
        ...

This what I am currently using right now:

        $roles = from($permissions)
            ->groupBy('$v["RoleName"]', '$v["AreaName"]', '$v["PermissionName"]')
            ->toArray();

Thanks in advance!

Athari commented 8 years ago

I have no idea how you managed to get groupBy('$v["RoleName"]', '$v["AreaName"]', '$v["PermissionName"]') to work, because it shouldn't. :laughing: The arguments aren't just key selectors, they're key selector (value to group by), value selector (value of item) and result selector (two functions joining results from key and value selectors), each with distinct functionality (please read PHPDoc).

What you need is nested grouping. Let's assume:

$items = [
    [ 'category' => 'Foos', 'group' => 'Big foos', 'name' => 'Foo 1' ],
    [ 'category' => 'Foos', 'group' => 'Big foos', 'name' => 'Foo 2' ],
    [ 'category' => 'Foos', 'group' => 'Small foos', 'name' => 'Foo 3' ],
    [ 'category' => 'Bars', 'group' => 'Big bars', 'name' => 'Bar A' ],
    [ 'category' => 'Bars', 'group' => 'Small bars', 'name' => 'Bar B' ],
    [ 'category' => 'Bars', 'group' => 'Small bars', 'name' => 'Bar C' ],
];

First you need to group by category. In the result selector for category's items, you group sequence again, by group.

$grouped = from($items)
    ->groupBy(
        function ($item) { return $item['category']; },
        null,
        function ($subitems, $category) {
            return from($subitems)
                ->groupBy(
                    function ($subitem) { return $subitem['group']; }
                );
        });
// -or-
$grouped = from($items)
    ->groupBy(
        '$v["category"]', '$v',
        function ($subitems) {
            return from($subitems)->groupBy('$v["group"]');
        });

print_r($grouped->toArrayDeep());

Output:

Array
(
    [Foos] => Array
        (
            [Big foos] => Array
                (
                    [0] => Array
                        (
                            [category] => Foos
                            [group] => Big foos
                            [name] => Foo 1
                        )
                    [1] => Array
                        (
                            [category] => Foos
                            [group] => Big foos
                            [name] => Foo 2
                        )
                )
            [Small foos] => Array
                (
                    [0] => Array
                        (
                            [category] => Foos
                            [group] => Small foos
                            [name] => Foo 3
                        )
                )
        )
    [Bars] => Array
        (
            [Big bars] => Array
                (
                    [0] => Array
                        (
                            [category] => Bars
                            [group] => Big bars
                            [name] => Bar A
                        )
                )
            [Small bars] => Array
                (
                    [0] => Array
                        (
                            [category] => Bars
                            [group] => Small bars
                            [name] => Bar B
                        )
                    [1] => Array
                        (
                            [category] => Bars
                            [group] => Small bars
                            [name] => Bar C
                        )
                )
        )
)

null as key selector means "use default", in this case just return value from an array's item (function ($item) { return $item['item']; } or '$v'). I haven't specified result selectors for keys, the default is used too (the value from key selector is used as item key).

Here is a more complex example which uses all arguments of the groupBy method:

$grouped = from($items)
    ->groupBy(
        function ($item) { return $item['category']; },
        function ($item) { return $item; },
        function ($subitems, $category) {
            return [
                'category' => $category,
                'items' => from($subitems)
                    ->groupBy(
                        function ($subitem) { return $subitem['group']; },
                        function ($subitem) { return $subitem['name']; },
                        function ($subsubitems, $group) {
                            return [
                                'group' => $group,
                                'items' => $subsubitems,
                            ];
                        },
                        Functions::increment()
                    )
            ];
        },
        Functions::increment());
// -or-
$grouped = from($items)
    ->groupBy(
        '$v["category"]', '$v',
        function ($subitems, $category) {
            return [
                'category' => $category,
                'items' => from($subitems)
                    ->groupBy(
                        '$v["group"]', '$v["name"]',
                        function ($subsubitems, $group) {
                            return [
                                'group' => $group,
                                'items' => $subsubitems,
                            ];
                        },
                        Functions::increment()
                    )
            ];
        },
        Functions::increment());

print_r($grouped->toArrayDeep());

Output:

Array
(
    [0] => Array
        (
            [category] => Foos
            [items] => Array
                (
                    [0] => Array
                        (
                            [group] => Big foos
                            [items] => Array
                                (
                                    [0] => Foo 1
                                    [1] => Foo 2
                                )
                        )
                    [1] => Array
                        (
                            [group] => Small foos
                            [items] => Array
                                (
                                    [0] => Foo 3
                                )
                        )
                )
        )
    [1] => Array
        (
            [category] => Bars
            [items] => Array
                (
                    [0] => Array
                        (
                            [group] => Big bars
                            [items] => Array
                                (
                                    [0] => Bar A
                                )
                        )
                    [1] => Array
                        (
                            [group] => Small bars
                            [items] => Array
                                (
                                    [0] => Bar B
                                    [1] => Bar C
                                )
                        )
                )
        )
)

If you need three levels of grouping, the same logic applies:

$grouped = from($items)
    ->groupBy(
        function ($item) { return $item['category']; },
        null,
        function ($catitems) {
            return from($catitems)
                ->groupBy(
                    function ($item) { return $item['group']; },
                    null,
                    function ($groupitems) {
                        return from($groupitems)
                            ->groupBy(
                                function ($item) { return $item['name']; }
                            );
                    }
                );
        });
// -or-
$grouped = from($items)
    ->groupBy(
        '$v["category"]', '$v',
        function ($catitems) {
            return from($catitems)
                ->groupBy(
                    '$v["group"]', '$v',
                    function ($groupitems) {
                        return from($groupitems)->groupBy(
                            '$v["name"]', '$v'
                        );
                    });
        });

Grouping and joining methods are the most complex in LINQ, so it takes some time to get used to them fully.

Athari commented 8 years ago

Grouping by multiple keys produces a bit too nested code, so you can write helper methods like this:

function group_by_multiple ($items, array $keySelectors, $valueSelector = null)
{
    $resultSelector = null;
    foreach (array_reverse($keySelectors) as $keySelector) {
        $resultSelector = function ($subitems) use ($keySelector, $valueSelector, $resultSelector) {
            return from($subitems)->groupBy(
                $keySelector,
                $resultSelector == null ? $valueSelector : null,
                $resultSelector);
        };
    }
    return $resultSelector($items);
}

print_r(
    group_by_multiple($items,
        [ '$v["category"]', '$v["group"]', '$v["name"]' ],
        '$v["name"]'
    )->toArrayDeep());
josoroma-zz commented 8 years ago

Amazing, doing my homework, thank you @Athari!