Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

richwilliamson's avatar

Eloquent Relationship

Hi. I am defining an eloquent relation and I wondered what the best approach would be in describing this structure using Eloquent.

I have 3 tables, products, gifts and a link table that tells me which products have which gifts available to them. A single product can have many gifts and likewise a single gift can be attached to many products. In my admin panel I want to display the associated gifts on a product page and vice versa on the gift page.

Products

| ID | Name |

| 1 | Samsung TV |

| 2 | Sony TV |

| 3 | Xbox One |

Gifts

| ID | Name |

| 1 | T-Shirt |

| 2 | Mug |

Product Gifts

| ID | product_id | gift_id |

| 1 | 1 | 1 |

| 2 | 1 | 2 |

| 3 | 2 | 1 |

| 4 | 3 | 2 |

I have already written something but I'm a bit concerned I've made it more complicated than it needs to be. For example I've setup a Product and Gift model and I've also setup a Model for the ProductGift link table. Then in my Product model defined the following

class Products extends Model
{
    public function productGifts()
    {
        return $this->hasMany('ProductGift');
    }
}

This will return obviously the records fine but they are the "links" from the ProductGift table but not the gifts themselves.

On my Product model simply I want to return the gifts and on the Gift model I want to return the products. Something like this, possible?

class Products extends Model
{
    public function gifts()
    {
        return $this->hasMany('Gift');
    }
}

Any ideas and suggestions would be greatly appreciated. Thank You

0 likes
6 replies
jgreen's avatar
jgreen
Best Answer
Level 5

A couple of thins stand out there.

First, Laravel uses a naming convention for pivot tables, so your Product Gifts table, should actually be gift_product. Next, hasMany() is a one-to-many and you are descripting a many-to-many relationship.

Many-To-Many: A product can have many gifts which can belong to many products. One-To-Many: A product can have many gifts which can belong to a product.

Also, your pivot table doesn't need its own id, you can make the relationship the primary key.

So, the migrations would look something like this:

//Products
class CreateProductsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->increments('id');
            ...
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('products');
    }
}

//Gifts
class CreateGiftsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('gifts', function (Blueprint $table) {
            $table->increments('id');
            ...
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('gifts');
    }
}

//Pivot table
class CreateGiftProductTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('gift_product', function (Blueprint $table) {
            $table->integer('gift_id')->unsigned();
            $table->integer('product_id')->unsigned();
            $table->timestamps();

            $table->unique(['gift_id','product_id']);

            $table->foreign('gift_id')
               ->references('id')
               ->on('gifts');
            $table->foreign('product_id')
               ->references('id')
               ->on('products');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('gift_product');
    }
}

Then your models will automagically resolve the pivot table name and field names.

//Product Model
class Product extends Model
{
    public function gifts()
    {
        return $this->belongsToMany('App\Models\Gift');
    }
}

//Gift Model
class Gift extends Model
{
    public function products()
    {
        return $this->belongsToMany('App\Models\Product');
    }
}

Here you have many to many setup and you can do this:

//show all the gifts for this product
$product = Product::where($id)->firstOrFail();
dd($product->gifts());

//show all the products for this gift
$gift= Gift::where($id)->firstOrFail();
dd($gift->products());

//show all the gifts this product has and what products those gifts belong to:
$product = Product::where($id)->firstOrFail();

$results = [];
foreach($product->gifts() as $gift) {
    $results[$gift->name] = $gift->products();
}

//then act on $results, or this can be done purely in OOP, but I'll let you figure that out.
1 like
richwilliamson's avatar

@jgreen Thank you so much for the extremely detailed explanation I appreciate the time that must have taken! That has worked perfectly and it's doing exactly what I need it to do!

One final question. How would I go about seeding my link "gift_product" table? Obviously I can easily seed my Product and Gift tables ...

public function run()
{
    $factory->define(App\Product::class, function (Faker\Generator $faker) {
        return [
            'sku' => $faker->lexify('??????'),
            'name' => $faker->sentence(5),
            'cost' => $faker->randomNumber(2),
            ...
        ];
    });
}

Then I could get all products and gifts and then loop through doing DB::inserts

$products = Product::all();
$gifts = Gift::all();

foreach ($products as $product) {
    foreach ($gifts as $gift) {
        ...
    }
}

But I wonder if there is a better way, or a more ORM way?

jgreen's avatar

I do most of my work on intranets, so we use targeted test data. As such I don't have much familiarity with Faker. Here's how I seed my many to many data though.

First, seed one set of the relation (Products):

use App\Models\Product;
use Illuminate\Database\Seeder;

class ProductsSeeder extends Seeder
{
    public function run()
    {
        Product::create([
        'field1' => 'value1',
            'field2' => 'value2',
            'field3' => 'value3',
            ...
        ]);
        ....
}

Then seed the other side of the relation (Gifts) and attach them to Products:

use App\Models\Gift;
use App\Models\Product;
use Illuminate\Database\Seeder;

class GiftsSeeder extends Seeder
{
    public function run()
    {
        $gift = Gift::create([
        'field1' => 'value1',
            'field2' => 'value2',
            'field3' => 'value3',
            ...
        ]);
       $gift->products()->attach(Product::where($id)->first()->id);
        //or
       $gift->products()->attach(Product::where('field1','=','value1')->first()->id);
       //this can be repeated for as many products as you wish to attach.
       ...
}

You can also do it in reverse and seed Gifts, then seed Products and attach() Gifts.

I'm sure there is a fluent way to do this with Faker, but I'm not familiar. Also, which ever Seeder is doing the attaching has to be run AFTER the first set of data is seeded. You can control the order in which Seeders run in the DatabaseSeeder class, so to follow the code examples above do Products before Gifts:

Model::unguard();

        $this->call('ProductsSeeder');
        $this->call('GiftsSeeder');

Model::reguard();

And one final tip, if you are going to doing a lot of relations, I recommend having a migration for all of your tables with no relations defined. Then make a migration dedicated to the relations that always runs last.

1 like
richwilliamson's avatar

Brilliant thank you @jgreen that's great you've been a great help! Faker just populates the data in the table with random data, names, slugs, lorem ipsum text etc so doesn't help with the population of the link table.

What you've suggested with the attach should do the job perfectly!

Thanks again I really appreciate the help!

Please or to participate in this conversation.