Martin Joo - Laravel Eloquent Recipes
Martin Joo - Laravel Eloquent Recipes
Introduction
This short PDF is all about Laravel Eloquent. We love it, we use it, but there are a lot of hidden gems!
For example:
On the following pages, you can read about my 39 favorite Eloquent recipes.
I also released other high-rated materials, you can find them here.
1 / 38
Martin Joo - Laravel Eloquent Recipes
You can pass a condition, and if it's false, Laravel won't throw exceptions. It's a "best practice" to only
disable it in development environment, so your server won't explode.
2 / 38
Martin Joo - Laravel Eloquent Recipes
Accessing the latest_comment property will run an additional query so you'll end up with an N+1 query
problem. Of course you can prevent it with the shouldBeStrict method:
If you want to avoid N+1 queries in projects where you cannot turn these features on check out Laracheck.
3 / 38
Martin Joo - Laravel Eloquent Recipes
Post"#where('author_id', $authorId)
"$withTrashed();
You probably already knew that. But did you know you can use the function with an entire route?
In this case route model binding will query not just active but also soft-deleted records.
Since Laracel 9.35 we can use this with resource routes as well:
Route"#resource('posts', PostController"#class)
"$withTrashed();
Now withTrashed will apply to every method that has a post parameter. In fact, you can even tell Laravel in
which method you want to use it:
Route"#resource('posts', PostController"#class)
"$withTrashed(['show']);
4 / 38
Martin Joo - Laravel Eloquent Recipes
class OrderController
{
public function index()
{
return Benchmark"#measure(fn () "% Order:all());
}
}
It'll run the callback function and then prints out the time required to run it. You can also measure multiple
tasks:
return Benchmark"#measure([
fn () "% Order:all(),
fn () "% Order"#where('status', 'pending')"$get(),
]);
$user = User"#first();
$user"$secret ""& null;
This query will return null despite the fact that it's a select * query. It's because password is invisible.
5 / 38
Martin Joo - Laravel Eloquent Recipes
$user = User"#select('password')"$first();
$user"$password ""' null;
Password
Tokens
API keys
Payment-related information
6 / 38
Martin Joo - Laravel Eloquent Recipes
saveQuietly
If you ever need to save a model but you don't want to trigger any model events, you can use this method:
$user = User"#first();
$user"$name = 'Guest User';
$user"$saveQuietly();
Migrations
Models
This is a well-known feature. The status column will have a default draft value.
In this case, the status will be null , because it's not persisted yet. And sometimes it causes annoying null
value bugs. But fortunately, you can specify default attribute values in the Model as well:
7 / 38
Martin Joo - Laravel Eloquent Recipes
You can use these two approaches together and you'll never have a null value bug again.
Attribute Cast
Before Laravel 8.x we wrote attribute accessors and mutators like these:
It's not bad at all, but as Taylor says in the pull request:
This aspect of the framework has always felt a bit "dated" to me. To be honest, I think it's one of the
least elegant parts of the framework that currently exists. First, it requires two methods. Second, the
framework does not typically prefix methods that retrieve or set data on an object with get and set
8 / 38
Martin Joo - Laravel Eloquent Recipes
use Illuminate\Database\Eloquent\Casts\Attribute;
In this example, I used PHP 8 named arguments (the get and set before the functions).
find
Everyone knows about the find method, but did you know that it accepts an array of IDs?
So instead of this:
$users = User"#find($ids);
9 / 38
Martin Joo - Laravel Eloquent Recipes
Get Dirty
In Eloquent you can check if a model is "dirty" or not. Dirty means it has some changes that are not
persisted yet:
$user = User"#first();
$user"$name = 'Guest User';
The isDirty simply returns a bool while the getDirty returns every dirty attribute.
push
Sometimes you need to save a model and its relationship as well. In this case, you can use the push
method:
$employee = Employee"#first();
$employee"$name = 'New Name';
$employee"$address"$city = 'New York';
$employee"$push();
In this case the, save would only save the name column in the employees table but not the city column in
the addresses table. The push method will save both.
10 / 38
Martin Joo - Laravel Eloquent Recipes
For example, if you have models with slug, you don't want to rewrite the slug creation logic in every model.
Instead, you can define a trait, and use the creating event in the boot method:
trait HasSlug
{
public static function bootHasSlug()
{
static"#creating(function (Model $model) {
$model"$slug = Str"#slug($model"$title);
});
}
}
So you need to define a bootTraitName method, and Eloquent will automatically call this when it's booting
a model.
updateOrCreate
Creating and updating a model often use the same logic. Fortunately Eloquent provides a very convenient
method called updateOrCreate :
$flight = Flight"#updateOrCreate(
['id' "% $id],
['price' "% 99, 'discounted' "% 1],
);
The first one is used to determine if the model exists or not. In this example, I use the id .
The second one is the attributes that you want to insert or update.
If a Flight is found based on the given id it will be updated with the second array.
If there's no Flight with the given id it will be inserted with the second array.
I want to show you a real-world example of how I handle creating and updating models.
11 / 38
Martin Joo - Laravel Eloquent Recipes
The Controller:
As you can see I often extract a method called upsert . This method accepts a Department . In the store
method I use an empty Department instance because in this case, I don't have a real one. But in the
update I pass the currently updated instance.
12 / 38
Martin Joo - Laravel Eloquent Recipes
class UpsertDepartmentAction
{
public function execute(
Department $department,
DepartmentData $departmentData
): Department {
return Department"#updateOrCreate(
['id' "% $department"$id],
$departmentData"$toArray(),
);
}
}
It takes a Department which is the model (an empty one, or the updated one), and a DTO (a simple object
that holds data). In the first array I use the $department->id which is:
And the second argument is the DTO as an array, so the attributes of the Department .
upsert
Just for confusion Laravel uses the word upsert for multiple update or create operations. This is how it
looks:
Flight"#upsert([
['departure' "% 'Oakland', 'destination' "% 'San Diego', 'price' "% 99],
['departure' "% 'Chicago', 'destination' "% 'New York', 'price' "% 150]
], ['departure', 'destination'], ['price']);
Insert or update a flight from Oakland to San Diego with the price of 99
Insert or update a flight from Chicago to New York with the price of 150
13 / 38
Martin Joo - Laravel Eloquent Recipes
when
We often need to append a where clause to a query based on some conditional, for example, a Request
parameter. Instead of if statements you can use the when method:
User"#query()
"$when($request"$searchTerm, function ($query) {
$query"$where('username', 'LIKE', "%$request"$searchTerm%");
})
"$get();
It will only run the callback if the first parameter returns true.
$query = User"#query();
if ($request"$searchTerm) {
$query"$where('username', 'LIKE', "%$request"$searchTerm%");
}
return $query"$get();
14 / 38
Martin Joo - Laravel Eloquent Recipes
appends
If you have an attribute accessor and you often need it when the model is converted into JSON you can use
the $appends property:
Now the current_price column will be appended to the Product model every time it gets converted into
JSON. It's useful when you're working with Blade templates. With APIs, I would stick to Resources.
15 / 38
Martin Joo - Laravel Eloquent Recipes
Relationship Recipes
whereRelation
Imagine you're working on a financial application like a portfolio tracker, and you have the following models:
Stock: each company has a stock with a ticker and a current price.
Holding: each holding has some stocks in their portfolios. It has columns like invested_capital,
market_value
A Holding belongs to a Stock and a Stock has many Holdings. You can write a simple join to get every Apple
holdings, for example:
$apple = Holding"#select('holdings.*')
"$leftJoin('stocks', 'stocks.id', 'holdings.stock_id')
"$where('stocks.ticker', 'AAPL')
"$get();
It reads like this: give every Holdings where the Stock relation's ticker column is equal to AAPL (this is the
ticker symbol of Apple). Under the hood, it will run an EXISTS query. So if you actually need data from the
stocks table it's not a good solution.
16 / 38
Martin Joo - Laravel Eloquent Recipes
whereBelongsTo
Consider this snippet:
oldestOfMany
There's a special relationship called oldestOfMany . You can use it if you constantly need the oldest model
from a hasMany relationship.
In this example, we have an Employee and a Paycheck model. An employee has many paychecks, so this is
the basic relationship:
If you want to get the oldest paycheck you don't have to write a custom query every time, you can create a
relationship for it:
17 / 38
Martin Joo - Laravel Eloquent Recipes
In this case, you have to use the hasOne method because it will return only one paycheck, the oldest one.
The oldest paycheck is the one with the smallest auto-increment ID. So it won't work if you are using UUIDs
as foreign keys.
latestOfMany
Similarly to oldestOfMany we can use the newestOfMany as well:
The latest paycheck is the one with the largest auto-increment ID.
18 / 38
Martin Joo - Laravel Eloquent Recipes
ofMany
You can also use the ofMany relationship with some custom logic, for example:
So instead of writing a custom query every time, you can use these relationships:
oldestOfMany
latestOfMany
ofMany
hasManyThrough
Often we have relationships like $parent->child->child . For example, a department has employees, and
each employee has paychecks. This is a simple hasMany relationship:
19 / 38
Martin Joo - Laravel Eloquent Recipes
$department"$employees"$paychecks;
Instead of this, you can define a paychecks relationship on the Department model with the
hasManyThrough :
The second argument is the "through" model. And now you simply write:
$department"$paychecks;
20 / 38
Martin Joo - Laravel Eloquent Recipes
hasManyDeep
Okay, this is clickbait, because there is no hasManyDeep relationship in Laravel but there is an excellent
package called eloquent-has-many-deep.
Country -> has many -> User -> has many -> Post -> has many -> Comment
If you try to get every comment from a country with Eloquent it will cost you around 1 billion database
queries. But with this package you can query every comment in one query using Eloquent:
So it can handle additional levels of relationships. Under the hood it uses subqueries.
21 / 38
Martin Joo - Laravel Eloquent Recipes
withDefault
Let's say you have a Post model in your application that has an Author relationship which is a User model.
Users can be deleted but often we need to keep their data. This means Author is a nullable relationship, and
probably cause some code like this:
$authorName = $post"$author
? $post"$author"$name
: 'Guest Author';
We obviously want to avoid code like this. Fortunately PHP provides us the nullable operator, so we can
write this:
The hardcoded Guest Author seems like an anti-pattern, and you have to write this snippet every time you
need the author's name. In this situation you can use the withDefault :
If the author_id in the Post is null , the author relationship will not return null but a new User
model.
The new User model's name is Guest User .
22 / 38
Martin Joo - Laravel Eloquent Recipes
ratings as average_rating
Product"#with('category:id,name')"$get();
In this case, Eloquent will run a select id, name from categories query.
23 / 38
Martin Joo - Laravel Eloquent Recipes
saveMany
With the saveMany function you can save multiple related models in one function call.
When you update a Product you may want to delete all the prices and save the new ones. In this scenario
you can use the saveMany :
$productPrices = collect($prices)
"$map(fn (array $priceData) "% new ProductPrice([
'from_date' "% $priceData['fromDate'],
'to_date' "% $priceData['toDate'],
'price' "% $priceData['price'],
]));
$product"$prices()"$delete();
$product"$prices()"$saveMany($productPrices);
It will create all the prices in one query, and you don't have to deal with loops:
createMany
Similarly to saveMany you can also use the createMany if you don't have models, but arrays instead:
$prices = [
['from' "% '2022-01-10', 'to' "% '2022-02-28', 'price' "% 9.99],
['from' "% '2022-03-01', 'to' "% null, 'price' "% 14.99],
];
$product"$prices()"$createMany($prices);
24 / 38
Martin Joo - Laravel Eloquent Recipes
$table"$foreignId('category_id')
"$references('id')
"$on('categories');
$table"$foreignId('category_id')"$constrained();
$table"$foreignIdFor(Category"#class)"$constrained();
nullOnDelete
If you have a nullable relationship just use the nullOnDelete helper:
$table"$foreignId('category_id')
"$nullable()
"$constrained()
"$nullOnDelete();
25 / 38
Martin Joo - Laravel Eloquent Recipes
afterCreating
There's an afterCreating method on the Factory class that you can use to do something after a Model
has been created.
When I have Users with profile pictures, I always use this feature:
Now any time you create a new User with a factory, a fake image will be created and the
profile_picture_path will be set:
$user = User"#factory()"$create();
$user"$profile_picture_path ""' null;
26 / 38
Martin Joo - Laravel Eloquent Recipes
Factory For
Let's say you want to create a Product with a Category:
$category = Category"#factory()"$create();
$product = Product"#factory([
'category_id' "% $category"$id,
])"$create();
Instead of creating the category and passing it as an attribute, you can do this:
$product = Product"#factory()
"$for(Category"#factory())
"$create();
Factory Has
With the has method you can do the inverse of the relationship. So you can use this for has many
relationships:
Category"#factory()
"$has(Product"#factory()"$count(10));
You can also specify the relationship's name if it's different from the table name:
Product"#factory()
"$has(ProductPrice"#factory(), 'prices');
In this example, the prices is the name of the relationship on the Product model.
27 / 38
Martin Joo - Laravel Eloquent Recipes
Factory States
When working with factories in tests (or seeders) we often need a specific 'state' in a given model. Let's say
we have a Product model and it has an active column.
With this setup you have to specify the active flag every time you want to set it:
$product = Product"#factory([
'active' "% false,
])"$create();
28 / 38
Martin Joo - Laravel Eloquent Recipes
But factories provide a way to define 'states' for your model. A state can be something like an inactive
product:
$product = Product"#factory()"$state('inactive')"$create();
29 / 38
Martin Joo - Laravel Eloquent Recipes
This snippet will log every database query to your default log file if your environment is set to local . If you
need a more robust solution check out:
Telescope
Clockwork
Laravel Debugbar
30 / 38
Martin Joo - Laravel Eloquent Recipes
whenLoaded
API resource is a really great concept, but it has a bottleneck: it's very easy to create N+1 query problems.
Let's say we have an Employee and a Department model. An employee belongs to a department.
Imagine the following Controller method for the GET /employees API:
If you have 500 employees you just made 501 queries with this setup. Let's see what's happening:
$employees = Employee:all();
select *
from emplooyes
This line:
31 / 38
Martin Joo - Laravel Eloquent Recipes
return EmployeeResource"#collection($employees);
Will convert every Employee model to an array, so practically it's a foreach . And this line in the resource:
select *
from department
where id = ?
For every employee. This is why you have 501 queries and this is why it's called N+1 query problem.
Don't worry! As always, Laravel can help us. Instead of using the $this->department directly in the
resource, we can use the whenLoaded helper:
It will check if the department relationship is already loaded. If it's not, it won't run an additional query. In
this case, the department index will be null .
Great, but now we don't return the departments, so it's empty in the user list on the frontend. To solve this
problem, we have to eager load the department relationships in the controller:
32 / 38
Martin Joo - Laravel Eloquent Recipes
In this case, Laravel will run one select and it will query all the employees with the department (using a
subquery).
33 / 38
Martin Joo - Laravel Eloquent Recipes
This works fine, and will return pagination URLs like this: /api/category/abcd-1234/threads?page=1 .
However, there is a problem: if you have any query string in your URL it will be lost.
You still have pagination links like the one above, so without the filter[title]=laravel query string.
It will append any query string to the URL, so your pagination link will be: /api/category/abcd-
1234/threads?page=1&filter[title]=laravel
34 / 38
Martin Joo - Laravel Eloquent Recipes
Prunable Trait
If you have a logic in your application that deletes some old or unused rows from the database, you can
refactor it using the Prunable trait provided by Laravel:
And you don't need to write your own command, you only need to schedule the one provided by Laravel:
35 / 38
Martin Joo - Laravel Eloquent Recipes
use Illuminate\Database\Eloquent\Builder;
whereTicker is a scope that you can use on your models. The main difference is that we need to return an
instance of self .
Second, we need to tell Eloquent that the Holding model has its own Builder class:
36 / 38
Martin Joo - Laravel Eloquent Recipes
$holding = Holding"#whereTicker($ticker)"$first();
$holding"$portfolio_id = $id;
$holding"$save();
Holding"#query()
"$whereTicker('AAPL')
"$whereBelongsTo($user)
"$where('created_at', '")', now())
"$get();
37 / 38
Martin Joo - Laravel Eloquent Recipes
Thank You
Thank you very much for reading this short PDF! I hope you have learned a few cool techniques.
By the way, I’m Martin Joo, a software engineer since 2012. I’m passionate about Domain-Driven Design,
Test-Driven Development, Laravel, and beautiful code in general.
38 / 38