As relações polimórficas permitem uma flexibilidade bastante útil em casos específicos. Neste artigo vemos como carregar relações descendentes de modelos com relações polimórficas com Eager Loading com o ORM Eloquent, usado por omissão no Laravel.
As relações polimórficas são úteis quando alguma entidade é comum por diversos modelos ou quando um determinado modelo pode desdobrar-se em várias coisas distintas, como por exemplo
- Temos um modelo que pode ser relacionado com vários outros modelos, como apresentado na documentação do Laravel, onde uma Imagem pode ser aplicada tanto num Post como num Utilizador
- Ou temos um modelo pode ter uma relação que corresponda a vários modelos
É este último caso que vou detalhar e tudo surgiu porque há algum tempo escrevi uma package para Laravel, semelhante à league/fractal e à spatie/laravel-query-builder. O objetivo desta package é adicionar funcionalidades a REST APIs que, com determinados parâmetros na query string, permitem devolver dados adicionais ou filtrados.
Abaixo apresento um exemplo de um request, exemplificativo do funcionamento da package, que deve obter os dados do utilizador com id = 1 e incluir todos os seus posts e comentários desses posts:
/users/1?include=posts.comments
{
"id": 1,
"name": "Luís Cruz",
"posts": {
"data": [
0 => {
"id": 123,
"title": "Carregar relações descendentes de MorphTo com Eager Load em Eloquent",
"url": "carregar-relacoes-descendentes-morphto-com-eager-load-eloquent-laravel",
"comments": {
"data": [
0 => {
"id": 70,
"subject": "Great post",
"content": "Thank you for the information provided"
}
]
}
}
]
}
}
Isto funciona muito bem com relações “normais”, do tipo BelongsTo
, HasMany
, HasOne
, etc. Contudo, em relações polimórficas do tipo MorphTo
a coisa complica-se, essencialmente devido ao Eager Loading.
Vamos a um caso específico, em que existem os seguintes modelos:
Installment
: Modelo que regista informações genéricas sobre as prestações de uma qualquer dívida (por exemplo, crédito à habitação)InstallmentCc
: Modelo que regista a informação do pagamento da prestação com cartão de crédito, relacionado com oInstallment
InstallmentBt
: Modelo que regista a informação do pagamento da prestação com transferência bancária, relacionado com oInstallment
InstallmentBtInfo
: Modelo relacionado com oInstallmentBt
com mais informações sobre a transferência bancária
Em termos de dados, seria qualquer coisa como:
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
E a estrutura dos Modelos Eloquent será assim:
<?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
{
}
A informação das prestações é registada parcialmente no modelo Installment
(informação mais genérica) e num dos modelos InstallmentCc
ou InstallmentBt
, mediante o tipo de pagamento. Ou seja, temos uma relação polimórfica chamada methodable
no modelo Installment
que irá devolver uma instância do modelo InstallmentCc
ou InstallmentBt
, mediante o tipo de pagamento. Além disto, o modelo específico InstallmentBt
tem uma relação hasMany
para o InstallmentBtInfo
.
Para carregar as relações através de Eager Loading usamos o with()
do Eloquent Builder. Então, caso queiramos obter toda a informação de todas as prestações, o request e a resposta deveriam ser:
/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"
}
]
}
}
}
]
A partir do Laravel 5.8.22 (12/06/2019) passou a estar disponível o método morphWith
e a query com Eager Loading seria semelhante a:
Installment:with('methodable', function (MorphTo $morphTo) {
$morphTo->morphWith([InstallmentBt::class => ['info']]);
})->get();
Por palavras, este código quer dizer Para todos os Installments, carrega a relação methodable
. Para essas, caso o modelo relacionado seja InstallmentBt
, carrega também a relação info
.
Neste caso, sabemos exatamente o que queremos obter. No entanto, numa package, em que tudo deve ser devidamente dinâmico, fica mais complicado saber isto. Porquê? Bem, no caso desta package, para se processar a query string e verificar se as relações existem efetivamente no modelo pedido, é usado o método getRelated()
sobre a relação.
Por exemplo, o código seguinte devolverá App\Models\InstallmentBtInfo
porque é o modelo relacionado com a relação info
do modelo InstallmentBt
.
$installmentBt = new InstallmentBt();
dd(get_class($installmentBt->info->getRelated()));
A grande dificuldade aqui situa-se na relação polimórfica, porque o seguinte código não devolve o que eventualmente se poderia esperar:
$installment = new Installment();
dd(get_class($installment->methodable->getRelated()));
Neste caso, a classe devolvida é o próprio App\Models\Installment
. Uma vez que a relação é polimórfica, é normal que o Eloquent tenha dificuldades em dizer qual é o modelo relacionado antes de executar a query.
Por esse motivo, e voltando ao request /installments?include=methodable.info
, não nos é possível saber se o modelo resultante da relação methodable
tem uma relação info
. Aliás, para o Eloquent essa relação não existe, porque a relação methodable
devolverá sempre o próprio modelo.
A abordagem correta para esta situação é só uma, passar a ignorar o info
no request que resulta em /installments?include=methodable
e alterar a relação methodable
do Installment
de forma a incluir aí o morphWith
:
<?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']
]);
}
}
Desta forma sempre que for incluída a relação polimórfica as relações descendentes / filhas também serão incluídas.
Imagem do artigo disponibilizada por Background vector created by vector_corp - www.freepik.com