Проблема: ресурс звертається до зв'язку, який не завантажено, - і на кожен елемент колекції виконується окремий запит.
// у ресурсі
'author' => new UserResource($this->author), // N+1 для колекції з 50 постів - 51 запит
whenLoaded включає зв'язок у відповідь лише якщо його вже завантажено - і сам нічого не завантажує:
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'author' => new UserResource($this->whenLoaded('author')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
'comments_count' => $this->whenCounted('comments'),
'average_rating' => $this->whenAggregated('reviews', 'rating', 'avg'),
];
}
}
Якщо зв'язок не завантажено, ключ зникає з відповіді (а не стає null).
Контролер вирішує, що завантажити:
$posts = Post::query()
->with(['author', 'tags'])
->withCount('comments')
->withAvg('reviews', 'rating')
->paginate();
return PostResource::collection($posts);
Той самий ресурс у списку (мінімум зв'язків) і на сторінці деталей (більше зв'язків) - різна кількість полів без двох окремих класів.
Включення на вимогу клієнта (?include=author,tags) - дозволений перелік, а не довільні зв'язки:
$allowed = ['author', 'tags', 'comments'];
$includes = array_intersect(explode(',', $request->string('include')), $allowed);
$posts = Post::with($includes)->paginate();
Пакет spatie/laravel-query-builder робить це разом із фільтрами й сортуванням. Вбудований JsonApiResource у Laravel 13 підтримує include за специфікацією JSON:API.
Інші помічники ресурсів:
$this->when($condition, $value)- поле за умовою (права, контекст);$this->mergeWhen($condition, [...])- кілька полів разом;$this->whenPivotLoaded('role_user', fn () => ...)- дані проміжної таблиці.
Як ловити N+1 в API:
Model::preventLazyLoading(! app()->isProduction())- виняток при ледачому завантаженні в розробці й тестах;- Telescope, Debugbar - кількість запитів на ендпойнт;
- тест, що перевіряє кількість запитів для колекції (
DB::enableQueryLog()/expectsDatabaseQueryCount).
Пастка: whenLoaded приховує «відсутні» дані - клієнт може не помітити, що зв'язок перестав приходити, бо контролер забув with(). Тому у важливих ендпойнтах варто мати тести структури відповіді.