Using pivot tables in Laravel: The complete guide to custom intermediary tables in many-to-many relationships

Oct 22, 2022

When working with many-to-many relationships the table structure is a little more complex than other relationships.

When using many-to-many relationships we need to create an intermediary table.

Let's say for example that we have an airline app where we have customers and flights, each customer can have multiple fights, and a flight can have multiple customers (or passengers).

Your migrations would look something like this (simplified to focus on the pivot table):

// clients migration
return new class extends Migration
{
    public function up()
    {
        Schema::create('clients', function (Blueprint $table) {
            $table->id();
            $table->string('id');
            $table->string('name');
            $table->timestamps();
        });
    }

    //...
}
// flights migration
return new class extends Migration
{
    public function up()
    {
        Schema::create('flights', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('airline');
            $table->timestamps();
        });
    }

    //...
}
// pivot table migration
return new class extends Migration
{
    public function up()
    {
        Schema::create('client_flight', function (Blueprint $table) {
            $table->foreignId('client_id');
            $table->foreignId('flight_id');
            $table->primary(['client_id', 'flight_id']);
        });
    }

    //...
}

In this article, I'm going to show you how to use that pivot table going from the basics to the most custom behaviors.

The basic usage

By using the table structure I mentioned above we can leverage Laravel's defaults. So our models will look very simple.

class Flight extends Model
{
    public function clients()
    {
        return $this->belongsTo(Client::class);
    }
}
class Client extends Model
{
    public function flights()
    {
        return $this->belongsTo(Flight::class);
    }
}

Laravel guesses the name of the pivot table by taking the singular of the model names and joining the two in alphabetical order, in this case, client is before flight so the table name it's client_flight.

Then it assumes that column names will be the same singular model name with the _id postfix. In this case client_id and flight_id

However, both the table name and the column names can be changed to whatever you want by passing some parameters to the belongsTo function.

Using custom table and column names

Let's say for example that for some reason you don't want to call your pivot table client_flight, you want to call it tickets and the columns won't be client_id and flight_id. Instead, you want to drop the _id and just go with client and flight.

So, the table migration for your pivot table would look something like this:

// pivot table migration
return new class extends Migration
{
    public function up()
    {
        Schema::create('tickets', function (Blueprint $table) {
            $table->foreignId('client');
            $table->foreignId('flight');
            $table->primary(['client', 'flight']);
        });
    }

    //...
}

To make this structure work we need to make some changes to our models, we need to pass the table name and the new column names. Let's look at the Flight model first.

class Flight extends Model
{
    public function clients()
    {
        return $this->belongsTo(Client::class, 'tickets', 'flight', 'client');
    }
}

We added 3 new parameters, tickets, flight and client. The first one represents the name of the pivot table, and the second on it's the name of the column that is related to our model (the one that will store the flight id).

And the last is the column related to the other model (the one that will store the client id).

We do the same in our Client model.

class Client extends Model
{
    public function flights()
    {
        return $this->belongsTo(Flight::class, 'tickets', 'client', 'flight');
    }
}

Notice that now the client and flight are inverted from the Flight model.

And done, we now have a costume table name with custom column names.

But what if you want to store when the pivot table is created and updated by using Laravel timestamps?

Adding timestamps to the pivot table

To add timestamps to the pivot table, we first need to add the timestamps columns in our migration. You can create a new migration if the table is already in use, but I'm going to add it to the one that creates the table.

// pivot table migration
return new class extends Migration
{
    public function up()
    {
        Schema::create('tickets', function (Blueprint $table) {
            $table->foreignId('client');
            $table->foreignId('flight');
            $table->primary(['client', 'flight']);
            $table->timestamps();
        });
    }

    //...
}

We call the method $table->timestamps() the same way we would do on any other migration.

Now we need to tell Laravel to update the created_at and updated_at columns automatically. To do that we add the ->withTimestamps()

class Flight extends Model
{
    public function clients()
    {
        return $this->belongsTo(Client::class, 'tickets', 'flight', 'client')
            ->withTimestamps();
    }
}
class Client extends Model
{
    public function flights()
    {
        return $this->belongsTo(Flight::class, 'tickets', 'client', 'flight')
            ->withTimestamps();
    }
}

And done, we now have timestamps in our pivot table.

Adding additional columns to the pivot table

By default, you can access your pivot table by using the keyword pivot after accessing a relationship. For example:

$client = Client::find(1);

foreach ($client->flights as $flight) {
    echo "The ticket was purchased at:" . $flight->pivot->created_at;
}

But what if you want to add more columns to your pivot table? For example, maybe I would like to store the seat that the user purchased in the pivot table. First, let's add a column to the tickets table.

// pivot table migration
return new class extends Migration
{
    public function up()
    {
        Schema::create('tickets', function (Blueprint $table) {
            $table->foreignId('client');
            $table->foreignId('flight');
            $table->primary(['client', 'flight']);
            $table->timestamps();
            $table->string('seat');
        });
    }

    //...
}

After adding the column to the table, we need to tell Laravel that we would like to fetch that column when we access the relationship. To do that we'll use the withPivot method.

class Flight extends Model
{
    public function clients()
    {
        return $this->belongsTo(Client::class, 'tickets', 'flight', 'client')
            ->withTimestamps()
            ->withPivot('seat');
    }
}
class Client extends Model
{
    public function flights()
    {
        return $this->belongsTo(Flight::class, 'tickets', 'client', 'flight')
            ->withTimestamps()
            ->withPivot('seat');
    }
}

We pass the column name to the withPivot method, if you want to use multiple columns you can pass multiple arguments to the method like this ->withPivot('column1', 'column2', 'etc..').

Now to store the value when we attach a client to a flight or vice versa we can pass an array of values corresponding to the column names.

$client->flights()->attach($flightId, ['seat' => $seat]);

And we can access it through the pivot keyword:

$client = Client::find(1);

foreach ($client->flights as $flight) {
    echo "The client's seat is:" . $flight->pivot->seat;
}

But it's a little weird that our table is called tickets but we need to use this pivot word to access the values. Would it be nice if we could call $flight->ticket->seat instead?

Name your pivot table

Well, you can, and it's very simple. Just add a call to ->as('ticket') in the relationship definition.

class Flight extends Model
{
    public function clients()
    {
        return $this->belongsTo(Client::class, 'tickets', 'flight', 'client')
            ->as('ticket')
            ->withTimestamps()
            ->withPivot('seat');
    }
}
class Client extends Model
{
    public function flights()
    {
        return $this->belongsTo(Flight::class, 'tickets', 'client', 'flight')
            ->as('ticket')
            ->withTimestamps()
            ->withPivot('seat');
    }
}

Now instead of pivot we can use ticket:

$client = Client::find(1);

foreach ($client->flights as $flight) {
    echo "The client's seat is:" . $flight->ticket->seat;
}

Extract the pivot table to a model

One other feature that we can use in Laravel is creating a model for the pivot table.

Doing this allows us to add behaviors to the pivot table. For example, let's say I want to add a way to upgrade a seat to first class.

First I'm going to create a model called Ticket, but this model will extend Pivot instead of Model like we would in other models.

And here, I'm going to add a method to upgrade to first class.

use Illuminate\Database\Eloquent\Relations\Pivot;
 
class Ticket extends Pivot
{
    public function upgradeToFirstClass()
    {
        // Example code that doesn't work :P
        $this->seat = $someFirstClassSeat;
    }
}

Now in our main models, we need to call the ->using() method when defining the relationship.

class Flight extends Model
{
    public function clients()
    {
        return $this->belongsTo(Client::class, 'tickets', 'flight', 'client')
            ->as('ticket')
            ->withTimestamps()
            ->withPivot('seat')
            ->using(Ticket::class);
    }
}
class Client extends Model
{
    public function flights()
    {
        return $this->belongsTo(Flight::class, 'tickets', 'client', 'flight')
            ->as('ticket')
            ->withTimestamps()
            ->withPivot('seat')
            ->using(Ticket::class);
    }
}

And now we can call the methods in our pivot table, for example:

$flight = $client->flights()->find($flightId);
$flight->ticket->updateToFirstClass();

And that's it. We went from the most basic example of a many-to-many relationship to creating a very custom setup. Hope you learned something :).