Skip to content

Instantly share code, notes, and snippets.

@brunogaspar
Last active September 15, 2024 18:42
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();
@brunogaspar
Copy link
Author

@PascalHesselink Pretty cool! What was the main use case for you?

@PascalHesselink
Copy link

@brunogaspar I needed to connect/compare some values by filtering and remapping them. But it's kinda unnecessary to apply 30 layers deep a recursive collection for me. So that's why I created a depth function 😁

@marcomessa
Copy link

@brunogaspar thank you for sharing!!

@bhaidar
Copy link

bhaidar commented Dec 21, 2022

Hey @brunogaspar, I'm trying to understand how this recursive macro works internally.
When you call recursive() on a collection, the first layer of arrays gets converted to collections. The inner layers get converted when for example, mapping or printing (toArray) the entire collection.

Maybe I'm missing something here.

When a value is an array or object, you return collect($value)->recursive(). This doesn't run right away, correct?


Oh I just got it! When you return the collect($value)->recursive() it actually runs the recursive() function on the $value. The 4value gets converted to a collection and all its children if any an array, will also get converted to a collection

Thanks

@joserick
Copy link

I hope this new function can help you in something @bhaidar .

Combination of "Collect Recursive with depth" and "Access collect items as properties" (improved*)

// Add a new method called "recursive" to the Collection class.
\Illuminate\Support\Collection::macro('recursive', function (int $depth = null) {
    // Use the map method to iterate over the items in the collection.
    return $this->map(function ($item) use ($depth) {
        // If the depth is 0 or the item is not a collection, array, or object, return the item as-is.
        if (($depth === 0) || !($item instanceof \Illuminate\Support\Collection || is_array($item) || is_object($item))) {
            return $item;
        }

        // Create a new anonymous class that extends the Collection class and overrides the __get and __set magic methods.
        // To be able to access the collection items as if they were properties of a object.
        return (new class(new static($item)) extends \Illuminate\Support\Collection {
            public function __get($key) { return $this->get($key); }
            public function __set($key, $value) { $this->put($key, $value); }
        })->recursive($depth - 1); // Apply the "recursive" method to the new Collection instance.
    });
});

Access item "name" as a property:

$collection = collect($data)->recursive();
$collection->first()->name; // John Doe

@cyppe
Copy link

cyppe commented Jan 7, 2024

joserick That macro does not work at all for me. At least not in Laravel 10.

No array key, even first level, is accessible as property. But I love the idea.

$product = collect($product)->recursive();
dump($product);
dd($product->product_key);

Output:
Illuminate\Support\Collection {#268554
  #items: array:88 [
    "product_key" => "p-46844-se"
     ....

And exception on the attempt to access $product->product_key:

Property [product_key] does not exist on this collection instance.

Do you maybe have some idea why it does not work? Or I maybe misunderstood the use case.

@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