DEV Community

Dendi Handian
Dendi Handian

Posted on • Updated on

Laravel Many-to-Many Pivot Relationship

As a beginner in Laravel, I bet you have ever implemented a simple relationship functionality like one-to-one or one-to-many in an Eloquent way. But when you do the many-to-many, you have to deal with a pivot table between two related entities and it's a little bit challenging to read the documentation. So here is my success story about it...

I call it Pivot table. Some people may call it Cross-reference table or Association table. See this discussion

Prerequisites to code along

Prepare your own laravel app for demonstrating this, make the apps configured for the database. You can use any database driver you want because we truly have a faith in the Laravel Eloquent and its database interface system.

The Idea

I have a catchy case for this many-to-many implementation. We will create Movie and Actor entities because in the real world any movie could have many actors and vice versa, right?

Generating The Needed Files

We can easily generate the files with these artisan commands:

php artisan make:model Movie -fsm
php artisan make:model Actor -fsm
Enter fullscreen mode Exit fullscreen mode

The -fsm flag will generate the factory, seeder, and migration for us. Also, we need an extra entity for the pivot:

php artisan make:model MovieActor -sm
Enter fullscreen mode Exit fullscreen mode

And here is the map of the generated files:

- app
  |_ Models
     |_ Movie.php
     |_ Actor.php
     |_ MovieActor.php
- database
  |_ factories
     |_ MovieFactory.php
     |_ ActorFactory.php
  |_ migrations
     |_ xxx_xx_xx_xxxxxx_create_movies_table.php
     |_ xxx_xx_xx_xxxxxx_create_actors_table.php
     |_ xxx_xx_xx_xxxxxx_create_movie_actors_table.php
  |_ seeders
     |_ ActorSeeder.php
     |_ MovieSeeder.php
     |_ MovieActorSeeder.php
Enter fullscreen mode Exit fullscreen mode

Populating the Files

Migrations

For the migrations, let's make it simple by having only the name field to Movie and Actor table:

xxx_xx_xx_xxxxxx_create_movies_table.php:


...

    public function up()
    {
        Schema::create('movies', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

...

Enter fullscreen mode Exit fullscreen mode

xxx_xx_xx_xxxxxx_create_actors_table.php:


...

    public function up()
    {
        Schema::create('actors', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->timestamps();
        });
    }

...

Enter fullscreen mode Exit fullscreen mode

And for the pivot table, we use only entities ids for the columns and make them unique as a pair.

xxx_xx_xx_xxxxxx_create_movie_actors_table.php:


...

    public function up()
    {
        Schema::create('movie_actors', function (Blueprint $table) {
            $table->integer('movie_id');
            $table->integer('actor_id');
            $table->unique(['movie_id', 'actor_id']);
        });
    }

...

Enter fullscreen mode Exit fullscreen mode

Factories

ActorFactory.php:


...

    public function definition()
    {
        return [
            'name' => $this->faker->name()
        ];
    }

...

Enter fullscreen mode Exit fullscreen mode

MovieFactory.php:


...

    public function definition()
    {
        return [
            'name' => Str::title($this->faker->words(3, true))
        ];
    }

...

Enter fullscreen mode Exit fullscreen mode

Seeders

ActorSeeder.php:


...

use Database\Factories\ActorFactory;

...

    public function run()
    {
        ActorFactory::times(5)->create();
    }

...

Enter fullscreen mode Exit fullscreen mode

MovieSeeder.php:


...

use Database\Factories\MovieFactory;

...

    public function run()
    {
        MovieFactory::times(5)->create();
    }

...

Enter fullscreen mode Exit fullscreen mode

MovieActorSeeder.php:


...

use App\Models\Actor;
use App\Models\Movie;
use App\Models\MovieActor;

...

    public function run()
    {
        $actors = Actor::paginate(5);
        $movies = Movie::paginate(5);

        foreach ($movies as $movie) {
            foreach ($actors as $actor) {
                MovieActor::firstOrCreate([
                    'movie_id' => $movie->id,
                    'actor_id' => $actor->id,
                ]);
            }
        }
    }

...

Enter fullscreen mode Exit fullscreen mode

And we need to register these seeders into database\seeders\DatabaseSeeder.php:


...

    public function run()
    {
        $this->call([
            ActorSeeder::class,
            MovieSeeder::class,
            MovieActorSeeder::class,
        ]);
    }

...

Enter fullscreen mode Exit fullscreen mode

Models

A little change for the MovieActor model because we don't need the default timestamps:

MovieActor.php:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class MovieActor extends Model
{
    public $timestamps = false;
}
Enter fullscreen mode Exit fullscreen mode

After finish populating the files, now you can do the:

php artisan migrate --seed
Enter fullscreen mode Exit fullscreen mode

And then check the data generated in the database to confirm we had everything we need to do the many-to-many operations.

Eloquent Many-to-Many Relationship

Now it's time for the magic. Eloquent has a method called hasManyThrough() to use the pivot in the relationship method. Here is the relationship method for Movie and Actor models along with my commented-explanation for the usage of the argument:

Movie.php:


...

    public function actors()
    {
        return $this->hasManyThrough(
            // required
            'App\Models\Actor', // the related model
            'App\Models\MovieActor', // the pivot model

            // optional
            'movie_id', // the current model id in the pivot
            'id', // the id of related model
            'id', // the id of current model
            'actor_id' // the related model id in the pivot
        );
    }

...

Enter fullscreen mode Exit fullscreen mode

Actor.php:


...

    public function movies()
    {
        return $this->hasManyThrough(
            // required
            'App\Models\Movie', // the related model
            'App\Models\MovieActor', // the pivot model

            // optional
            'actor_id', // the current model id in the pivot
            'id', // the id of related model
            'id', // the id of current model
            'movie_id' // the related model id in the pivot
        );
    }

...

Enter fullscreen mode Exit fullscreen mode

Test

You simply test to check the relationship works and return the relations using tinker:

php artisan tinker
Enter fullscreen mode Exit fullscreen mode

And these interactive scripts should show if it works or not:

\App\Models\Movie::first()->actors;

\App\Models\Actor::first()->movies;
Enter fullscreen mode Exit fullscreen mode

Hope this useful.


Laravel version used: 8.9
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
pelmered profile image
Peter Elmered

Hm.. shouldn't it be $this->belongsToMany(''App\Models\Movie'')->using('App\Models\MovieActor') when you want many-to-many with an intermediate model?
I also much prefer to import the models and use Movie::class instead of strings. That makes it much easier for the IDE to understand the code and helps when refactoring.

Here's the relevant documentation: laravel.com/docs/8.x/eloquent-rela...

Otherwise, it was a solid article!