JSON-ресурси
JSON-ресурс — це тонкий клас, який вирішує, які саме поля моделі покидають ваш сервер і як вони виглядають. Контролери залишаються зосередженими на потоці; ресурси володіють провідним форматом.
Використовуйте їх, коли:
- Форма відповіді відрізняється від рядка бази даних (перейменувати
created_at→createdAt, опуститиpasswordтощо). - Та сама модель рендериться однаково в багатьох місцях.
- Вам потрібні узгоджені конверти колекцій (
{"data": [...]}).
Ментальна модель: думайте про ресурс як про відповідь на запитання «як виглядає
Userу JSON?» — оголошено один раз, повторно використовується всюди.
Найпростіший приклад
use Lift\Http\JsonResource;
final class UserResource extends JsonResource
{
public function toArray(): array
{
return [
'id' => $this->value('id'),
'email' => $this->value('email'),
'createdAt' => $this->value('created_at'),
];
}
}
Використання з обробника:
$app->get('/users/{id:\d+}', function (Request $req) use ($repo) {
return new UserResource($repo->find((int) $req->param('id')));
});
Дві речі, на які варто звернути увагу:
- Обробник повернув об’єкт ресурсу напряму — маршрутизатор викликає на ньому
jsonSerialize(), тож він автоматично стає JSON. $this->value('field')читає з того, що ви обгорнули (масив, об’єктArrayAccessчи публічна властивість).
Тіло відповіді:
{ "id": 1, "email": "[email protected]", "createdAt": "2025-..." }
Що приймає value()
$this->resource може бути:
| Обгорнутий тип | Читається через |
|---|---|
array |
$resource[$key] |
ArrayAccess |
$resource[$key] |
object (будь-який клас) |
$resource->$key |
Тож той самий ресурс працює для рядка-масиву, stdClass із PDO чи власної моделі.
new UserResource(['id' => 1, 'email' => '...']);
new UserResource($model); // будь-який об’єкт із публічними властивостями
new UserResource(json_decode($json));
Встановлення коду стану
$app->post('/users', function (Request $req) use ($repo) {
$user = $repo->create($req->json());
return (new UserResource($user))->response(201);
});
->response($status) повертає Lift\Http\Response із тілом JSON і заданим статусом.
Колекції
Поверніть список ресурсів через статичний помічник:
$app->get('/users', function () use ($repo) {
return UserResource::collection($repo->all());
});
collection() обходить будь-який iterable (масив, генератор, результат запиту) й обгортає кожен елемент у new static(...). Маршрутизатор серіалізує отриманий масив ресурсів у:
[
{ "id": 1, "email": "..." },
{ "id": 2, "email": "..." }
]
Обгортання у конверт
Багато API віддають перевагу {"data": [...]}. Обгорніть це явно:
$app->get('/users', function () use ($repo) {
return Response::json([
'data' => UserResource::collection($repo->all()),
'meta' => ['count' => count($repo->all())],
]);
});
…або побудуйте власний підклас UserCollection:
final class UserCollection
{
public function __construct(private readonly iterable $items) {}
public function toArray(): array
{
return [
'data' => UserResource::collection($this->items),
'meta' => ['count' => count((array) $this->items)],
];
}
}
return new UserCollection($repo->all());
(Будь-який об’єкт, чия повернена обробником форма — array, стає JSON — Lift не важливо, що це не JsonResource.)
Умовні поля
Показуйте поля лише для адмінів, але лише адмінам:
public function __construct(
protected readonly mixed $resource,
private readonly bool $includeAdminFields = false,
) {
parent::__construct($resource);
}
public function toArray(): array
{
$data = [
'id' => $this->value('id'),
'email' => $this->value('email'),
];
if ($this->includeAdminFields) {
$data['isStaff'] = (bool) $this->value('is_staff');
$data['lastIp'] = $this->value('last_login_ip');
}
return $data;
}
// Використання:
return new UserResource($user, includeAdminFields: $currentUser->isAdmin());
Вкладені ресурси
У користувача є профіль? Включіть його через інший ресурс:
public function toArray(): array
{
return [
'id' => $this->value('id'),
'email' => $this->value('email'),
'profile' => $this->value('profile') ? new ProfileResource($this->value('profile')) : null,
];
}
Зовнішній виклик jsonSerialize() рекурсивно серіалізує кожен вкладений ресурс — кожен із них є JsonSerializable.
Використання базового форматувальника дат
Вам швидко захочеться узгодженого форматування дат у всіх ресурсах. Виділіть базу:
abstract class BaseResource extends JsonResource
{
protected function date(string $key): ?string
{
$value = $this->value($key);
if ($value === null) return null;
$dt = $value instanceof \DateTimeInterface ? $value : new \DateTimeImmutable($value);
return $dt->format(\DateTimeInterface::ATOM);
}
}
final class UserResource extends BaseResource
{
public function toArray(): array {
return [
'id' => $this->value('id'),
'email' => $this->value('email'),
'createdAt' => $this->date('created_at'),
];
}
}
Генерація через CLI
vendor/bin/lift make:resource UserResource
Створює src/Http/Resources/UserResource.php із правильним скелетом. Див. Консоль.
Порівняння зі звичайними масивами
Для разового return ['id' => ..., 'email' => ...] ресурс надмірний. Використовуйте його, коли у вас два або більше ендпоінтів рендерять те саме, або є логіка формування полів, що заслуговує на ім’я.
Часті підводні камені
| Симптом | Причина | Виправлення |
|---|---|---|
Cannot access offset on object |
Обгорнутий ресурс не реалізує ArrayAccess, але ви індексували його напряму |
Завжди використовуйте $this->value('key'), ніколи $this->resource['key']. |
| Відсутні поля у виводі | Забули додати їх у toArray() |
Редагуйте ресурс, не контролер. |
| Та сама форма, але різні імена полів | Два викликачі хочуть camelCase vs snake_case | Зробіть два ресурси (UserResource, UserApiResource) — композиція замість умов. |
| Вкладена модель витікає всі стовпці БД | Ви повернули $this->value('profile') напряму |
Обгорніть: new ProfileResource($this->value('profile')). |
JsonException: malformed UTF-8 |
Обгорнуті дані містять не-UTF-8 байти (бінарний blob) | Не включайте blob або спершу зробіть base64_encode. |
Шпаргалка
// Визначити
final class UserResource extends JsonResource
{
public function toArray(): array
{
return ['id' => $this->value('id'), 'email' => $this->value('email')];
}
}
// Використати
return new UserResource($user); // → 200 JSON
return (new UserResource($user))->response(201); // власний статус
return UserResource::collection($users); // масив ресурсів
// Читання з обгорнутого значення
$this->value('field');
$this->value('field', $default);