This title was written for Laravel version 4. If you're looking for material on the latest version of Laravel, then please check out Code Smart.
I love collections. I have far too many of them. As a kid I used to collect eggcups and transformer toys. As an adult I collect video games and manga comics. Nerdy collections are the best.
Laravel has its own collections too. It has a collection of wonderful fans, eager to help each other and develop the community. It has a collection of amazing developers contributing to it. It has a collection of stories about where the name came from, most of them false. It also has Eloquent Collections.
The Collection Class
Eloquent collections are an extension of Laravel's Collection
class with some handy methods for dealing with query results. The Collection
class itself, is merely a wrapper for an array of objects, but has a bunch of other interesting methods to help you pluck items out of the array.
In Laravel three, an array of model instances was returned from any query method that is used to provide multiple results. However, in Laravel four you will instead receive a Collection
of model instances. Don't worry, you can still iterate through a collection of results with the variety of loops that PHP offers because it inherits some properties of an array, but because the collection is a class, and not native type, there are also methods available on the object.
Let's take a look at the available methods of the Collection
class. For our sample data we will use the albums
table from the previous chapter, and assume that the collection is the result of a call to Album::all()
, like this.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
});
Collection Methods
Let's look at the methods available to the Collection
class. Some of the methods are related to inserting, and retrieving elements by their key. However, in the case of Eloquent results, the keys do not match the primary key of the tables that the model instances represent, and so these methods will not be very useful to us. Instead, I will cover the methods that do have some use.
All
The all()
method can be used to get hold of the internal array used by the Collection
object. This means that if you want your results to be identical to those supplied by Laravel 3, then just call the all()
method and you will have your instance array.
Let's var_dump()
the result.
Here's our code.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
var_dump($collection->all());
});
And here is the result.
array (size=6)
0 =>
object(Album)[127]
public 'timestamps' => boolean false
protected 'connection' => null
protected 'table' => null
protected 'primaryKey' => string 'id' (length=2)
protected 'perPage' => int 15
public 'incrementing' => boolean true
protected 'attributes' =>
array (size=5)
'id' => int 1
'title' => string 'Some Mad Hope' (length=13)
'artist' => string 'Matt Nathanson' (length=14)
'genre' => string 'Acoustic Rock' (length=13)
'year' => int 2007
protected 'original' =>
array (size=5)
'id' => int 1
'title' => string 'Some Mad Hope' (length=13)
'artist' => string 'Matt Nathanson' (length=14)
'genre' => string 'Acoustic Rock' (length=13)
'year' => int 2007
protected 'relations' =>
array (size=0)
empty
protected 'hidden' =>
array (size=0)
empty
protected 'visible' =>
array (size=0)
empty
protected 'fillable' =>
array (size=0)
empty
protected 'guarded' =>
array (size=1)
0 => string '*' (length=1)
protected 'touches' =>
array (size=0)
empty
protected 'with' =>
array (size=0)
empty
public 'exists' => boolean true
protected 'softDelete' => boolean false
1 =>
object(Album)[128]
... TONNES MORE INFO ...
As you can see, we have an array of our Eloquent model instances.
First
The first()
method of the collection can be used to retrieve the first element in the set. This will be the first element contained within the collections internal array.
Let's give it a go.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
var_dump($collection->first());
});
Now we visit the /
URI to view the result. What do you expect it will be?
object(Album)[127]
public 'timestamps' => boolean false
protected 'connection' => null
protected 'table' => null
protected 'primaryKey' => string 'id' (length=2)
protected 'perPage' => int 15
public 'incrementing' => boolean true
protected 'attributes' =>
array (size=5)
'id' => int 1
'title' => string 'Some Mad Hope' (length=13)
'artist' => string 'Matt Nathanson' (length=14)
'genre' => string 'Acoustic Rock' (length=13)
'year' => int 2007
protected 'original' =>
array (size=5)
'id' => int 1
'title' => string 'Some Mad Hope' (length=13)
'artist' => string 'Matt Nathanson' (length=14)
'genre' => string 'Acoustic Rock' (length=13)
'year' => int 2007
protected 'relations' =>
array (size=0)
empty
protected 'hidden' =>
array (size=0)
empty
protected 'visible' =>
array (size=0)
empty
protected 'fillable' =>
array (size=0)
empty
protected 'guarded' =>
array (size=1)
0 => string '*' (length=1)
protected 'touches' =>
array (size=0)
empty
protected 'with' =>
array (size=0)
empty
public 'exists' => boolean true
protected 'softDelete' => boolean false
That's right! It's a single model instance that represents one of our albums. The first row that we inserted into the table. Note that it's only the first row because we used the all()
method as part of the query. If we had used a different query, then the array in our result set might be of a different order and then a call to first()
could yield a different result.
Last
This one should be obvious. The first()
method was used to retrieve the first value contained within the collections internal array, this means that the last()
method must retrieve the last item of the array.
Let's prove this theory.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
var_dump($collection->last());
});
Here's the result from the /
URI.
object(Album)[138]
public 'timestamps' => boolean false
protected 'connection' => null
protected 'table' => null
protected 'primaryKey' => string 'id' (length=2)
protected 'perPage' => int 15
public 'incrementing' => boolean true
protected 'attributes' =>
array (size=5)
'id' => int 6
'title' => string '...Is A Real Boy' (length=16)
'artist' => string 'Say Anything' (length=12)
'genre' => string 'Indie Rock' (length=10)
'year' => int 2006
protected 'original' =>
array (size=5)
'id' => int 6
'title' => string '...Is A Real Boy' (length=16)
'artist' => string 'Say Anything' (length=12)
'genre' => string 'Indie Rock' (length=10)
'year' => int 2006
protected 'relations' =>
array (size=0)
empty
protected 'hidden' =>
array (size=0)
empty
protected 'visible' =>
array (size=0)
empty
protected 'fillable' =>
array (size=0)
empty
protected 'guarded' =>
array (size=1)
0 => string '*' (length=1)
protected 'touches' =>
array (size=0)
empty
protected 'with' =>
array (size=0)
empty
public 'exists' => boolean true
protected 'softDelete' => boolean false
Great, that's the last album contained within the internal array. It's also the last row of the database, but that just depends on the query we use to retrieve the collection.
Shift
The shift()
method is similar to the first()
method. It will retrieve the first value within the collections internal array. However, unlike the first()
method, the shift()
method will also remove that value from the array. Let's prove this with a little test.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
var_dump(count($collection));
var_dump($collection->shift());
var_dump(count($collection));
});
Our test will display the number of elements within the collection using the PHP count()
method before and after we have shift()
ed a value. Remember that the collection inherits many properties of an array, this allows the count()
method to act upon it.
Let's take a look at the result of our test.
int 6
object(Album)[127]
public 'timestamps' => boolean false
protected 'connection' => null
protected 'table' => null
protected 'primaryKey' => string 'id' (length=2)
protected 'perPage' => int 15
public 'incrementing' => boolean true
protected 'attributes' =>
array (size=5)
'id' => int 1
'title' => string 'Some Mad Hope' (length=13)
'artist' => string 'Matt Nathanson' (length=14)
'genre' => string 'Acoustic Rock' (length=13)
'year' => int 2007
protected 'original' =>
array (size=5)
'id' => int 1
'title' => string 'Some Mad Hope' (length=13)
'artist' => string 'Matt Nathanson' (length=14)
'genre' => string 'Acoustic Rock' (length=13)
'year' => int 2007
protected 'relations' =>
array (size=0)
empty
protected 'hidden' =>
array (size=0)
empty
protected 'visible' =>
array (size=0)
empty
protected 'fillable' =>
array (size=0)
empty
protected 'guarded' =>
array (size=1)
0 => string '*' (length=1)
protected 'touches' =>
array (size=0)
empty
protected 'with' =>
array (size=0)
empty
public 'exists' => boolean true
protected 'softDelete' => boolean false
int 5
As you can see, we receive our Album
model instance. It's the same as the one we received using the first()
method. However, if you look at the two integer values, you will noticed that the size of the array has decreased. This is because the instance has not only been returned from the method, but also removed from the array.
Pop
Pop is a genre of music that has been around for decades, and is generally used to encourage the consumption of alcohol, or the wild dreams of teenagers.
Oh yes, it's also a method on the Eloquent model instances collection. It works in a similar way to the shift()
method in that it will return the value from the end of the internal array, and remove it. Let's trap it's result with our test.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
var_dump(count($collection));
var_dump($collection->pop());
var_dump(count($collection));
});
Here is the result.
int 6
object(Album)[138]
public 'timestamps' => boolean false
protected 'connection' => null
protected 'table' => null
protected 'primaryKey' => string 'id' (length=2)
protected 'perPage' => int 15
public 'incrementing' => boolean true
protected 'attributes' =>
array (size=5)
'id' => int 6
'title' => string '...Is A Real Boy' (length=16)
'artist' => string 'Say Anything' (length=12)
'genre' => string 'Indie Rock' (length=10)
'year' => int 2006
protected 'original' =>
array (size=5)
'id' => int 6
'title' => string '...Is A Real Boy' (length=16)
'artist' => string 'Say Anything' (length=12)
'genre' => string 'Indie Rock' (length=10)
'year' => int 2006
protected 'relations' =>
array (size=0)
empty
protected 'hidden' =>
array (size=0)
empty
protected 'visible' =>
array (size=0)
empty
protected 'fillable' =>
array (size=0)
empty
protected 'guarded' =>
array (size=1)
0 => string '*' (length=1)
protected 'touches' =>
array (size=0)
empty
protected 'with' =>
array (size=0)
empty
public 'exists' => boolean true
protected 'softDelete' => boolean false
int 5
We receive the last element of the array, and the result from our count()
methods suggest that the length of the array has been decreased. This is because the value we received was removed from the internal array.
Each
If you have used the 'Underscore' library for Javascript or PHP then you will be familiar with the next few methods. Instead of creating a foreach()
loop to iterate over our results, we can instead pass a Closure to the each()
method.
This is best described with an example.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
$collection->each(function($album)
{
var_dump($album->title);
});
});
Our Closure accepts a parameter that will be a placeholder for the current object within the iteration. This closure is then passed to the each()
method. For each iteration, we will dump the value of the model's 'title' column.
Let's examine the result.
string 'Some Mad Hope' (length=13)
string 'Please' (length=6)
string 'Leaving Through The Window' (length=26)
string 'North' (length=5)
string '...Anywhere But Here' (length=20)
string '...Is A Real Boy' (length=16)
Brilliant! Those are our album titles as expected.
Map
The map()
function works in a similar way to the each()
method. However it can be used to iterate and work with our collection elements, returning a new collection as a result.
Let's imagine that we want to prefix all of our album titles with An ode to a fair panda:
. We can do this using the map()
function.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
$new = $collection->map(function($album)
{
return 'An ode to a fair panda: '.$album->title;
});
var_dump($new);
});
First we ensure that the value of Collection::map()
method is assigned to a variable. Then we iterate the collection in the same manner as the each()
method, but this time we return each value that we wish to be present within the new collection.
Here's the result.
object(Illuminate\Database\Eloquent\Collection)[117]
protected 'items' =>
array (size=6)
0 => string 'An ode to a fair panda: Some Mad Hope' (length=37)
1 => string 'An ode to a fair panda: Please' (length=30)
2 => string 'An ode to a fair panda: Leaving Through The Window' (length=50)
3 => string 'An ode to a fair panda: North' (length=29)
4 => string 'An ode to a fair panda: ...Anywhere But Here' (length=44)
5 => string 'An ode to a fair panda: ...Is A Real Boy' (length=40)
Now we have a collection of strings that we built using our iterative map()
method.
Filter
The filter()
method can be used to reduce the number of elements contained within the resulting collection by using a Closure. If the result of the Closure is boolean true
then the current element of the iteration will be given to the resulting collection. If the Closures iteration returns a false
or nothing at all, then that element will not exist within the new collection.
This might be easier to understand with an example.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
$new = $collection->filter(function($album)
{
if ($album->artist == 'Something Corporate') {
return true;
}
});
var_dump($new);
});
We iterate the collection with the filter()
method and our Closure. For each iteration, if the value of the artist
column on our model instance is equal to 'Something Corporate' then we will return true. This indicates that the model instance should be present within the new collection.
Here's the result.
object(Illuminate\Database\Eloquent\Collection)[117]
protected 'items' =>
array (size=2)
2 =>
object(Album)[135]
public 'timestamps' => boolean false
protected 'connection' => null
protected 'table' => null
protected 'primaryKey' => string 'id' (length=2)
protected 'perPage' => int 15
public 'incrementing' => boolean true
protected 'attributes' =>
array (size=5)
'id' => int 3
'title' => string 'Leaving Through The Window' (length=26)
'artist' => string 'Something Corporate' (length=19)
'genre' => string 'Piano Rock' (length=10)
'year' => int 2002
protected 'original' =>
array (size=5)
'id' => int 3
'title' => string 'Leaving Through The Window' (length=26)
'artist' => string 'Something Corporate' (length=19)
'genre' => string 'Piano Rock' (length=10)
'year' => int 2002
3 =>
object(Album)[136]
public 'timestamps' => boolean false
protected 'connection' => null
protected 'table' => null
protected 'primaryKey' => string 'id' (length=2)
protected 'perPage' => int 15
public 'incrementing' => boolean true
protected 'attributes' =>
array (size=5)
'id' => int 4
'title' => string 'North' (length=5)
'artist' => string 'Something Corporate' (length=19)
'genre' => string 'Piano Rock' (length=10)
'year' => int 2002
protected 'original' =>
array (size=5)
'id' => int 4
'title' => string 'North' (length=5)
'artist' => string 'Something Corporate' (length=19)
'genre' => string 'Piano Rock' (length=10)
'year' => int 2002
I shortened the results a little to save space, but you can clearly see that we have our two albums by 'Something Corporate'.
Sort
The sort()
method can be used to sort the collection. It hands our Closure to the uasort()
PHP method which uses integer values to represent a comparison between two values. Our closure receives two parameters, let's call them A and B. Then we apply the rules of our sorting to provide an integer result from our closure.
If A > B then we return 1.
If A < B then we return -1.
If A = B then we return 0.
Here's an example.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
$collection->sort(function($a, $b)
{
$a = $a->year;
$b = $b->year;
if ($a === $b) {
return 0;
}
return ($a > $b) ? 1 : -1;
});
$collection->each(function($album)
{
var_dump($album->year);
});
});
We provide a closure with two parameters, these represent any two albums from the collection. First we assign $a
and $b
to the year
column to simplify our comparison code. If $a
is equal to $b
we return 0
. If $a
is greater than $b
we return 1
, otherwise -1
.
This method is descructive. It alters the original collection. We iterate through the collection dumping the year values to show the now order. Here is the result.
int 1993
int 1997
int 2002
int 2002
int 2006
int 2007
As you can see, our albums are now ordered by year
in ascending order. Here's some homework for you. Try to modify the above example changing only a single character so that the album years are instead presented in descending order. I promise it can be done!
Reverse
The reverse()
method can be used to reverse()
the models contained within the internal array. Does this really require an example? Oh go on then, you are wonderfully handsome readers after all...
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
$collection->each(function($album)
{
var_dump($album->title);
});
$reverse = $collection->reverse();
$reverse->each(function($album)
{
var_dump($album->title);
});
});
First we iterate through all of the albums outputting their title. Next we reverse the collection, assigning the now collection to the $reverse
variable. We then iterate through the $reverse
collection to see what has changed.
Here is the result.
string 'Some Mad Hope' (length=13)
string 'Please' (length=6)
string 'Leaving Through The Window' (length=26)
string 'North' (length=5)
string '...Anywhere But Here' (length=20)
string '...Is A Real Boy' (length=16)
string '...Is A Real Boy' (length=16)
string '...Anywhere But Here' (length=20)
string 'North' (length=5)
string 'Leaving Through The Window' (length=26)
string 'Please' (length=6)
string 'Some Mad Hope' (length=13)
Great! Our collection has been reversed.
Merge
The merge()
method can be used to combine two collections. The only parameter to the method is the collection that should be merged. Here's an example.
<?php
// app/routes.php
Route::get('/', function()
{
$a = Album::where('artist', '=', 'Something Corporate')
->get();
$b = Album::where('artist', '=', 'Matt Nathanson')
->get();
$result = $a->merge($b);
$result->each(function($album)
{
echo $album->title.'<br />';
});
});
In the above example we perform two queries. Result set $a
is a collection of albums by 'Something Corporate'. Set $b
contains albums by 'Matt Nathanson'. We use the merge()
method to merge the collections, and assign the result to a new collection named $result
. Then we echo the album titles within an each()
loop.
Here are the results.
Leaving Through The Window
North
Some Mad Hope
Please
Slice
The slice()
method is the equivelent of the PHP slice()
function. It can be used to produce a subset of models using a collection offset. Confused? Take a look at this.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
$sliced = $collection->slice(2, 4);
$sliced->each(function($album)
{
echo $album->title.'<br />';
});
});
We create a new collection using the slice()
method. The first parameter is the position that the new sub set is started. Here we are telling it to start slicing at the second element of the array. The second optional parameter is the length of the collection. We tell the slice()
method that we want four elements, after the second element in the array.
Let's take a look at the result.
Leaving Through The Window
North
...Anywhere But Here
...Is A Real Boy
Did you know that you can pass a negative value to slice()
? For example...
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
$sliced = $collection->slice(-2, 4);
$sliced->each(function($album)
{
echo $album->title.'<br />';
});
});
By passing -2
as the first parameter, we are telling it to start the collection two elements from the end of the collection. Here's the result.
...Anywhere But Here
...Is A Real Boy
C> Wait, didn't we tell it to retrieve four models?
We did, however since we are positioned two elements from the end of the array, only two elements are available to retrieve. The slice()
method does not wrap around the collection.
IsEmpty
The isEmpty()
method can be used to check whether or not the container has elements within it. I bet you didn't see that coming! It accepts no value, and returns a boolean result. Here's an example.
<?php
// app/routes.php
Route::get('/', function()
{
// This query will return items.
$a = Album::all();
// This query won't.
$b = Album::where('title', '=', 'foo')->get();
var_dump($a->isEmpty());
var_dump($b->isEmpty());
});
Aha, a cunning trap! We know that the first query will return results, and that the second query will not. Let's dump the result of the isEmpty()
method for both to see what we get.
boolean false
boolean true
The first isEmpty()
returns a boolean false
because the array has elements within it. The second collection is empty, and the isEmpty()
method returns a boolean true
.
ToArray
The toArray()
method can be used to return the internal array of the collection. Also, any elements within the array that that can be transformed into an array, for example objects, will be transformed during the process.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
var_dump( $collection->toArray() );
});
Here's our result.
array (size=6)
0 =>
array (size=5)
'id' => int 1
'title' => string 'Some Mad Hope' (length=13)
'artist' => string 'Matt Nathanson' (length=14)
'genre' => string 'Acoustic Rock' (length=13)
'year' => int 2007
1 =>
array (size=5)
'id' => int 2
'title' => string 'Please' (length=6)
'artist' => string 'Matt Nathanson' (length=14)
'genre' => string 'Acoustic Rock' (length=13)
'year' => int 1993
2 =>
array (size=5)
'id' => int 3
'title' => string 'Leaving Through The Window' (length=26)
'artist' => string 'Something Corporate' (length=19)
'genre' => string 'Piano Rock' (length=10)
'year' => int 2002
... and more ...
As you can see, not only has an array representing our collection been returned, but the model instances held within have also been transformed into arrays.
ToJson
The toJson()
method will tranform the collection into a JSON string that can be used to represent it's contents. In the previous chapter we discovered how to return collections directly from a routed closure or controller action to serve a JSON response. The toString()
method allowing the collection to be transformed to JSON makes a call to the toJson()
method internally.
Let's take a look at an example.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
var_dump( $collection->toJson() );
});
Here is the result from the above example.
string '[{"id":1,"title":"Some Mad Hope","artist":"Matt Nathanson","genre":"Acoustic Rock","year":2007},{"id":2,"title":"Please","artist":"Matt Nathanson","genre":"Acoustic Rock","year":1993},{"id":3,"title":"Leaving Through The Window","artist":"Something Corporate","genre":"Piano Rock","year":2002},{"id":4,"title":"North","artist":"Something Corporate","genre":"Piano Rock","year":2002},{"id":5,"title":"...Anywhere But Here","artist":"The Ataris","genre":"Punk Rock","year":1997},{"id":6,"title":"...Is A Real Boy","artist":"Say Anything","genre":"Indie Rock","year":2006}]' (length=570)
It's our entire collection represented by a JSON string.
Count
In an earlier example I used the PHP count()
method to count the number of model instances contained within the collection's internal array. How silly of me! I had completely forgotten about the count()
method of the collection. In honesty, it does the same thing. Let me show you.
<?php
// app/routes.php
Route::get('/', function()
{
$collection = Album::all();
var_dump( $collection->count() );
});
Here's the result. I bet you can't wait!
int 6
So the result is the same, which should we use? Whichever ever one suits us best. I leave the decision up to you. You are over 300 pages into the book by now, and are old and wise. Still handsome though. Well done you!
Best Practice
Some of the methods available to the collection are duplicates of those available on the query builder. For example, we can use the first()
method when building eloquent queries to retrieve only a single model instance. We can also use the first()
method of the collection to retrieve the first element contained within. Take a close look at the following example.
Album::all()->first();
Album::first();
These two lines both arrive at the same result. The first item within the albums
table will be returned as a result. So which should we use?
Well, as always, the answer is 'it depends'. Yes, I know that nobody likes to hear that reply, but sometimes it's true. Let's take a look at two different scenarios.
In the first scenario, we wish to display the title of the first album stored within the database within one of our templates. Sure, not a problem! We can use the first()
method on the Eloquent query builder.
Album::first();
In the second scenario, we wish to display a listing of all album titles, but also display the title of the first album within it's own little box. For example, the 'album of the year' section. Well, we could do something like this I suppose.
$allAlbums = Album::all();
$albumOfTheYear = Album::first();
I'm sure that would do what we need, but this method will run two queries against the database. Increasing the number of queries sent to the database server is a way of rapidly decreasing application performance. It might not matter within our examples, but within your enterprise software every split second wasted is money lost.
We could alleviate some stress upon the database server, and reduce the number of queries to one by shifting some of the query responsibilities to the collection.
$allAlbums = Album::all();
$albumOfTheYear = $allAlbums->first();
We retrieve all of our albums as usual, but this time we use the first()
method on the collection to retrieve the album of the year. This results in only a single query.
Choosing whether to perform a query on the collection, or on the database is a matter of choice. Querying against the database will allow for faster and more complex searches that would result in high CPU / memory usage on the webserver if performed on the collection. Then again, using the collection effectively can also benefit your application with fewer individual SQL queries.
Use your best judgement to decide when to use the collection helper methods. I trust you!
My books are available online for free to encourage learning. However, if you'd like for me to keep writing, then please consider buying a digital copy over at Leanpub.com.
It's available in PDF, ePub, and Kindle format, and contains a bunch of extras that you won't find on the site. I have a full-time job, and I write my books in my spare time. Please consider buying a copy so that I can continue to write new books from the comfort of my sofa!