Skip to content

Instantly share code, notes, and snippets.

@brunogaspar
Last active May 1, 2024 07:24
Show Gist options
  • Save brunogaspar/154fb2f99a7f83003ef35fd4b5655935 to your computer and use it in GitHub Desktop.
Save brunogaspar/154fb2f99a7f83003ef35fd4b5655935 to your computer and use it in GitHub Desktop.
Recursive Laravel Collection Macros

What?

If a nested array is passed into a Laravel Collection, by default these will be threaded as normal arrays.

However, that's not always the ideal case and it would be nice if we could have nested collections in a cleaner way.

This is where this macro comes in handy.

Setup

Register this macro for example on the boot method of your app\Providers\AppServiceProvider.php file:

\Illuminate\Support\Collection::macro('recursive', function () {
    return $this->map(function ($value) {
        if (is_array($value) || is_object($value)) {
            return collect($value)->recursive();
        }

        return $value;
    });
});

Note: Tested on Laravel 5.5 and 5.6!

How

Usage is quite simple:

$data = [
    [
        'name' => 'John Doe',
        'emails' => [
            'john@doe.com',
            'john.doe@example.com',
        ],
        'contacts' => [
            [
                'name' => 'Richard Tea',
                'emails' => [
                    'richard.tea@example.com',
                ],
            ],
            [
                'name' => 'Fergus Douchebag', // Ya, this was randomly generated for me :)
                'emails' => [
                    'fergus@douchebag.com',
                ],
            ],
        ],
    ],
];

$collection = collect($data)->recursive();
@joserick
Copy link

joserick commented Jan 8, 2024

@cyppe Interesting, if you like, send me an example of your $data that you tried to use and since it has been 1 year since I did this, something may have changed in Laravel.

@Rikaelus
Copy link

Rikaelus commented May 1, 2024

I found this while researching a similar need and this is really good, but I ended up taking a slightly different approach that might be appealing to others who find their way here.

I opted for a helper function for IDE code prediction purposes but the real difference is trying to keep close to the core Collection transforming philosophy (which can collectivize various types) but then making most of it toggleable. I went ahead and threw in a depth option, too, after seeing @PascalHesselink's contribution here.

if (!function_exists('rCollect')){
    /**
     * Recursively convert all arrays to collections
     *
     * @param  array  $value
     * @param  bool  $array  True to convert array descendants
     * @param  bool  $enumerable  True to convert Enumerable descendants
     * @param  bool  $arrayable  True to convert Arrayable descendants
     * @param  bool  $traversable  True to convert Traversable descendants
     * @param  bool  $jsonable  True to convert Jsonable descendants
     * @param  bool  $jsonSerializable  True to convert JsonSerializable descendants
     * @param  bool  $unitEnum  True to convert UnitEnum descendants
     * @param  bool  $all  True to convert all eligible descendants
     * @param  int|null  $depth  Levels of children to traverse into
     * @param  int  $currentLayer  (do not set; used for internal depth tracking)
     * @return Collection
     */
    function rCollect(
        mixed $value = [],
        bool $array = true,
        bool $enumerable = false,
        bool $arrayable = false,
        bool $traversable = false,
        bool $jsonable = false,
        bool $jsonSerializable = false,
        bool $unitEnum = false,
        bool $all = false,
        ?int $depth = null,
        int $currentLayer = 0,
    ): Collection
    {
        // Because func_get_args omits defaults and ReflectionFunction is too bulky
        $args = [$array,$enumerable,$arrayable,$traversable,$jsonable,$jsonSerializable,$unitEnum,$all,$depth,$currentLayer+1];
        return collect($value)
            ->when(
                $depth === null || $currentLayer < $depth,
                fn(Collection $c) => $c->map(
                    fn($child) =>
                        (is_array($child) && ($all || $array)) ||
                        ($child instanceof Enumerable && ($all || $enumerable)) ||
                        ($child instanceof Arrayable && ($all || $arrayable)) ||
                        ($child instanceof Traversable && ($all || $traversable)) ||
                        ($child instanceof Jsonable && ($all || $jsonable)) ||
                        ($child instanceof JsonSerializable && ($all || $jsonSerializable)) ||
                        ($child instanceof UnitEnum && ($all || $unitEnum)) ?
                            rCollect($child, ...$args) :
                            $child
                )
            );
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment