Laravel many to many polymorphic Eloquent relationship tutorial

Laravel provides eloquent relationship which provides powerful query builders. In Laravel, eloquent relationships are defined in model classes. Laravel eloquent provides easy ways to create common relationships:

In this article, we will discuss on Laravel's many to many polymorphic eloquent relationship. This is slightly complicated than many to many relationship. For example, in a e-commerce application, an order and wishlist may have multiple items. Same way, an item may be associated with multiple orders and wishlists model. So, a multiple Item model may be associated with multiple Order model as well as multiple Wishlist model.

Example:

Now, let's create an simple example with these models. In this example, we will build relationship between Item model with Order and Wishlist model. We assume that you have created fresh Laravel application. We also assume that you have confiured database connection.

Database tables

In this relationship, we will need four database tables: orders, wishlists, items and itemables. An items table will be associated with orders and wishlists table using itemables intermediate table.

Migration

We need to create four migration table. Run the following four commands into Terminal to create migration classes at database/migrations directory.

php artisan make:migration create_orders_table
php artisan make:migration create_wishlists_table
php artisan make:migration create_items_table
php artisan make:migration create_itemables_table

Below are the migration table fields for these table:

orders migration

In the orders migration, we have defined the following fields:

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('orders', function (Blueprint $table) {
        $table->id();
        $table->integer('user_id');
        $table->string('number');
        $table->timestamps();
    });
}

wishlists migration

Same migration fields are for wishlists table.

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('wishlists', function (Blueprint $table) {
        $table->id();
        $table->integer('user_id');
        $table->string('number');
        $table->timestamps();
    });
}

items migration

The items migration generally contains name and price fields.

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('items', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->decimal('price', 10, 2);
        $table->timestamps();
    });
}

itemables migration

Now we have one intermediate table which stores relationship between items and orders/wishlists tables.

/**
 * Run the migrations.
 *
 * @return void
 */
public function up()
{
    Schema::create('itemables', function (Blueprint $table) {
        $table->id();
        $table->integer('item_id');
        $table->integer('itemable_id');
        $table->string('itemable_type');
        $table->timestamps();
    });
} 

Let's look at the migration fields. In the itemables table, We have item_id, itemable_id and itemable_type fields. The item_id will store id value of items table, itemable_id will store id value of orders or wishlists table and itemable_type will store the Order or Wishlist class name like App\Models\Order or App\Models\Wishlist.

Now run the migrate command to create tables into database.

php artisan migrate

Model

Laravel models are located at app/Models directory. Now, we also need to create the model classes for these tables using following Artisan commands one by one into Terminal.

php artisan make:model Order
php artisan make:model Wishlist
php artisan make:model Item

After we have created the model classes, we will define relationship. Order and Wishlist both model will contain items() method that will call eloquent's morphToMany() method.

First create items() method into Order model which will return morphToMany() method. The morphToMany() method accept two arguments: The name of related model and the name we have that we used for intermediate table name which we will refer to the relationship. In our cases, it is "itemable".

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Order extends Model
{
    use HasFactory;

    /**
     * Get all of the items for the order.
     */
    public function items()
    {
        return $this->morphToMany(Item::class, 'itemable');
    }
}

Same way add, items method into Wishlist model.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Wishlist extends Model
{
    use HasFactory;

    /**
     * Get all of the items for the wishlist.
     */
    public function items()
    {
        return $this->morphToMany(Item::class, 'itemable');
    }
}

Now we need to define reverse relationship. For example, we have an item model record and we want to get all orders and wishlists records that contains the selected item.

For that we need to create orders() and wishlists() method into Item model that will return eloquent's morphedByMany() method. The morphedByMany() method accept two arguments: The name of related model and the name we have that we used for intermediate table name which we will refer to the relationship. In our cases, it is "itemable".

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Item extends Model
{
    use HasFactory;

    /**
     * Get all of the orders that includes this item.
     */
    public function orders()
    {
        return $this->morphedByMany(Order::class, 'itemable');
    }

    /**
     * Get all of the wishlists that includes this item.
     */
    public function wishlists()
    {
        return $this->morphedByMany(Wishlist::class, 'itemable');
    }
}

Route

In routes/web.php file, we have added two new routes for relationship testing.

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ItemController;

Route::get('/order-items', [ItemController::class, 'orderItems']);
Route::get('/wishlist-items', [ItemController::class, 'wishlistItems']);

Controller

As we have added route, also create ItemController using following command.

php artisan make:controller ItemController

Open the controller at app/Http/Controllers/ItemController and create orderItems() and wishlistItems() method. To retrieve the items of the order or wishlist, you can access items property of the model object.

<?php

namespace App\Http\Controllers;

use App\Models\Item;
use App\Models\Order;
use App\Models\Wishlist;
use Illuminate\Http\Request;

class ItemController extends Controller
{
    /**
     * get all items of order
     *
     * @return \Illuminate\Http\Response
     */
    public function orderItems()
    {
        $order_items = Order::find(1)->items;

        dd($order_items);
    }

    /**
     * get all items of wishlist
     *
     * @return \Illuminate\Http\Response
     */
    public function wishlistItems()
    {
        $wishlist_items = Wishlist::find(1)->items;

        dd($wishlist_items);
    }
}

This will return all items record with related model. If the relationship not found, the eloquent will return null record.

In the reverse, you may already have the item model record and you may want to retrieve all orders and wishlists of the item, you may access orders or wishlists property of item model.

/**
 * return all orders and wishlists of item
 *
 * @return \Illuminate\Http\Response
 */
public function index()
{
    $item = Item::find(2);

    // all orders that include item
    $item_orders = $item->orders

    // all wishlists that include item
    $item_wishlists = $item->wishlists
}

This will return parent model record which is associated with item model. If no records found, it will return null records.

I hope this will help you to understand many to many polymorphic relationship.