Polymorphic relations give us a very useful flexibility is specific cases. In this article we’ll see how to proper eager load nested relations based on models with polymorphic relations using Laravel’s default ORM Eloquent.

They are useful when an entity is shared across multiple Models or when a Model has a relation that can match in one of multiple models. For example

  1. A model with properties shared across other different models as shown in Laravel’s documentation, where an Image can be used in a Post as well as a User
  2. Or a Model that has additional properties (a relation) that vary based on some properties of the model itself that can point to different models

It’s about this last case that I’ll be focus here. A while ago I was coding a Laravel package, similar to league/fractal and spatie/laravel-query-builder and the purpose of this package is to add specific features to Eloquent Model for REST APIs. Based on what’s given on the request’s query string the API can retrieve more information or filter data.

Below is an example of a request and what an API using the package should return. The request should retrieve the information of the user with id = 1 and include all the posts created by the user and all comments related to those posts:

/users/1?include=posts.comments

{
  "id": 1,
  "name": "Luís Cruz",
  "posts": {
    "data": [
      0 => {
        "id": 123,
        "title": "Eager Load nested relations of a MorphTo in Laravel Eloquent ORM",
        "url": "eager-load-nested-relations-of-morph-to-laravel-eloquent",
        "comments": {
          "data": [
            0 => {
              "id": 70,
              "subject": "Great post",
              "content": "Thank you for the information provided"
            }
          ]
        }
      }
    ]
  }
}

This works very well with “normal” relations such as BelongsTo, HasMany, HasOne, etc. However, for polymorphic relations (Morphto) this gets tricky when using Eager Loading.

Let’s go through a mock scenario where you have the following Models:

  • Installment: Model that records all the generic information about a debt installment (for eg, home loans)
  • InstallmentCc: Model that records the installment information when payed with a credit card, related to Installment
  • InstallmentBt: Model that records the installment information when payed with a bank transfer, related to Installment
  • InstallmentBtInfo: Model related to InstallmentBt with additional information about the bank transfer

The database schema would look like:

installments
    id - integer
    due_date - datetime
    methodable_id - int
    methodable_type - string

installments_ccs
    id - integer
    token - string

installments_bts
    id - integer
    iban - string
    swift - string

installments_bts_infos
    id - integer
    subject - string
    info - string

And the Model structure:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Installment extends Model
{
    /**
     * Get additional information about the installment, based on the payment method
     */
    public function methodable()
    {
        return $this->morphTo();
    }
}

class InstallmentCc extends Model
{
}

class InstallmentBt extends Model
{
    public function info()
    {
        return this->hasMany(InstallmentBtInfo::class);
    }
}

class InstallmentBtInfo extends Model
{
}

So the installment information is stored partially on the Installment and in one of two Models: either InstallmentCC or InstallmentBt based on the chosen payment type. This means we have a polymorphic relation called methodable on the Installment model that will retrieve InstallmentCc or InstallmentBt based on the payment type. Additionally there’s a info relationship in InstallmentBt related to the InstallmentBt model.

In order for us to load relations with Eager Loading we’ll use Eloquent’s with() method. So, if we want to retrieve all the installment information, the request and response should be similar to:

/installments?include=methodable.info

[
    0 => {
        "id": 1,
        "due_date": "2019-05-30T23:59:59",
        "methodable_id": 1,
        "methodable_type": "App\Models\InstallmentCc",
        "methodable": {
            "id": 1,
            "token": "123456789"
        }
    },
    1 => {
        "id": 2,
        "due_date": "2019-06-30T23:59:59",
        "payed_at": "2019-06-21T08:43:54",
        "methodable_id": 1,
        "methodable_type": "App\Models\InstallmentBt",
        "methodable": {
            "id": 1,
            "iban": "PT15154823651254789625417",
            "swift": "BWF12487",
            "info": {
                "data": [
                    0 => {
                        "subject": "Bank info: Insufficient Funds",
                        "info": "Code 0980: bank account with insufficient funds",
                        "updated_at": "2019-06-20T15:01:36"
                    },
                    0 => {
                        "subject": "Bank info: Insufficient Funds",
                        "info": "Code 0980: bank account with insufficient funds",
                        "updated_at": "2019-06-21T08:43:12"
                    }
                ]
            }
        }
    }
]

As of Laravel 5.8.22 (2019-06-12) there’s a new morphWith method and to load nested relations with Eager Loading you can use:

Installment:with('methodable', function (MorphTo $morphTo) {
    $morphTo->morphWith([InstallmentBt::class => ['info']]);
})->get();

This code reads as For all the Installments load the methodable relation. For those, if the loaded Model is a InstallmentBt then load the info relation as well.

At this point we know exactly what we want to retrieve. However, in a package where all things should be dynamic enough it gets trickier. Why? Well, in this package when the query string is being processed, in order to know if a given model has the relation (ie: it wasn’t a typo) I’m using getRelated() on the relation.

For instance, the following code will retrieve App\Models\InstallmentBtInfo because it is the model associated with the info relation of the Installment.

$installmentBt = new InstallmentBt();
dd(get_class($installmentBt->info->getRelated()));

But in a polymorphic relation this does not work, well as is shown in the following code:

$installment = new Installment();
dd(get_class($installment->methodable->getRelated()));

The retrieved class is App\Models\Installment. Since this is a polymorphic relation Eloquent cannot know what will be the related model before it executes the query, so the related model will be the model itself.

For that reason, and going back to the request /installments?include=methodable.info we cannot know the if the related model of the methodable relation has a info relation. Furthermore, for Eloquent the info relation doesn’t exist at all, because as stated, the getRelated method on top of the methodable relation will return the Installment.

So there’s only one way to properly solve this: ignore the info on the request and change the methodable relation on Installment to include the morphWith. So the request would be /installments?include=methodable and the code:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Installment extends Model
{
    /**
    * Get additional information about the installment, based on the payment method
    */
    public function methodable()
    {
        return $this->morphTo()->morphWith([
            InstallmentBt::class => ['info']
        ]);
    }
}

This way whenever you load the polymorphic relation, the nested / child relations will also be loaded.

Article image downloaded from Background vector created by vector_corp - www.freepik.com