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
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
:
...
public function up()
{
Schema::create('movies', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
}
...
xxx_xx_xx_xxxxxx_create_actors_table.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
:
...
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
:
...
public function definition()
{
return [
'name' => $this->faker->name()
];
}
...
MovieFactory.php
:
...
public function definition()
{
return [
'name' => Str::title($this->faker->words(3, true))
];
}
...
Seeders
ActorSeeder.php
:
...
use Database\Factories\ActorFactory;
...
public function run()
{
ActorFactory::times(5)->create();
}
...
MovieSeeder.php
:
...
use Database\Factories\MovieFactory;
...
public function run()
{
MovieFactory::times(5)->create();
}
...
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,
]);
}
}
}
...
And we need to register these seeders into database\seeders\DatabaseSeeder.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
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
:
...
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
:
...
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:
\App\Models\Movie::first()->actors;
\App\Models\Actor::first()->movies;
Hope this useful.
Laravel version used: 8.9
Top comments (1)
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!