⬆️ Go to main menu ⬅️ Previous (DB Models and Eloquent) ➡️ Next (Migrations)
- OrderBy on Eloquent relationships
- Add where statement to Many-to-Many relation
- Conditional relationships
- Raw DB Queries: havingRaw()
- Eloquent has() deeper
- Has Many. How many exactly?
- Default model
- Use hasMany to create Many
- Multi level Eager Loading
- Eager Loading with Exact Columns
- Touch parent updated_at easily
- Always Check if Relationship Exists
- Use withCount() to Calculate Child Relationships Records
- Extra Filter Query on Relationships
- Load Relationships Always, but Dynamically
- Instead of belongsTo, use hasMany
- Rename Pivot Table
- Update Parent in One Line
- Laravel 7+ Foreign Keys
- Combine Two "whereHas"
- Check if Relationship Method Exists
- Pivot Table with Extra Relations
- Load Count on-the-fly
- Randomize Relationship Order
- Filter hasMany relationships
- Filter by many-to-many relationship pivot column
- A shorter way to write whereHas
- You can add conditions to your relationships
- New
whereBelongsTo()
Eloquent query builder method - The
is()
method of one-to-one relationships for comparing models whereHas()
multiple connections- Update an existing pivot record
- Relation that will get the newest (or oldest) item
- Replace your custom queries with ofMany
- Avoid data leakage when using orWhere on a relationship
You can specify orderBy() directly on your Eloquent relationships.
public function products()
{
return $this->hasMany(Product::class);
}
public function productsByName()
{
return $this->hasMany(Product::class)->orderBy('name');
}
In your many-to-many relationships, you can add where statements to your pivot table using the wherePivot
method.
class Developer extends Model
{
// Get all clients related to this developer
public function clients()
{
return $this->belongsToMany(Clients::class);
}
// Get only local clients
public function localClients()
{
return $this->belongsToMany(Clients::class)
->wherePivot('is_local', true);
}
}
Tip given by @cosmeescobedo
If you notice that you use same relationship often with additional "where" condition, you can create a separate relationship method.
Model:
public function comments()
{
return $this->hasMany(Comment::class);
}
public function approved_comments()
{
return $this->hasMany(Comment::class)->where('approved', 1);
}
You can use RAW DB queries in various places, including havingRaw()
function after groupBy()
.
Product::groupBy('category_id')->havingRaw('COUNT(*) > 1')->get();
You can use Eloquent has()
function to query relationships even two layers deep!
// Author -> hasMany(Book::class);
// Book -> hasMany(Rating::class);
$authors = Author::has('books.ratings')->get();
In Eloquent hasMany()
relationships, you can filter out records that have X amount of children records.
// Author -> hasMany(Book::class)
$authors = Author::has('books', '>', 5)->get();
You can assign a default model in belongsTo
relationship, to avoid fatal errors when calling it like {{ $post->user->name }}
if $post->user doesn't exist.
public function user()
{
return $this->belongsTo(User::class)->withDefault();
}
If you have hasMany()
relationship, you can use saveMany()
to save multiple "child" entries from your "parent" object, all in one sentence.
$post = Post::find(1);
$post->comments()->saveMany([
new Comment(['message' => 'First comment']),
new Comment(['message' => 'Second comment']),
]);
In Laravel you can Eager Load multiple levels in one statement, in this example we not only load the author relation but also the country relation on the author model.
$users = Book::with('author.country')->get();
You can do Laravel Eager Loading and specify the exact columns you want to get from the relationship.
$users = Book::with('author:id,name')->get();
You can do that even in deeper, second level relationships:
$users = Book::with('author.country:id,name')->get();
If you are updating a record and want to update the updated_at
column of parent relationship (like, you add new post comment and want posts.updated_at
to renew), just use $touches = ['post'];
property on child model.
class Comment extends Model
{
protected $touches = ['post'];
}
Never ever do $model->relationship->field
without checking if relationship object still exists.
It may be deleted for whatever reason, outside your code, by someone else's queued job etc.
Do if-else
, or {{ $model->relationship->field ?? '' }}
in Blade, or {{ optional($model->relationship)->field }}
. With php8 you can even use the nullsafe operator {{ $model->relationship?->field) }}
If you have hasMany()
relationship, and you want to calculate “children” entries, don’t write a special query. For example, if you have posts and comments on your User model, write this withCount()
:
public function index()
{
$users = User::withCount(['posts', 'comments'])->get();
return view('users', compact('users'));
}
And then, in your Blade file, you will access those number with {relationship}_count
properties:
@foreach ($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td class="text-center">{{ $user->posts_count }}</td>
<td class="text-center">{{ $user->comments_count }}</td>
</tr>
@endforeach
You may also order by that field:
User::withCount('comments')->orderBy('comments_count', 'desc')->get();
If you want to load relationship data, you can specify some limitations or ordering in a closure function. For example, if you want to get Countries with only three of their biggest cities, here's the code.
$countries = Country::with(['cities' => function($query) {
$query->orderBy('population', 'desc');
}])->get();
You can not only specify what relationships to ALWAYS load with the model, but you can do it dynamically, in the constructor method:
class ProductTag extends Model
{
protected $with = ['product'];
public function __construct() {
parent::__construct();
$this->with = ['product'];
if (auth()->check()) {
$this->with[] = 'user';
}
}
}
For belongsTo
relationship, instead of passing parent's ID when creating child record, use hasMany
relationship to make a shorter sentence.
// if Post -> belongsTo(User), and User -> hasMany(Post)...
// Then instead of passing user_id...
Post::create([
'user_id' => auth()->id(),
'title' => request()->input('title'),
'post_text' => request()->input('post_text'),
]);
// Do this
auth()->user()->posts()->create([
'title' => request()->input('title'),
'post_text' => request()->input('post_text'),
]);
If you want to rename "pivot" word and call your relationship something else, you just use ->as('name')
in your relationship.
Model:
public function podcasts() {
return $this->belongsToMany(Podcast::class)
->as('subscription')
->withTimestamps();
}
Controller:
$podcasts = $user->podcasts();
foreach ($podcasts as $podcast) {
// instead of $podcast->pivot->created_at ...
echo $podcast->subscription->created_at;
}
If you have a belongsTo()
relationship, you can update the Eloquent relationship data in the same sentence:
// if Project -> belongsTo(User::class)
$project->user->update(['email' => '[email protected]']);
From Laravel 7, in migrations you don't need to write two lines for relationship field - one for the field and one for foreign key. Use method foreignId()
.
// Before Laravel 7
Schema::table('posts', function (Blueprint $table)) {
$table->unsignedBigInteger('user_id');
$table->foreign('user_id')->references('id')->on('users');
}
// From Laravel 7
Schema::table('posts', function (Blueprint $table)) {
$table->foreignId('user_id')->constrained();
}
// Or, if your field is different from the table reference
Schema::table('posts', function (Blueprint $table)) {
$table->foreignId('created_by_id')->constrained('users', 'column');
}
In Eloquent, you can combine whereHas()
and orDoesntHave()
in one sentence.
User::whereHas('roles', function($query) {
$query->where('id', 1);
})
->orDoesntHave('roles')
->get();
If your Eloquent relationship names are dynamic and you need to check if relationship with such name exists on the object, use PHP function method_exists($object, $methodName)
$user = User::first();
if (method_exists($user, 'roles')) {
// Do something with $user->roles()->...
}
In many-to-many relationship, your pivot table may contain extra fields, and even extra relationships to other Model.
Then generate a separate Pivot Model:
php artisan make:model RoleUser --pivot
Next, specify it in belongsToMany()
with ->using()
method. Then you could do magic, like in the example.
// in app/Models/User.php
public function roles()
{
return $this->belongsToMany(Role::class)
->using(RoleUser::class)
->withPivot(['team_id']);
}
// app/Models/RoleUser.php: notice extends Pivot, not Model
use Illuminate\Database\Eloquent\Relations\Pivot;
class RoleUser extends Pivot
{
public function team()
{
return $this->belongsTo(Team::class);
}
}
// Then, in Controller, you can do:
$firstTeam = auth()->user()->roles()->first()->pivot->team->name;
In addition to Eloquent's withCount()
method to count related records, you can also load the count on-the-fly, with loadCount()
:
// if your Book hasMany Reviews...
$book = Book::first();
$book->loadCount('reviews');
// Then you get access to $book->reviews_count;
// Or even with extra condition
$book->loadCount(['reviews' => function ($query) {
$query->where('rating', 5);
}]);
You can use inRandomOrder()
to randomize Eloquent query result, but also you can use it to randomize the relationship entries you're loading with query.
// If you have a quiz and want to randomize questions...
// 1. If you want to get questions in random order:
$questions = Question::inRandomOrder()->get();
// 2. If you want to also get question options in random order:
$questions = Question::with(['answers' => function($q) {
$q->inRandomOrder();
}])->inRandomOrder()->get();
Just a code example from my project, showing the possibility of filtering hasMany relationships.
TagTypes -> hasMany Tags -> hasMany Examples
And you wanna query all the types, with their tags, but only those that have examples, ordering by most examples.
$tag_types = TagType::with(['tags' => function ($query) {
$query->has('examples')
->withCount('examples')
->orderBy('examples_count', 'desc');
}])->get();
If you have a many-to-many relationship, and you add an extra column to the pivot table, here's how you can order by it when querying the list.
class Tournament extends Model
{
public function countries()
{
return $this->belongsToMany(Country::class)->withPivot(['position']);
}
}
class TournamentsController extends Controller
{
public function whatever_method() {
$tournaments = Tournament::with(['countries' => function($query) {
$query->orderBy('position');
}])->latest()->get();
}
}
Released in Laravel 8.57: a shorter way to write whereHas() with a simple condition inside.
// Before
User::whereHas('posts', function ($query) {
$query->where('published_at', '>', now());
})->get();
// After
User::whereRelation('posts', 'published_at', '>', now())->get();
class User
{
public function posts()
{
return $this->hasMany(Post::class);
}
// with a getter
public function getPublishedPostsAttribute()
{
return $this->posts->filter(fn ($post) => $post->published);
}
// with a relationship
public function publishedPosts()
{
return $this->hasMany(Post::class)->where('published', true);
}
}
Tip given by @anwar_nairi
Laravel 8.63.0 ships with a new whereBelongsTo()
Eloquent query builder method. Smiling face with heart-shaped eyes
This allows you to remove BelongsTo foreign key names from your queries, and use the relationship method as a single source of truth instead!
// From:
$query->where('author_id', $author->id)
// To:
$query->whereBelongsTo($author)
// Easily add more advanced filtering:
Post::query()
->whereBelongsTo($author)
->whereBelongsTo($cateogry)
->whereBelongsTo($section)
->get();
// Specify a custom relationship:
$query->whereBelongsTo($author, 'author')
Tip given by @danjharrin
We can now make comparisons between related models without further database access.
// BEFORE: the foreign key is taken from the Post model
$post->author_id === $user->id;
// BEFORE: An additional request is made to get the User model from the Author relationship
$post->author->is($user);
// AFTER
$post->author()->is($user);
Tip given by @PascalBaljet
// User Model
class User extends Model
{
protected $connection = 'conn_1';
public function posts()
{
return $this->hasMany(Post::class);
}
}
// Post Model
class Post extends Model
{
protected $connection = 'conn_2';
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
}
// wherehas()
$posts = Post::whereHas('user', function ($query) use ($request) {
$query->from('db_name_conn_1.users')->where(...);
})->get();
Tip given by @adityaricki
If you want to update an existing pivot record on the table, use updateExistingPivot
instead of syncWithPivotValues
.
// Migrations
Schema::create('role_user', function ($table) {
$table->unsignedId('user_id');
$table->unsignedId('role_id');
$table->timestamp('assigned_at');
})
// first param for the record id
// second param for the pivot records
$user->roles()->updateExistingPivot(
$id, ['assigned_at' => now()],
);
Tip given by @sky_0xs
New in Laravel 8.42: In an Eloquent model can define a relation that will get the newest (or oldest) item of another relation.
public function historyItems(): HasMany
{
return $this
->hasMany(ApplicationHealthCheckHistoryItem::class)
->orderByDesc('created_at');
}
public function latestHistoryItem(): HasOne
{
return $this
->hasOne(ApplicationHealthCheckHistoryItem::class)
->latestOfMany();
}
class User extends Authenticable {
// Get most popular post of user
public function mostPopularPost() {
return $this->hasOne(Post::class)->ofMany('like_count', 'max');
}
}
Tip given by @LaravelEloquent
$user->posts()
->where('active', 1)
->orWhere('votes', '>=', 100)
->get();
Returns: ALL posts where votes are greater than or equal to 100 are returned
select * from posts where user_id = ? and active = 1 or votes >= 100
use Illuminate\Database\Eloquent\Builder;
$users->posts()
->where(function (Builder $query) {
return $query->where('active', 1)
->orWhere('votes', '>=', 100);
})
->get();
Returns: Users posts where votes are greater than or equal to 100 are returned
select * from posts where user_id = ? and (active = 1 or votes >= 100)
Tip given by @BonnickJosh