Отношения в Laravel в принципе довольно удобно сделаны и работать с ними приятно. Но бывают ситуации, решения для которых находятся в поиске и доках не сразу. Приведу пару небольших примерчиков.
Метод morphWith
Конкретно этот метод есть в доках, но он появился совсем недавно, в версии 5.8.22 (а текущая — 5.8.25) и не сразу нашёлся именно поэтому. Суть вот в чем. Допустим, у нас есть модель Event
, которая радостно полиморфирует с другими моделями. Для неё отношения описаны так:
class Event extends Model
{
protected $with = ['parent'];
public function parent() {
return $this->morphTo();
}
// ...
}
Как мы видим, благодаря свойству $with
модель будет сразу загружаться с родителем, независимо от того, что это за родитель. Однако что, если нам нужно разом загрузить и какие-то отношения этого (неопределенного пока) родителя с другими моделями. Допустим, модели Payment
и Deadline
связаны полиморфно с Event
, и в то же время модель Payment
связана с моделью Order
, а Deadline
— скажем, с моделью User
. Примерно так:
class Payment extends Model
{
public function events() {
return $this->morphMany('App\Event', 'parent');
}
public function order() {
return $this->belongsTo(Order::class);
}
// ...
}
// (в другом, конечно, файле)
class Deadline extends Model
{
public function events() {
return $this->morphMany('App\Event', 'parent');
}
public function User() {
return $this->belongsTo(User::class);
}
// ...
}
И мы хотим, чтобы платежи грузились с заказами, а дедлайны — с пользователями. Конечно, мы могли бы воспользоваться свойством $with
для моделей Payment
и Deadline
. Но что если в Order
и User
уже используется свойство $with
для загрузки платежей и дедлайнов? Тогда мы не сможем воспользоваться этим свойством, поскольку уйдем в рекурсию.
Тут-то и поможет нам метод morphWith()
:
$events = Event::query()
->with(['parent' => function (MorphTo $morphTo) {
$morphTo->morphWith([
Payment::class => ['order'],
Deadline::class => ['user'],
]);
}])
->get();
То есть что мы тут делаем? Мы вызываем для отношения MorphTo
метод morphWith()
и для каждого класса родителя, для которого нужно грузить его отношения, указываем массив из этих отношений. И они грузятся.
А что если надо отменить eager loading
Может возникнуть и прямо противоположная проблема. Допустим, мы автоматически грузим модель Payment
из Order
через свойство $with
— это будет работать всякий раз, когда модель заказа загружается. Но что если нам в данном конкретном случае нужно автоматически загрузить заказы для группы платежей, и не попасть при этом ни в рекурсию, ни в «проблему N+1 запросов»?
Очевидно, нужно отключить eager-loading. Вот два примера, как можно это сделать.
1) Если может возникнуть рекурсия из-за $with
, можно отключить автозагрузку прямо в объявлении отношения:
public function order() {
return $this->belongsTo(Order::class)->setEagerLoads([]);
}
Здесь мы говорим, что при загрузке заказов для данных моделей, не нужно автоматически грузить никакие модели, связанные с заказами (и перечисленные в Order::$with
). Кстати, вместо пустого массива мы можем передать массив из отношений, которые всё же нужно загрузить, ограничив таким образом автозагрузку заданным списком.
2) Если же мы прогружаем отношения явным образом с помощью метода load()
, можно отключить автозагрузку вот так:
$payment->load(['order' => function ($query) {
$query->setEagerLoads([]);
}]);
Напоследок замечу вот еще что. Хотя свойство $with
и довольно удобно в целом, так как работает автоматически, бездумное его использование представляется частным случаем преждевременной оптимизации. Когда мы не знаем, как часто и какие зависимости нам потребуются, велик соблазн пихнуть их все в массив $with
и забыть. Однако позже, при описании уже реальных кейсов, вполне может возникнуть ситуация, когда отношения, перечисленные в автозагрузке придётся удалять и переделывать одно за другим.
Не нужно, в общем, торопиться с оптимизацией.