Managing Pivot Data with Laravel Eloquent

Dan Pastori avatar
Dan Pastori October 25th, 2022

Laravel provides the most beautiful Object Relational Mapping systems I’ve ever used with Eloquent. Eloquent has so many powerful features making working with a database so enjoyable! One of my favorite features of Laravel Eloquent is the many-to-many relationship structure. When I had to write raw SQL, I always hated many-to-many relationships. With Eloquent, they are a breeze!

In this tutorial we’ll go through a few of my favorite methods when updating and creating many-to-many relationships with Laravel Eloquent. There are a lot of other basic and useful methods available for defining and querying these relationships.

However, I was always slightly confused when creating and updating related data so I wrote this tutorial as a guide for myself and for you. I’ve tended to use a lot of these to set up Gates & Policies and other permission structures in Laravel.

Set up a Many-To-Many Relationship with Laravel Eloquent

For this example, we are going to use a real life situation we came across when building Bugflow. That’s managing user permissions on projects and organizations. For this example, every user can belong to multiple projects and multiple organizations. On each project and organization, the user has permissions. These will be stored in the pivot table along with the relationship with the project. Let’s set this up.

First we will create 3 tables within our Laravel Install. The tables will be users , projects, and users_projects. For simplicity sake, I won’t be adding EVERY field, just enough to make sure this tutorial makes sense.

At the most basic level, our users table should look like:

users
	id
	name

Our projects table should look like:

projects
	id
	name

And our users_projects table should look like:

users_projects
	user_id
	project_id
	permission

Notice the permission column on the users_projects table? That column will determine the level of access the user has on a project. In future references, this table is also referred to as the pivot table.

For now, the values of the permission column could either be admin or member. I’ll document how to update this value efficiently. When creating the table, I always set the default permission column to be member. This way we can only directly set this value if needed and don’t accidentally create users with admin permissions.

Now that we have our tables constructed, let’s define our relationships in the App\Models\User.php and App\Models\Project.php Eloquent models respectively.

Defining our Relationships

Before we move into all of the methods regarding many-to-many relationships, we have to define the relationship on each of our models. With Eloquent you have to use the belongsToMany() relationship on both the User and the Project. First, let’s open our User model and add the projects relationship:

<?php

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable;

    //... Other relationships, etc.

    public function projects()
    {
        return $this->belongsToMany('App\Models\Project', 'users_projects', 'user_id', 'project_id');
    }
}

When setting up many-to-many relationships with Eloquent, there are 4 parameters you need to set. The first parameter is the model you are relating to. In this case, it’s App\Models\Project. The second parameter is the table the relationship takes place on. So, we use our pivot table which is users_projects. The third parameter is the key on the pivot table that holds the key of the model we are relating. Our users all have an id field. On the pivot table, this is user_id. We use the user_id value. Finally, we select the related foreign key. This is the key on the pivot table which identifies the relationship to the project. In this case it’s project_id which will reference the id on the projects table.

Now when we look at our Project model, it should look very similar, just from the opposite perspective:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Project extends Model
{
    public function users()
    {
        return $this->belongsToMany('App\Models\User', 'users_projects', 'project_id', 'user_id');
    }
}

So now we can call $project->users() and $users->projects() to get all of the users on a project and all of the projects that belong to a user.

Since we now have our relationships setup, let’s take a look at all the different ways we can add and delete relationships.

Attaching with Pivot Data

One of the best features about Laravel is their docs provide so much helpful information. They cover a lot of the basics for many-to-many relationships, such as attach() and detach(). What I’m going to focus on are some of the more “edge cases” that I’ve run across.

If you were to bind a project to a user using attach(), the code would look like this:

$user->projects()->attach([1]);

or you could pass the project model itself:

$user->projects()->attach($project);

These are great, however they present a challenge if you want the permissions on the users_projects table to be anything other than member. Like if you were adding an admin to a project. To do that, you’d have to pass along the permission with the id of the project you are attaching:

$user->projects()->attach([1 => [
    'permission' => 'admin'
]]);

Now the attach method takes a multi-dimensional array with the key being the id of the project we are attaching and the sub array being an array mapped to the columns of the pivot table. If you wanted to attach multiple projects with different permissions, you could do:

$user->projects()->attach([
    1 => [
        'permission' => 'admin'
    ],
    2 => [
        'permission' => 'member'
    ]
]);

Or if you wanted to attach multiple projects with the same permission, you could simplify the above code to be:

$user->projects()->attach([1, 2], [
    'permission' => 'member'
]);

These little methods are super helpful! I tend to use multiple forms of the attach() method throughout the project in different scenarios. However, I only use a form of attach() if I know the relationship has no possibility of existing. If there’s a possibility the relationship exists (i.e. the user is already on the project), then it’s possible to bind them twice. That’s not convenient. For that I use a form of sync() which is coming up next!

Syncing Relationships

Any time I don’t know if a relationship already exists, I use a form of sync(). The sync() method is another way of updating relationships between two models. The difference is that the pivot table will always remain in-sync with what is passed.

What this means, is if I run the following code:

$user->projects()->sync([1]);

The user will only be bound to project ID 1. If I run that code thirty times, the relationship will only be bound once in the table and it will stay in sync (get it?). However, this becomes an issue if your user has twenty projects, you don’t know if they have project ID 2 and you run:

$user->projects()->sync([2]);

The other 19 projects that the user had have now been deleted! That’s where syncWithoutDetaching() comes in handy.

Sync Without Detaching

The syncWithoutDetaching() method is extremely useful. This way you can add a user to a project IF they have not already been added to the project! Solving a few of the caveats of attach() and sync().

To attach a project to a user, not knowing if they are already on the project, you’d run:

$user->projects()->syncWithoutDetaching(3);

Now if the user already has the project, nothing will happen. However, if the user has not been added to the project, this this method will add the user to the project.

You can also attach pivot data:

$user->projects()->syncWithoutDetaching([3 => [
    'permission' => 'admin'
]]);

Now you will make sure that if the user has the project, they are an admin on the project. Pretty slick!

Sync With Pivot Values

Similar to how you can pass an array that attaches a permission to every relationship, you can do that with sync as well. The method is syncWithPivotValues(). In Bugflow, we do this when we upgrade a user’s member ship on an organization. So if the organization has 15 projects and the user gets upgraded to an admin we want to pass an array of projects and have all of them be bound to the user with proper permission. To do that, you can run:

$user->projects()->syncWithPivotValues([1, 2, 3], [
    'permission' => 'admin'
]);

Now the user will have permissions for all projects with the level of admin. While on the subject of updating pivot table data, let’s check out one of my favorite methods, updateExistingPivot(). You will see why in a second.

Updating Existing Pivot Table

When working with a many-to-many relationship with Eloquent, you are mapping objects to a database table. Well, what if you have a pivot table and you want to update one of the pivot fields. You could either build a model like UserProject.php and map to that table. But that requires you to have an id on the users_projects table. The pivot table is pretty dynamic so the id would become kind of a pain and your auto increments could be large for no reason.

You could also use the DB facade and directly query the pivot table. That’s nice, but there’s a better option.

The updateExistingPivot() option solves all of these problems! What this method does is takes a relationship on the pivot table, in our case users_projects and updates a pivot value such as permission.

Let’s say we want to update the permission on project 2 to be admin:

$user->projects()->updateExistingPivot( 2, ['permission' => 'admin'] );

That’s all you have to do! Now you can update whatever pivot table data exists without building intermediate models or direct access to the database.

Conclusion

Hope these little snippets and explanations help you just as much as they help me! For all of the Eloquent documentation check out the official docs. Figured I’d share a few of my own experiences. Sometimes I get confused at what I should use, so it’s good to have something to reference.

By all means, if you have questions, feel free to reach out on Twitter (@danpastori). If you like these tutorials, sign up for our mailing list.

Support future content

The Ultimate Guide to Building APIs and Single-Page Applications with Laravel + VueJS + Capacitor book cover.

Psst... any earnings that we make off of our book is being reinvested to bringing you more content. If you like what you read, consider getting our book or get sweet perks by becoming a sponsor.

Written By Dan

Dan Pastori avatar Dan Pastori

Builder, creator, and maker. Dan Pastori is a Laravel certified developer with over 10 years experience in full stack development. When you aren't finding Dan exploring new techniques in programming, catch him at the beach or hiking in the National Parks.

Like this? Subscribe

We're privacy advocates. We will never spam you and we only want to send you emails that you actually want to receive. One-click unsubscribes are instantly honored.