Let's talk about a belongs-to-many, also called many-to-many relationships, but on a deeper level. What are other options besides the foreign keys columns in the pivot table?
This is a typical pivot table between a project and a user. A project may belong to many users.
Schema::create('project_user', function (Blueprint $table) { $table->foreignId('project_id')->constrained(); $table->foreignId('user_id')->constrained();});
To show users for each project, you typically get the projects with user relations and do a foreach loop.
use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Project extends Model{ public function users(): BelongsToMany { return $this->belongsToMany(User::class); }}
$projects = Project::with('users')->get(); foreach ($projects as $project) { print $project->id . ': ' . $project->title . ' ('; foreach ($project->users as $user) { print $user->email .'; '; } print ')<hr />';}
This is a typical default simple belongs-to-many relation.
Adding Timestamps to Pivot
What can we add to the pivot? For example, you want to know when that record was added. By default, pivot tables are without timestamps. To enable timestamps, we must first add columns to the migration.
Schema::create('project_user', function (Blueprint $table) { $table->foreignId('project_id')->constrained(); $table->foreignId('user_id')->constrained(); $table->timestamps(); });
But that wouldn't be enough. Timestamps, by default, wouldn't be set in a pivot table. To enable setting timestamps in the relationship belongs-to-many, you need to define withTimestamps()
.
use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Project extends Model{ public function users(): BelongsToMany { return $this->belongsToMany(User::class)->withTimestamps(); }}
I have re-seeded the database, and now the timestamps are filled.
How do we show the created_at
from the pivot table? On the object, first, you must use the pivot
, which is the intermediate table, and then call the column.
$projects = Project::with('users')->get(); foreach ($projects as $project) { print $project->id . ': ' . $project->title . ' ('; foreach ($project->users as $user) { print $user->email .' <i>' . $user->pivot->created_at . '</i>; '; } print ')<hr />';}
Now, as a result, we can see email and timestamp.
Adding Custom Fields to Pivot
Similarly, you can add any field, not just the timestamps. Again, it was a similar scenario—migrations, then model, and then where do you use that? For example, we can add if it is active.
Schema::create('project_user', function (Blueprint $table) { $table->foreignId('project_id')->constrained(); $table->foreignId('user_id')->constrained(); $table->boolean('is_active')->default(false); $table->timestamps();});
Next, you define withPivot()
in the belongs to many relationships and provide all the columns.
use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Project extends Model{ public function users(): BelongsToMany { return $this->belongsToMany(User::class)->withPivot('is_active')->withTimestamps(); }}
Then, where you want to show the value, you again use pivot
on the object and then call the column.
$projects = Project::with('users')->get(); foreach ($projects as $project) { print $project->id . ': ' . $project->title . ' ('; foreach ($project->users as $user) { print $user->email .' <u>' . $user->pivot->is_active . '</u> <i>' . $user->pivot->created_at . '</i>; '; } print ')<hr />';}
As a result, some records are active, and some aren't.
Query By Pivot Fields
What if you want to query by the pivot field is_active
? You can do this on the Eloquent query whereHas()
, but also you can define a specific relationship.
For example, we can define a relation only for is_active
true. When describing the relation, instead of withPivot()
, we must use the wherePivot()
method.
use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Project extends Model{ public function users(): BelongsToMany { return $this->belongsToMany(User::class)->withPivot('is_active')->withTimestamps(); } public function activeUsers(): BelongsToMany { return $this->belongsToMany(User::class)->wherePivot('is_active', true)->withTimestamps(); }}
Then, instead of eagerly loading the users
relation, we must use activeUsers
.
$projects = Project::with('activeUsers')->get(); foreach ($projects as $project) { print $project->id . ': ' . $project->title . ' ('; foreach ($project->activeUsers as $user) { print $user->email .' <u>' . $user->pivot->is_active . '</u> <i>' . $user->pivot->created_at . '</i>; '; } print ')<hr />';}
As a result, only users with is_active
true on a pivot table are shown.
Rename "pivot"?
Another possibility is to rename the word pivot
. What if you don't like the phrase pivot, or it's not 100% clear for you and your team, and you want to rename it for clarity? In the relationship, you may define as
.
use Illuminate\Database\Eloquent\Relations\BelongsToMany; class Project extends Model{ public function users(): BelongsToMany { return $this->belongsToMany(User::class)->withPivot('is_active')->withTimestamps(); } public function activeUsers(): BelongsToMany { return $this->belongsToMany(User::class) ->as('active_user') ->wherePivot('is_active', true) ->withPivot('is_active')->withTimestamps(); }}
Then, when showing data, instead of pivot
for the intermediate table, you use active_user
.
$projects = Project::with('activeUsers')->get(); foreach ($projects as $project) { print $project->id . ': ' . $project->title . ' ('; foreach ($project->activeUsers as $user) { print $user->email .' <u>' . $user->active_user->is_active . '</u> <i>' . $user->active_user->created_at . '</i>; '; } print ')<hr />';}
The result stays the same. It still works. To be honest, I've never used this feature. I like the word pivot,
and it describes the table best, but it is possible to rename it if you want.
Code for this lesson can be found on GitHub.
Hello, i think in first function
activeUsers()
missing->withPivot('is_active')
In case of a single-value
is_active
column, a separate relation seems the better option. What if we have a multiple-value column on the pivot table and we want to filter withwhereIn()
? It doesn't seem reasonable to create different relations for every possible value. You've mentioned thatwhereHas()
can be used. How do we do that? Is there any other eloquent ways?It is not only reasonable to create columns for each potential value, but also beneficial.
When searching inside a JSON or some other multi-value column - you are creating performance issues. And using single value columns - you are avoiding them.
Now about the
whereHas()
there iswherePivotIn()
and otherwherePivot()
methods availableI apologize for any confusion caused by my earlier question.
By single-value column, I mean a column with a boolean value, such as
is_active
(as in the example above). By multi-value column, I mean a column that can take one value among several possible options, such ascountry_code
. For instance, this column may have a value like us, uk, ca, or de, but not multiple codes simultaneously. Since the list of possible values can be extensive, it doesn't seem practical to create separate relationships for each of them.However, this is just background information.
My main question is: I don't want to create separate relationships like
activeUsers
. Instead, I want to query dynamically with arbitrary values. I've read above that this should be possible usingwhereHas
, but I couldn't find an example. When I tried, I wasn't able to make it work.Here’s the scenario:
I have two models:
User
andAbility
. There’s a pivot table between them that includes a columnin_which_country
. I want to query users based on both thename
column of theabilities
table and thein_which_country
column of the pivot table (ability_user
).I assumed the following syntax should work, but it fails:
Okay, this makes it more clear :)
So to filter this, in theory you could do:
This should trigger the pivot table. Or, you could install
debugbar
and see the query it generates. From there it would be easier to work out what needs to be done ;)ps. It's all about table/column naming. Laravel relationship via pivot table loads that pivot table too. So it can be queried by prefixing the column with table name
Thank you very much! I think I got a bit too fixated on the
wherePivotIn
method. I guess we should usewhereIn
instead ofwhere
for handling multiple values (['us', 'ca']
). Additionally, specifying the table name doesn’t seem necessary unless there’s ambiguity, such as when both tables have columns with the same name. For future reference, I can confirm that the following works: