Laravel: Eloquent - Many to Many Relationship

Carlos Costa

Many-to-many relationships are slightly more complicated than hasOne and hasMany relationships. An example of such a relationship is a user with many roles, where the roles are also shared by other users.

In this note we demostrate how to create a many-to-many relationship between two models and how to create a pivot table to store the relationship.

Let’s define a relationship between Client and Flight models.

First, we need to create our files.

php artisan make:model Client -mf --pest
php artisan make:model Flight -mf --pest
php artisan make:model Tickets -mfp --pest

-p flag indicate that we want to create a pivot table.

Migrations


We need to create the migrations for our models.

//client
Schema::create('clients', function (Blueprint $table) {
    $table->id();
    $table->timestamps();
    $table->string('name');
    $table->string('email');
    $table->integer('age');
});

//flight
Schema::create('flights', function (Blueprint $table) {
    $table->id();
    $table->timestamps();
    $table->string('name');
    $table->string('destination');
    $table->dateTime('departure');
});

//tickets
Schema::create('ticket', function (Blueprint $table) {
    $table->foreignId('flight_id');
    $table->foreignId('client_id');
    $table->primary(['flight_id', 'client_id']);

    $table->timestamps();
    $table->string('seat');
});

Models


To that relationship work we need to define 3 models.

//client
class Client extends Model
{
    use HasFactory;

    public function flights()
    {
        return $this->belongsToMany(
            Flight::class,
            'tickets', //table name
            'flight_id', //foreign key in pivot table
            'client_id' //foreign key in pivot table
        )
        ->as('ticket') //alias for the pivot table
        ->withTimestamps() //timestamps are optional
        ->withPivot('seat') //pivot table column
        ->using(Ticket::class); //pivot table model
    }
}

//flight
class Flight extends Model
{
    use HasFactory;

    public function clients()
    {
        return $this->belongsToMany(
            Clients::class,
            'tickets', //table name
            'flight_id', //foreign key in pivot table
            'client_id' //foreign key in pivot table
        )
        ->as('ticket') //alias for the pivot table
        ->withTimestamps() //timestamps are optional
        ->withPivot('seat') //pivot table column
        ->using(Ticket::class); //pivot table model
    }
}

//ticket
class Tickets extends Pivot
{
    //
}

Factories


We need to create factories for our models.

//client
class ClientFactory extends Factory
{
    protected $model = Client::class;

    public function definition()
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'age' => $this->faker->numberBetween(18, 80),
        ];
    }
}

//flight
class FlightFactory extends Factory
{
    protected $model = Flight::class;

    public function definition()
    {
        return [
            'flight_number' => $this->faker->unique()->randomNumber(4),
            'departure_airport' => $this->faker->randomElement(['LAX', 'SFO', 'JFK', 'DEN', 'ORD']),
            'arrival_airport' => $this->faker->randomElement(['LAX', 'SFO', 'JFK', 'DEN', 'ORD']),
            'departure_time' => $this->faker->dateTimeBetween('now', '+1 year'),
        ];
    }
}

Tests


To finally let’s apply some tests to our models.

public function test_models(): void
{
    //create a client
    $client = Client::factory()->create();

    //create a flight
    $flight = Flight::factory()->create();

    //attach the client to the flight
    $flight->clients()->attach($client->id, ['seat' => 'A1']);

    //check if the client is attached to the flight
    $ticket = Ticket::where('client_id', $client->id)->first();

    $this->assertEquals($ticket->seat, 'A1');
}

Reference