API-ресурсы
Зачем нужны ресурсы
Без ресурса:
return Post::find(1);
В JSON попадёт всё:
{
"id": 1,
"title": "...",
"body": "...",
"user_id": 5,
"created_at": "2026-10-15T12:00:00Z",
"updated_at": "...",
"deleted_at": null,
"meta": "...",
"internal_notes": "..."
}
Проблемы:
- Лишние поля — размер ответа.
- Приватные данные — утечка.
- Формат БД — не подходит клиенту.
- Нет вычисляемых полей — приходится считать на клиенте.
Resource
Resource — класс, который преобразует модель в массив для JSON.
Плюсы:
- Выбор полей — только нужные.
- Переименование — snake_case → camelCase.
- Вычисляемые поля.
- Вложенные связи.
- Условные поля.
- Метаданные.
Создание
php artisan make:resource PostResource
Файл app/Http/Resources/PostResource.php.
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'body' => $this->body,
'published_at' => $this->published_at?->toISOString(),
'created_at' => $this->created_at->toISOString(),
];
}
}
$this — модель. Доступ через свойства, не $this->resource->title.
Использование
Один:
public function show(Post $post): PostResource
{
return new PostResource($post);
}
Коллекция:
public function index()
{
return PostResource::collection(Post::paginate(15));
}
Вложенные ресурсы
return [
'id' => $this->id,
'title' => $this->title,
'author' => new UserResource($this->whenLoaded('user')),
'comments' => CommentResource::collection($this->whenLoaded('comments')),
];
whenLoaded — включает, только если связь загружена.
whenLoaded
Проблема: если в контроллере нет with('user'), обращение к $this->user вызовет N+1.
Решение: whenLoaded.
'user' => new UserResource($this->whenLoaded('user')),
Что происходит:
- Загружено → включает в ответ.
- Не загружено → поле не появляется.
В контроллере:
return PostResource::collection(
Post::with('user')->paginate(15)
);
whenCounted
'comments_count' => $this->whenCounted('comments'),
Работает, если в контроллере:
Post::withCount('comments')->get();
when
Условные поля:
return [
'id' => $this->id,
'title' => $this->title,
'email' => $this->when(
$request->user()?->isAdmin(),
$this->email,
),
];
Что делает: если условие true — поле в ответе, иначе — нет.
С альтернативой:
'secret' => $this->when($isAdmin, 'value', 'hidden'),
mergeWhen
Группа полей:
return [
'id' => $this->id,
$this->mergeWhen($request->user()?->isAdmin(), [
'email' => $this->email,
'phone' => $this->phone,
'internal_notes' => $this->internal_notes,
]),
];
whenHas
Если атрибут есть:
'deleted_at' => $this->whenHas('deleted_at'),
Условные вычисляемые
'excerpt' => $this->when(
$request->boolean('with_excerpt'),
fn () => mb_substr($this->body, 0, 100),
),
fn — ленивое вычисление (только если нужно).
Обёртка data
По умолчанию:
{
"data": {
"id": 1,
"title": "..."
}
}
Коллекция:
{
"data": [
{ "id": 1 },
{ "id": 2 }
]
}
Отключить глобально:
// AppServiceProvider::boot()
use Illuminate\Http\Resources\Json\JsonResource;
public function boot(): void
{
JsonResource::withoutWrapping();
}
Для одного ресурса:
class PostResource extends JsonResource
{
public static $wrap = null;
}
Метаданные
with:
public function with(Request $request): array
{
return [
'meta' => [
'version' => '1.0',
'generated_at' => now()->toISOString(),
],
];
}
Ответ:
{
"data": { ... },
"meta": { "version": "1.0" }
}
Ресурс коллекции
php artisan make:resource PostCollection
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class PostCollection extends ResourceCollection
{
public function toArray(Request $request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'per_page' => $this->perPage(),
],
];
}
}
Полезно для кастомного формата пагинации.
Пагинация
PostResource::collection(Post::paginate(15)) вернёт:
{
"data": [...],
"links": {
"first": "...",
"last": "...",
"prev": null,
"next": "..."
},
"meta": {
"current_page": 1,
"per_page": 15,
"total": 73
}
}
Работает из коробки.
Полный пример
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => mb_substr($this->body, 0, 100),
'body' => $this->when(
$request->routeIs('posts.show'),
$this->body,
),
'status' => $this->status,
'views' => $this->views,
'author' => new UserResource($this->whenLoaded('user')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
'comments_count' => $this->whenCounted('comments'),
'published_at' => $this->published_at?->toISOString(),
'created_at' => $this->created_at->toISOString(),
'updated_at' => $this->updated_at->toISOString(),
'links' => [
'self' => route('api.posts.show', $this->id),
'comments' => route('api.posts.comments.index', $this->id),
],
];
}
public function with(Request $request): array
{
return [
'meta' => [
'version' => 'v1',
],
];
}
}
Контроллер:
public function index()
{
$posts = Post::query()
->with(['user:id,name,avatar', 'tags:id,name'])
->withCount('comments')
->latest()
->paginate(15);
return PostResource::collection($posts);
}
Практика
Создай PostResource:
php artisan make:resource PostResource
Опиши поля:
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'excerpt' => mb_substr($this->body, 0, 100),
'author' => new UserResource($this->whenLoaded('user')),
'created_at' => $this->created_at->toISOString(),
];
}
Используй в контроллере:
public function show(Post $post): PostResource
{
return new PostResource($post->load('user'));
}
public function index()
{
return PostResource::collection(
Post::with('user')->paginate(15)
);
}
Проверь через curl:
curl http://localhost:8000/api/posts/1
curl http://localhost:8000/api/posts
Частые ошибки
1. N+1 без whenLoaded:
'user' => new UserResource($this->user), // N+1 при списке
// Правильно:
'user' => new UserResource($this->whenLoaded('user')),
2. Забыли ->toISOString():
"created_at": { "date": "2026-10-15 12:00:00.000000" }
Решение: $this->created_at?->toISOString().
3. Забыли про data-обёртку:
{ "data": { ... } }
Клиент ждёт без обёртки — сломается. Или отключи обёртку, или используй её на клиенте.
4. Открытые приватные поля:
return ['email' => $this->email]; // если это админ-поле
Решение: $this->when($request->user()->isAdmin(), $this->email).
5. Много данных из модели:
return $this->resource->toArray(); // всё, включая служебное
Правило: явно перечисляй поля.
Итог
- Resource — преобразование модели в JSON.
toArray— что вернуть.whenLoaded— только загруженные связи.whenCounted— счётчики.when— условные поля.PostResource::collection— список.- Обёртка
data— по умолчанию. - Пагинация — из коробки.
Дальше — Resource-контроллеры для API.