Skip to content

Instantly share code, notes, and snippets.

@brunogaspar
Last active November 2, 2023 16:48
Star You must be signed in to star a gist
Embed
What would you like to do?
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();
@medteck
Copy link

medteck commented Jun 22, 2018

Had to deal with a large amount of recursive JSON data! This fixed my problem.
Thanks 👍

@adrienne
Copy link

Please consider submitting a pull request to these folks: https://github.com/spatie/laravel-collection-macros who have a super handy collection of macros that this would be a great fit for!

@roni-estein
Copy link

Thx

@marcheffels
Copy link

Thx

@Mombuyish
Copy link

Thanks!

@brunogaspar
Copy link
Author

Hey peeps!

Since i've saw a bit of "demand" for this helper/macro to be in a more installable way and considering Spatie refused a few pull requests to add this macro, i've decided to create my own package.

https://github.com/werxe/laravel-collection-macros

Feel free to use it and suggest new macros, i'm sure i'll be adding a few more.

Thanks!

@jonaslm
Copy link

jonaslm commented Oct 17, 2019

We wanted something similar, but for it to leave standard objects alone (because $object->foo is nicer than $object->get('foo') or $object['foo']). We also rewrote it as a helper method instead of a Collection macro, since it's more like the collect() helper method than something you do for an existing Collection.

If anyone else has the same needs, here you go:

function recursivelyCollect($item) {
	if (is_array($item)) {
		return recursivelyCollect(collect($item));
	} elseif ($item instanceof Collection) {
		$item->transform(static function ($collection) {
			return recursivelyCollect($collection);
		});
	} elseif (is_object($item)) {
		foreach ($item as $key => &$val) {
			$item->{$key} = recursivelyCollect($val);
		}
	}
	return $item;
}

A tinker example based on the one given in the original gist:

>>> $data
=> [
     {#3989
       +"name": "John Doe",
       +"emails": [
         "john@doe.com",
         "john.doe@example.com",
       ],
       +"contacts": [
         {#3992
           +"name": "Richard Tea",
           +"emails": [
             "richard.tea@example.com",
           ],
         },
         {#3990
           +"name": "Fergus Douchebag",
           +"emails": [
             "fergus@douchebag.com",
           ],
         },
       ],
     },
   ]
>>> recursivelyCollect($data);
=> Illuminate\Support\Collection {#3942
     all: [
       {#3989
         +"name": "John Doe",
         +"emails": Illuminate\Support\Collection {#3975
           all: [
             "john@doe.com",
             "john.doe@example.com",
           ],
         },
         +"contacts": Illuminate\Support\Collection {#3971
           all: [
             {#3992
               +"name": "Richard Tea",
               +"emails": Illuminate\Support\Collection {#3974
                 all: [
                   "richard.tea@example.com",
                 ],
               },
             },
             {#3990
               +"name": "Fergus Douchebag",
               +"emails": Illuminate\Support\Collection {#3972
                 all: [
                   "fergus@douchebag.com",
                 ],
               },
             },
           ],
         },
       },
     ],
   }

@lorisleiva
Copy link

Thanks, that was very helpful! 👍

I've tweaked the macro so that it wraps the $value using the lowest inheritance class.

Collection::macro('recursive', function () {
    return $this->map(function ($value) {
        return is_array($value) || is_object($value)
            ? (new static($value))->recursive()     // <- new static() instead of collect()
            : $value;
    });
});

For example, if you extend the Collection class like this:

class Knowledge extends Collection
{
    // ...
}

Then you can recursively wrap a nested array like that:

Knowledge::wrap($array)->recursive();

And now all nested collections will be instances of Knowledge.

@brunogaspar
Copy link
Author

No problem, glad you found it helpful!

@amirasyraf
Copy link

Thanks a lot for this.

@wivaku
Copy link

wivaku commented Nov 29, 2020

Nice! Is it possible to get combination of the 1) the original macro, 2) @lorisleiva 's tweak and 3) the modification by @jonaslm ?

This so I can use e.g.

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

(instead of the original macro: $collection->first()->get('name'))

@Tantalump
Copy link

@wivaku Can you share your code? Thanks.

@joserick
Copy link

A thousand years later... Requested by @wivaku:

Combination of the 1) the original macro @brunogaspar, 2) @lorisleiva 's tweak and 3) the modification by @jonaslm.

Collection::macro('recursive', function () {
    return $this->whenNotEmpty($recursive = function ($item) use (&$recursive) {
        if (is_array($item)) {
            return $recursive(new static($item));
        } elseif ($item instanceof Collection) {
            $item->transform(static function ($collection, $key) use ($recursive, $item) {
                return $item->{$key} = $recursive($collection);
            });
        } elseif (is_object($item)) {
            foreach ($item as $key => &$val) {
                $item->{$key} = $recursive($val);
            }
        }
        return $item;
    });
});
$collection = collect($data)->recursive();
$collection->first()->name; // John Doe

If anyone has any ideas on how to reduce it (less code) I would appreciate it.

@wivaku @Tantalump

@wivaku
Copy link

wivaku commented May 20, 2021

Excellent, thanks @joserick !

@77media-creations
Copy link

@joserick Nice, this works in laravel 8.

@dani0332
Copy link

dani0332 commented Sep 25, 2021

Resolved

Should have added \Illuminate\Support\Collection at $item instanceof Collection


@joserick Strangely though It doesn't work for me. But the original macro works/

$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();
  dd($collection->first()->name);

And the macro is

        \Illuminate\Support\Collection::macro('recursive', function () {
            return $this->whenNotEmpty($recursive = function ($item) use (&$recursive) {
                if (is_array($item)) {
                    return $recursive(new static($item));
                } elseif ($item instanceof Collection) { // <-- Should have added \Illuminate\Support\Collection
                    $item->transform(static function ($collection, $key) use ($recursive, $item) {
                        return $item->{$key} = $recursive($collection);
                    });
                } elseif (is_object($item)) {
                    foreach ($item as $key => &$val) {
                        $item->{$key} = $recursive($val);
                    }
                }
                return $item;
            });
        });

on dd($collection->first()); means its making it an array

array:3 [
  "name" => "John Doe"
  "emails" => array:2 [
    0 => "john@doe.com"
    1 => "john.doe@example.com"
  ]
  "contacts" => array:2 [
    0 => array:2 [
      "name" => "Richard Tea"
      "emails" => array:1 [
        0 => "richard.tea@example.com"
      ]
    ]
    1 => array:2 [
      "name" => "Fergus Douchebag"
      "emails" => array:1 [
        0 => "fergus@douchebag.com"
      ]
    ]
  ]
]

@PascalHesselink
Copy link

Collect Recursive with depth:

\Illuminate\Support\Collection::macro('recursive', function ($depth = null, $currentLayer = 1) {
    return $this->map(function ($value) use ($depth, $currentLayer) {
        if ((isset($depth) && $depth <= $currentLayer) || !(is_array($value) || is_object($value))) return $value;

        return collect($value)->recursive($depth, ($currentLayer + 1));
    });
});

@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

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