Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@Braunson
Last active August 13, 2023 19:25
Show Gist options
  • Star 76 You must be signed in to star a gist
  • Fork 28 You must be signed in to fork a gist
  • Save Braunson/8b18b7fc7efd0890136ce5e46452ec72 to your computer and use it in GitHub Desktop.
Save Braunson/8b18b7fc7efd0890136ce5e46452ec72 to your computer and use it in GitHub Desktop.
Laravel 8.x - Diving into Pivot Tables

Laravel 6 - Diving Into Pivot Tables

Pivot tables can be confusing and a little hard to wrap your head around at first. In this quick article we are going to dive into what a pivot table is, how to create one and finally how to use the pivot table. Let's dive in!

What is a pivot table?

A pivot table is used to connect relationships between two tables. Laravel provides a Many To Many relationship where you can use a pivot table.

The use of a pivot table

In the example next, we will review how a users table and a roles table are joined together with a user_roles pivot table. Pivot tables allow you to (in this case) add multiple roles to a user.

Example of a Pivot Table

Let's create two tables which we need to use the Many To Many relationship for.

  • Table: users
  • Table: roles
  • (Pivot) Table: user_roles

The users table will look something like this:

  • id (INT)
  • name (VARCHAR)

The roles table will look like this:

  • id (INT)
  • name (VARCHAR)

Now our pivot table user_roles will look like this:

  • user_id (INT)
  • role_id (INT)

Using Pivot Tables

So now that we've gone over the structure of the two tables and our pivot table, let's dig into how to use them!

Creating The Pivot Table's Migration

The migration should look like this, it establishes the proper columns and foreign key relations to both the users and roles table. The migration will also set both user_id and role_id as the primary keys so there cannot be duplicates with both the same user_id and role_id.

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUserRolesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('user_roles', function (Blueprint $table) {
            $table->integer('user_id')->unsigned()->index();
            $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
            $table->integer('role_id')->unsigned()->index();
            $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
            $table->primary(['user_id', 'role_id']);
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('user_roles');
    }
}

Setting Up The Model Relations

We will open our User.php and Role.php models and define the relations needed in both models.

<?php

namespace App\Models;

use App\Models\Role;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    use Notifiable;
    
	/*
     * The roles that belong to this user
     */
    public function roles()
    {
        return $this->belongsToMany(Role::class);
    }
}
<?php

namespace App\Models;

use App\Models\User;
use Illuminate\Database\Eloquent\Model;

class Role extends Model
{    
	/*
     * The users that belong to this role
     */
    public function users()
    {
        return $this->belongsToMany(User::class);
    }
}

Defining Your Custom Intermediate Table

If you want to define the custom model that represents the pivot or intermediate table of your relation, you can call the using method when defining the relationship. Note that custom Many To Many pivot models should extend the Illuminate\Database\Eloquent\Relations\Pivot class. For example, we want to define a Role which uses a custom UserRole pivot model:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Relations\Pivot;

class UserRole extends Pivot
{    
    //
}

Retrieving a User's Role

Let's assume our User object has many Role objects that it is related to. After accessing this relationship, we may access the intermediate table using the pivot attribute on the models.

$user = App\Models\User::first();

foreach ($user->roles as $role) {
    echo $role->name; // This will echo out the role name
}

Notice that each Role model we retrieve is automatically assigned a pivot attribute. This attribute contains a model representing the intermediate table, and may be used like any other Eloquent model.

Customizing The pivot Attribute Name

As mentioned above, attributes from the intermediate table may be accessed on models using the pivot attribute. However, you can customize the name of this attribute to better reflect its purpose within your application.

For example, if your application contains users that may subscribe to multiple podcasts, you probably have a many-to-many relationship between users and podcasts. If this is the case, you may wish to rename your intermediate table accessor to subscription instead of pivot. This can be done using the as method when defining the relationship:

public function podcasts()
{
    return $this->belongsToMany(Podcast::class)
                ->as('subscriptions');
}

Once this is done, you can access the intermediate table data using the customized name from above:

$users = App\Models\User::with('podcasts')->get();

foreach ($users->flatMap->podcasts as $podcast) {
    echo $podcast->name; // This will echo out the podcast name
}

You may be asking what flatMap does in the above code example. It's a Collection helper. Eloquent returns a Collection on a relation by default. We use flatMap in the example to iterate through the collection and pass each value to the given callback. The callback is free to modify the item and return it, thus forming a new collection of modified items. Then, the array is flattened by a level (which is what we want in this case).

Adding A Role To A User

$user = App\Models\User::first(); // John Doe
$role = App\Models\Role::first(); // Admin

$user->roles()->attach($role->id);

Adding Multiple Roles To a User

You can pass an array of IDs to the attach method to add multiple roles to a user.

$user = App\Models\User::first(); // John Doe
$roles = App\Models\Role::all()->pluck('id'); // Admin, User

// Adds MULTIPLE roles to the user
$user->roles()->attach($roles);

Removing A Role From A User

$user = App\Models\User::first(); // John Doe
$role = App\Models\Role::first(); // Admin

$user->roles()->detach($role->id);

Removing Multiple Roles From a User

This works the same as above when adding a role but you can pass an array of IDs to the detach method to remove multiple roles from a user.

$user = App\Models\User::first(); // John Doe
$roles = App\Models\Role::all()->pluck('id'); // Admin, User

// Removes MULTIPLE roles to the user
$user->roles()->detach($roles);

Syncing Roles

There's a method called sync that accepts new vales as an array of IDs and will take care of the syncing of roles. The result is that no matter what values were in the table prior to running sync, after the code is run there will only be these role IDs attached to the user. It will remove any not in this list that are in the database and add any that aren't in the database but that are in this list.

$user  = App\Models\User::first();
$roles = App\Models\Role::all()->pluck('id');

// $roles will provide an array of role IDs that will be synced to the user 
$user->roles()->sync($roles);

Attaching a Role with Additional Parameters

Sometimes you may want to add additional parameters when attaching a role to a user. Let's take a look at an example on how to do this. In this example, assume the Administrator is assigning a user a new role but they want to specify who it was added by (just a string for simplicity sake, assume added_by is a column on the user_roles table alongside user_id and role_id).

$user = App\Models\User::first(); // John Doe
$role = App\Models\Role::find(2); // User

$user->roles()->attach($role->id, ['added_by' => 'Braunson']);

Wrapping up

There's a bunch more things you can do with relations and they are documented in the Laravel documentation. The goal of this write-up was to establish a basic understanding and usage with pivot tables in Laravel.

@BohdanKyryliuk
Copy link

Hi @Braunson
Thank you so much for this great article. It is very helpful and easy for a basic understanding.
I've noticed that you have a duplication of paragraphs in sections: Adding Multiple Roles To a User and Removing Multiple Roles From a User, but their meanings should be different.

@Braunson
Copy link
Author

@BohdanKyryliuk Thank you! I'm glad to hear, I was having to reference the docs and tutorials and figured I should just write a simple straight to the point guide.

Thanks for noticing the errors, I've updated the gist 😄

@GenrriMorgan
Copy link

Great stuff! Thank you

@eballeste
Copy link

eballeste commented Aug 26, 2020

for this section

you are talking about podcast/subscription entities but then the code snippets have some weird mix of the previous roles entities with the position pivot

@Braunson
Copy link
Author

@eballeste Thanks, I've updated the text.

@gilles6
Copy link

gilles6 commented Nov 5, 2020

It seems this:

Schema::create('user_roles', function (Blueprint $table) {
    $table->integer('user_id')->unsigned()->index();
    $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
    $table->integer('role_id')->unsigned()->index();
    $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
    $table->primary(['user_id', 'role_id']);
});

Could be simplified into this:

Schema::create('user_roles', function (Blueprint $table) {
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->foreignId('role_id')->constrained()->onDelete('cascade');
    $table->primary(['user_id', 'role_id']);
});

as per the documentation

@Braunson
Copy link
Author

Braunson commented Nov 5, 2020

@gilles6 Yes you could, however foreignId wasn't introduced until Laravel 7 (this was written when Laravel 6 was available). Either will work and for this example.

@jestin-g
Copy link

jestin-g commented Nov 7, 2020

Clear explainations ! Thank you

@Gams101
Copy link

Gams101 commented Dec 3, 2020

Thank you so much for sharing, this is such a great explanation and I have been struggling to understand this concept as a beginner in Laravel.

@bhaidar
Copy link

bhaidar commented Jan 15, 2021

Hi @Braunson

$user = App\Models\User::first();

foreach ($user->roles as $role) {
    echo $role->pivot->name; // This will echo out the role name
}

The "pivot" refers to the pivot table. In your migration above you don't add a "name" column to the pivot table. How come $role->pivot->name refers to role name?

Thanks

@Braunson
Copy link
Author

Braunson commented Feb 6, 2021

@bhaidar Thanks for noticing this, fixed!

@wdebusschere
Copy link

@Braunson Should the name of the pivot table not be: role_user
Naming convention: Pivot table names are listed in alphabetical order / singular.

@ionware
Copy link

ionware commented Mar 6, 2021

Thank you so much for this nice article @Braunson

@EeEmDee
Copy link

EeEmDee commented Apr 9, 2021

Great Article! I have a question though. Can you define a relationship on a pivot model on an extra custom pivot column?
Like this for example:

Schema for pivot table:

Schema::create('language_tutor', function (Blueprint $table) {
        $table->foreignId('language_id')->constrained()->onDelete('cascade');
        $table->foreignId('tutor_id')->constrained()->onDelete('cascade');
        $table->primary(['language_id', 'tutor_id']);
        $table->foreignId("language_level_id"); // This should reference a language level
        $table->timestamps();
});
class Language extends Model
{
    public function tutors(){
    	return $this->belongsToMany(Tutor::class)->using(LanguageTutor::class)->withPivot("language_level_id")->withTimestamps();
    }
}
class Tutor extends Model
{
    public function languages(){
    	return $this->belongsToMany(Language::class)->using(LanguageTutor::class)->withPivot("language_level_id")->withTimestamps();
    }
}
class LanguageTutor extends Pivot
{
	public function language_levels() {
		return $this->belongsTo(LanguageLevel::class);
	}
}

Would I be able to execute this query App\Models\Tutor::first()->languages->first()->pivot->language_levels?

@Farouk615
Copy link

Can i have a pivot table with 3 foreign keys ,??
lets say we have a :
user belongsToMany roles and vice versa
user also belongsToMany Teams and vice versa
a team belongsToMany Roles and vice versa

can i translate this in a pivot table called user_team_role contains :
user_id , role_id , team_id => they are 3 fk

is this possible ??

@bridgeyuwa
Copy link

@Farouk615
I was wondering the same.
I am stuck on a project that requires 3 tables all linked in a ManyToMany relation which I think would require a pivot table with 3 foreign keys.

I hope someone can help out ASAP

@dangolbeeker
Copy link

Checkout its by someone on the laravel core team
https://github.com/staudenmeir/eloquent-has-many-deep

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment