Collections in Laravel are "hidden gems": not everyone is using them. They are especially effective when performing MULTIPLE operations with data - so-called "chains". I've gathered 15 real-life examples from open-source Laravel projects. The goal is not only to show the Collection methods but also the practical scenarios of WHEN to use them.
This long tutorial is a text-version of my video course with the same examples.
So, let's dive into examples, one by one, with the links to the original sources.
Let's start this tutorial about Laravel Collection Chains with a very simple example, with a chain of two methods. The goal here is to show permissions divided by a new HTML tag.
Code:
$role = Role::with('permissions')->first();$permissionsListToShow = $role->permissions ->map(fn($permission) => $permission->name) ->implode("<br>");
The initial value of $role->permissions
is permission objects, and we are interested only in the name
field.
Then we do map
through those permissions, where we new collection containing only the names.
And finally after implode()
we get:
"manage users<br>manage posts<br>manage comments"
The GitHub repository with code can be found here. Inspiration source: Bottelet/DaybydayCRM.
You have an array of some kinds of scores, and then you have the collection of the scores from the database or from elsewhere.
Code:
$hazards = [ 'BM-1' => 8, 'LT-1' => 7, 'LT-P1' => 6, 'LT-UNK' => 5, 'BM-2' => 4, 'BM-3' => 3, 'BM-4' => 2, 'BM-U' => 1, 'NoGS' => 0, 'Not Screened' => 0,]; $score = Score::all()->map(function($item) use ($hazards) { return $hazards[$item->field];})->max();
The initial value could be this:
And your goal is to find the highest value from that array, from those values of the scores.
After map()
, we return only the number from an array by score field.
And the last max()
only runs through all values and returns the maximum:
7
The GitHub repository with code can be found here. Inspiration source: Laracasts
Initial data comes from config which has a multi-dimentional array of settings. And our goal is to get the array of elements
.
Code:
// config/setting_fields.phpreturn [ 'app' => [ 'title' => 'General', 'desc' => 'All the general settings for application.', 'icon' => 'fas fa-cube', 'elements' => [ [ 'type' => 'text', // input fields type 'data' => 'string', // data type, string, int, boolean 'name' => 'app_name', // unique name for field 'label' => 'App Name', // you know what label it is 'rules' => 'required|min:2|max:50', // validation rule of laravel 'class' => '', // any class for input 'value' => 'Laravel Starter', // default value if you want ], // ... ], ], 'email' => [ 'title' => 'Email', 'desc' => 'Email settings for app', 'icon' => 'fas fa-envelope', 'elements' => [ [ 'type' => 'email', // input fields type 'data' => 'string', // data type, string, int, boolean 'name' => 'email', // unique name for field 'label' => 'Email', // you know what label it is 'rules' => 'required|email', // validation rule of laravel 'class' => '', // any class for input 'value' => 'info@example.com', // default value if you want ], ], ], 'social' => [ 'title' => 'Social Profiles', 'desc' => 'Link of all the social profiles.', 'icon' => 'fas fa-users', 'elements' => [ [ 'type' => 'text', // input fields type 'data' => 'string', // data type, string, int, boolean 'name' => 'facebook_url', // unique name for field 'label' => 'Facebook Page URL', // you know what label it is 'rules' => 'required|nullable|max:191', // validation rule of laravel 'class' => '', // any class for input 'value' => '#', // default value if you want ], // ... ], // ... ],]; $elements = collect(config('setting_fields')) ->pluck('elements') ->flatten(1);
First, we make a collection for the array by doing collect(config('setting_fields'))
.
After the pluck()
returns elements
, those arrays form another collection element
And then if we need to get rid of those indexes we can use flatten()
and that will give us:
The GitHub repository with code can be found here. Inspiration source: nasirkhan/laravel-starter.
Another example of two-method collection chains or in this case it's two chains by two methods. In the scenario, we have a few namespaces, where you can load Livewire components. And the goal is to have a class found in which namespace that is and then return the class name transformed into kebab case.
Code:
$class = 'App\\Base\\Http\\Livewire\\SomeClass';$classNamespaces = [ 'App\\Base\\Http\\Livewire', 'App\\Project\\Livewire']; $classNamespace = collect($classNamespaces)->filter(fn ($x) => strpos($class, $x) !== false)->first();$namespace = collect(explode('.', str_replace(['/', '\\'], '.', $classNamespace))) ->map([Str::class, 'kebab']) ->implode('.');
First, we need to find which namespace corresponds to that class. We collect all namespaces and then filter by whether it contains that class or not. And then we get the first of the match.
Value after filter():
Value after filter()->first():
"App\Base\Http\Livewire"
Then, we get that namespace, replace the slashes with the dot and explode that into an array and turn it into a new collection, and then map through that collection with a method. This is another way how you can use map()
. Not only by providing a callback function, but providing a method from a class.
Value after filter()->first()->map():
If any of those folder names are not corresponding to the kebab case they will be turned into a kebab case. In this case, it just goes into lowercase without any more transformations. And then we implode it back into one string:
Value after filter()->first()->map()->implode():
"app.base.http.livewire"
The GitHub repository with code can be found here. Inspiration source: iluminar/goodwork).
Now let's go one step higher and let's look at three methods of collection chains. A real-life example is the Twitter artisan giveaway command with the option to exclude some users.
Code:
$excluded = collect($this->option('exclude')) ->push('povilaskorop', '@dailylaravel') ->map(fn (string $name): string => str_replace('@', '', $name)) ->implode(', ');
Call with parameters:
php artisan twitter:giveaway --exclude=someuser --exclude=@otheruser
This $this->option('exclude')
is an array, and the initial value after making it into collection looks like this:
Initial value of collect($this->option('exclude')):
When we do push()
we add items to that collection.
Value after push():
And then finally we do map()
which is going through those items and replacing @ symbol with nothing.
Value after push()->map():
And then we implode with a comma to provide the result in a visual format to be shown somewhere.
Value after push()->map()->implode():
"someuser, otheruser, povilaskorop, dailylaravel"
The GitHub repository with code can be found here. Inspiration source: Gummibeer/gummibeer.de.
The next example is very similar but with a filter first and the example is different. So for example you have a User model with a lot of links to different social media profiles and you want to show those links as actual links separated by some parameter and also remove the empty ones.
Code:
$socialLinks = collect([ 'Twitter' => $user->link_twitter, 'Facebook' => $user->link_facebook, 'Instagram' => $user->link_instagram,])->filter()->map(fn ($link, $network) => '<a href="' . $link . '">' . $network . '</a>')->implode(' | ');
Initial value of collect():
One value, in this case, Facebook is empty, that's why we need filter()
. And filter()
may have a parameter of callback function of what to filter by, or just filter()
filters empty values.
Value after filter():
Then we map through those values and put them as links.
Value after filter()->map():
And then we do a familiar implode with a vertical bar symbol as a separator and then we can put this string as a part of the blade file.
Value after filter()->map()->implode():
"<a href="https://twitter.com/povilaskorop">Twitter</a> | <a href="https://instagram.com/povilaskorop">Instagram</a>"
The GitHub repository with code can be found here. Inspiration source: spatie/freek.dev.
The next example is repeating the same method twice. The task is you have a list of Models and you need to filter some objects out by some relationship condition, and then perform some job on each of them.
Code:
Repository::query() ->with('owner') ->get() ->reject(function (Repository $repository): bool { return $repository->owner instanceof User && $repository->owner->github_access_token === null; }) ->reject(function (Repository $repository): bool { return $repository->owner instanceof Organization && $repository->owner->members()->whereIsRegistered()->doesntExist(); }) ->each(static function (Repository $repository): void { UpdateRepositoryDetails::dispatch($repository); });
Why use reject twice? Of course, we could use one reject()
and then return condition and condition. But that would be more complicated to read. In this case, it's easier to read, reject the repository if the owner is User and User doesn't have a GitHub access token, or reject if the repository owner is Organization and members where is registered don't exist. It reads in plain English language.
Initial value of Repository::query()->with('owner')->get():
Value after first reject():
Value after second reject():
The GitHub repository with code can be found here. Inspiration source: Astrotomic/opendor.me.
A few new methods in this example. The task here is that you have log files and then you need to identify older ones than X days.
Code:
$files = Storage::disk("logs")->allFiles();$logFiles = collect($files) ->mapWithKeys(function ($file) { $matches = []; $isMatch = preg_match("/^laravel\-(.*)\.log$/i", $file, $matches); if (count($matches) > 1) { $date = $matches[1]; } $key = $isMatch ? $date : ""; return [$key => $file]; }) ->forget("") ->filter(function ($value, $key) use ($thresholdDate) { try { $date = Carbon::parse($key); } catch (\Exception $e) { return true; } return $date->isBefore($thresholdDate); });
We get all the log files and put them into the collection.
Initial value of collect($files):
Then we do a map but with keys. The goal is to return an array of [$date => $filename]
.
Value after mapWithKeys():
As you see there is one file without a date which we don't need to delete. For that, we use the forget()
method.
Value after mapWithKeys()->forget():
And finally, we do a filter of actually older files by key.
Value after mapWithKeys()->forget()->filter():
The GitHub repository with code can be found here. Inspiration source: opendialogai/opendialog.
The next example is a typical kind of form for example or social network. You have a comment and some users mentioned the @username
syntax. And from this string, you need to define those users and send a notification to them.
Code:
$comment = Comment::first();collect($comment->mentionedUsers()) ->map(function ($name) { return User::where('name', $name)->first(); }) ->filter() ->each(function ($user) use ($comment) { $user->notify(new YouWereMentionedNotification($comment)); });
Initial value of $comment->description:
"I mention the @First user and the @Second user and @Third non-existing one."
First, we get mentioned users into a collection. The mentionedUsers()
looks like this:
public function mentionedUsers(){ preg_match_all('/@([\w\-]+)/', $this->description, $matches); return $matches[1];}
Initial value of $comment->mentionedUsers():
Then we map through that collection and try to find a User with that name.
Value after map():
And then we filter null results.
Value after map()->filter():
And then we can send to every User notification using the each()
method.
The GitHub repository with code can be found here. Inspiration source: Bottelet/DaybydayCRM.
We have a lot of categories and we need to get the slugs of those categories with active language, but the categories may have ancestors. It's a tree of categories.
Code:
$locale = 'en';Category::all() ->map(function ($i) use ($locale) { return $i->getSlug($locale); }) ->filter() ->implode('/');
Initial value of Category::all():
Then we map through categories to get slug. The getSlug()
method looks like this:
public function getSlug($locale = null){ if (($slug = $this->getActiveSlug($locale)) != null) { return $slug->slug; } if (config('translatable.use_property_fallback', false) && (($slug = $this->getFallbackActiveSlug()) != null)) { return $slug->slug; } return "";} public function getActiveSlug($locale = null){ return $this->slugs->first(function ($slug) use ($locale) { return ($slug->locale === ($locale ?? app()->getLocale())) && $slug->active; }) ?? null;} public function getFallbackActiveSlug(){ return $this->slugs->first(function ($slug) { return $slug->locale === config('translatable.fallback_locale') && $slug->active; }) ?? null;}
Value after map():
Then we filter empty values.
Value after map()->filter():
And then we implode with a slash to create a URL.
Value after map()->filter()->implode():
"first-category/second-category/third-category"
The GitHub repository with code can be found here. Inspiration source: area17/twill.
This example is pretty similar to earlier seen reject + reject
. Here we have Post and we need to filter if Post is a Tweet. Then we need to filter that the external URL is empty. And then we fill external URL if we find twitter.com inside.
Code:
Post::all() ->filter->isTweet() ->filter(function (Post $post) { return empty($post->external_url); }) ->each(function (Post $post) { preg_match('/(?=https:\/\/twitter.com\/).+?(?=")/', $post->text, $matches); if (count($matches) > 0) { $post->external_url = $matches[0]; $post->save(); } });
Here what is interesting is you can use filter
like so filter->isTweet()
. And that isTweet()
method looks like this:
public function isTweet(): bool{ return $this->getType() === 'tweet';} public function getType(): string{ if ($this->hasTag('tweet')) { return 'tweet'; } if ($this->original_content) { return 'original'; } return 'link';} public function hasTag(string $tagName): bool{ return $this->tags->contains(fn (Tag $tag) => $tag->name === $tagName);}
Initial value of Post::all():
Now after the first filter isTweet()
:
One of them has an external URL null and another one has an external URL already filled. This is where another filter comes in to filter out Posts without external URLs.
The GitHub repository with code can be found here. Inspiration source: spatie/freek.dev.
Here we have some events. Every event has a message, status, and subject. Then we need to get the unique messages, filter the ones that don't have a subject and extract the data into another structure
Code:
$events = Event::all();$filteredEvents = $events ->unique(fn ($event) => $event->message) ->filter(fn ($event) => !is_null($event->subject)) ->map(fn ($event) => $this->extractData($event)) ->values();
Initial value of Event::all():
Events with ID 1 and 2 are the same, so using unique()
we remove duplicates from the collection.
Value after first unique():
The next one is filter()
which filters out the ones without the subject.
Value after ->unique()->filter():
The next one is map()
which gives a different structure of data here.
Value after ->unique()->filter()->map():
And the last one is to get the values and change the values of the array keys.
Value after ->unique()->filter()->map()->values():
The GitHub repository with code can be found here. Inspiration source: opendialogai/opendialog.
This example's goal is to log the last versions of Laravel and tell the users when was the last version when it is upgraded and stuff like that.
Code:
$versionsFromGithub = collect([ ['name' => 'v9.19.0'], ['name' => 'v8.83.18'], ['name' => 'v9.18.0'], ['name' => 'v8.83.17'], ['name' => 'v9.17.0'], ['name' => 'v8.83.16'], // ...]); $versionsFromGithub // Map into arrays containing major, minor, and patch numbers ->map(function ($item) { $pieces = explode('.', ltrim($item['name'], 'v')); return [ 'major' => $pieces[0], 'minor' => $pieces[1], 'patch' => $pieces[2] ?? null, ]; }) // Map into groups by release; pre-6, major/minor pair; post-6, major ->mapToGroups(function ($item) { if ($item['major'] < 6) { return [$item['major'] . '.' . $item['minor'] => $item]; } return [$item['major'] => $item]; }) // Take the highest patch or minor/patch number from each release ->map(function ($item) { if ($item->first()['major'] < 6) { // Take the highest patch return $item->sortByDesc('patch')->first(); } // Take the highest minor, then its highest patch return $item->sortBy([['minor', 'desc'], ['patch', 'desc']])->first(); }) ->each(function ($item) { if ($item['major'] < 6) { $version = LaravelVersion::where([ 'major' => $item['major'], 'minor' => $item['minor'], ])->first(); if ($version->patch < $item['patch']) { $version->update(['patch' => $item['patch']]); info('Updated Laravel version ' . $version . ' to use latest patch.'); } } $version = LaravelVersion::where([ 'major' => $item['major'], ])->first(); if (! $version) { // Create it if it doesn't exist $created = LaravelVersion::create([ 'major' => $item['major'], 'minor' => $item['minor'], 'patch' => $item['patch'], ]); info('Created Laravel version ' . $created); } // Update the minor and patch if needed else if ($version->minor != $item['minor'] || $version->patch != $item['patch']) { $version->update(['minor' => $item['minor'], 'patch' => $item['patch']]); info('Updated Laravel version ' . $version . ' to use latest minor/patch.'); } });
Initial values come from GitHub API and are put into the collection.
Initial value of $versionsFromGithub:
First, using map()
we get major, minor, and patch versions, and remove the v
from the start.
Value after map():
Then we map into groups, where the group is the major
version number.
Value after map()->mapToGroups():
And then we use map()
to return sorted results by minor and then by patch versions. And return only the first result which is the latest version.
Value after map()->mapToGroups()->map():
And then with each version, there's a huge operation that doesn't change the collection itself, but it checks if that version is in our database witch the major and minor versions. If it is, we update only the latest patch version. Otherwise, we create that Laravel version.
The GitHub repository with code can be found here. Inspiration source: tighten/laravelversions.
For example, we have two folders where the developer can put Livewire components. And we map through those folders to get the files. And the final result of that is the list of its items with a path name.
Code:
$folders = collect([ "/Users/Povilas/Sites/project3/app/Base/Http/Livewire/", "/Users/Povilas/Sites/project3/app/Project/Livewire/"]); $classNames = $folders ->map(function ($item) { return (new Filesystem())->allFiles($item); }) ->flatten() ->map(function (\SplFileInfo $file) { return app()->getNamespace().str_replace( ['/', '.php'], ['\\', ''], Str::after($file->getPathname(), app_path().'/') ); }) ->filter(function (string $class) { return is_subclass_of($class, Component::class) && ! (new \ReflectionClass($class))->isAbstract(); });
Value after map():
Then we do flatten()
without any parameters. We flatten all the files into one level.
Value after map()->flatten():
And then we map to get the data we want.
Value after map()->flatten()->map():
And the last filter if the class is a Livewire Component and not an abstract component.
Value after map()->flatten()->map()->filter():
The GitHub repository with code can be found here. Inspiration source: iluminar/goodwork.
This example gets all packages from the composer and filters them out in various ways. Here the goal is only to have Laravel packages. It is saved in $package['extra']['laravel']
.
Code:
if ($filesystem->exists($path = base_path() . '/vendor/composer/installed.json')) { $plugins = json_decode($filesystem->get($path), true);} $packages = collect($plugins['packages']) ->mapWithKeys(function ($package) { return [$this->format($package['name']) => $package['extra']['laravel'] ?? []]; }) ->each(function ($configuration) use (&$ignore) { $ignore = array_merge($ignore, $configuration['dont-discover'] ?? []); }) ->reject(function ($configuration, $package) use ($ignore) { return in_array($package, $ignore); }) ->filter() ->all();
Initial value of $plugins['packages']:
First, we simplify the array with mapWithKeys()
. We only need the package name and ['extra']['laravel']
values.
Value after mapWithKeys():
The next move is to remove packages with dont-discover
, and then reject()
all those packages from the list. Now we have fewer packages.
Value after mapWithKeys()->each()->reject():
But we have a lot of packages with empty parameters and the filter()
method without any parameters removes empty ones from the collection.
Value after mapWithKeys()->each()->reject()->filter():
And finally, we are doing all()
which transforms the collection into an array.
Value after mapWithKeys()->each()->reject()->filter()->all():
The GitHub repository with code can be found here. Inspiration source: iluminar/goodwork.
I hope these examples will give you the ideas how you can use Collections in your applications, especially in the chained way.