Laravel Many-to-Many Pivot Relationship
Tags: laravel, eloquent, mysql, php
Published: 2020-10-09 18:02:01

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](https://stackoverflow.com/questions/3045034/whats-the-correct-name-for-an-association-table-a-many-to-many-relationship)
 
## 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
```
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
```

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
```

## 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`:
```php

...

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

...

```

`xxx_xx_xx_xxxxxx_create_actors_table.php`:
```php

...

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

...

```

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`:
```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']);
        });
    }

...

```

### Factories
`ActorFactory.php`:
```php

...

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

...

```

`MovieFactory.php`:
```php

...

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

...

```

### Seeders
`ActorSeeder.php`:
```php

...

use Database\Factories\ActorFactory;

...

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

...

```

`MovieSeeder.php`:
```php

...

use Database\Factories\MovieFactory;

...

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

...

```

`MovieActorSeeder.php`:
```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,
                ]);
            }
        }
    }

...

```

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

```php

...

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

...

```

### Models

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

`MovieActor.php`:
```php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class MovieActor extends Model
{
    public $timestamps = false;
}
```

After finish populating the files, now you can do the:
```
php artisan migrate --seed
```
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`:
```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
        );
    }

...

```

`Actor.php`:
```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
        );
    }

...

``` 

## Test

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

```
php artisan tinker
```

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

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

\App\Models\Actor::first()->movies;
```

Hope this useful.

___________

```
Laravel version used: 8.9
```