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
- 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
- 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 toInstallment
InstallmentBt
: Model that records the installment information when payed with a bank transfer, related toInstallment
InstallmentBtInfo
: Model related toInstallmentBt
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