Response
Lift\Http\Response — це незмінний об’єкт HTTP-відповіді. Він реалізує Psr\Http\Message\ResponseInterface і надає фабричні методи для частих випадків (JSON, HTML, текст, редирект, без вмісту), а також помічники для cookie та плавний білдер.
Ментальна модель: зберіть
Response, поверніть його з обробника — Lift надішле його клієнту. Як іRequest, він незмінний — кожен методwith*()повертає новий екземпляр.
Найкоротша можлива відповідь
$app->get('/', fn() => Response::json(['hello' => 'world']));
Ось і все. Якщо вам не потрібно задавати власні заголовки чи коди стану, фабричні методи — найчистіший API.
Фабричні методи
Response::json($data, $status = 200, $flags = ...)
Надсилає масив/об’єкт як JSON із Content-Type: application/json; charset=utf-8.
Response::json(['status' => 'ok']); // 200 OK
Response::json(['error' => 'Conflict'], 409); // власний статус
Response::json($data, 200, JSON_PRETTY_PRINT); // власні прапори кодування
Прапори за замовчуванням включають JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES (вони майже завжди вам потрібні). Помилки кодування викидають JsonException — ніколи не дають мовчки зіпсований вивід.
Response::html($content, $status = 200)
Response::html('<h1>Hello</h1>');
Response::html($view->render('home'), 200);
Content-Type: text/html; charset=utf-8 встановлюється автоматично.
Response::text($content, $status = 200)
Response::text('pong');
Response::text("Hello, $name", 200);
Content-Type: text/plain; charset=utf-8.
Response::redirect($url, $status = 302, $headers = [])
Response::redirect('/login'); // 302 Found
Response::redirect('/new-home', 301); // 301 Moved Permanently
Response::redirect('/after-post', 303); // 303 See Other (патерн POST → GET)
Response::redirect('/short-cache', 307); // 307 Temporary Redirect (зберігає метод)
Response::redirect('/forever', 308); // 308 Permanent Redirect (зберігає метод)
Третій аргумент $headers підмішує додаткові заголовки у відповідь-редирект:
// Редирект і очищення cookie за один раз
Response::redirect('/login', 302, ['Clear-Site-Data' => '"cookies"']);
// Редирект із власним керуванням кешем
Response::redirect('/new-home', 301, ['Cache-Control' => 'no-store']);
Можна також зчепити ->withHeader(...) / ->withCookie(...) на результаті:
return Response::redirect('/dashboard')
->withCookie('flash', 'Welcome back!');
Response::noContent()
return Response::noContent(); // 204, порожнє тіло
Використовуйте, коли DELETE / PUT тощо успішні, але повертати нічого.
Плавний білдер (у стилі PSR-7)
Для всього, що не покривають фабрики, використовуйте ланцюжки with*(). Кожен виклик повертає новий екземпляр:
return (new Response())
->withStatus(201)
->withHeader('Location', '/users/42')
->withHeader('X-Request-Id', $id)
->withJson(['id' => 42]); // задає тіло + Content-Type, зберігає статус
// ->withJson(['id' => 42], 201); // необов’язковий другий аргумент перевизначає код статусу
Тонкий, але частий баг:
// ❌ НЕПРАВИЛЬНО — withHeader повертає новий об’єкт; цей код його викидає.
$res = Response::json($data);
$res->withHeader('X-Custom', 'value');
return $res;
// ✅ ПРАВИЛЬНО
$res = Response::json($data)->withHeader('X-Custom', 'value');
return $res;
Автоматичне перетворення
Якщо обробник повертає щось, що не є Response, Lift перетворює це за вас:
| Повернене значення | Що ви отримуєте назад |
|---|---|
Response |
передається без змін |
array, object |
Response::json(...) |
string |
Response::html(...) |
null |
Response::noContent() (204) |
| будь-що інше | Response::text((string) $v) |
Тож ці два обробники ідентичні:
fn() => ['ok' => true]
fn() => Response::json(['ok' => true])
Обирайте те, що читається краще. Підказка: явний Response::json(...) сяє щоразу, коли вам також потрібен код статусу або заголовок — вони все одно змушують використовувати Response.
Cookie
Відповідь Lift несе першокласні помічники для cookie — вам не потрібен setcookie() PHP.
return Response::json($user)
->withCookie('remember_token', $token, [
'max_age' => 86400 * 30, // 30 днів
'http_only' => true, // за замовчуванням true
'same_site' => 'Lax', // за замовчуванням 'Lax'
'secure' => true, // надсилати лише через HTTPS
'path' => '/', // за замовчуванням '/'
'domain' => 'example.com',// необов’язково
]);
Швидко видалити cookie:
return Response::noContent()->withoutCookie('remember_token');
Прочитайте значення на наступному запиті через
$req->cookie('remember_token').
Довідник опцій cookie
| Ключ | Тип | За замовчуванням | Ефект |
|---|---|---|---|
max_age |
int | — | Max-Age=N секунд. Рекомендується замість expires. |
expires |
int | — | Unix-мітка часу. Ігнорується, коли задано max_age. |
path |
string | / |
Префікс URL, до якого застосовується cookie. |
domain |
string | — | Домен cookie (контроль піддоменів). |
secure |
bool | false |
Додає прапор Secure (лише HTTPS). |
http_only |
bool | true |
Додає прапор HttpOnly (без доступу з JS). |
same_site |
string | Lax |
Strict / Lax / None. |
Власні коди стану
return Response::json(['error' => 'I refuse to brew coffee.'], 418);
// Власна фраза причини
return (new Response())->withStatus(418, "I'm a teapot");
Lift знає стандартні фрази (200 OK, 404 Not Found тощо) — фразу ви передаєте, лише якщо хочете її перевизначити.
Доступ до тіла / зміна тіла
$stream = $res->getBody(); // Psr\Http\Message\StreamInterface
$content = (string) $res->getBody(); // рядок
// Замінити тіло
$newRes = $res->withBody(\Lift\Http\Stream::fromString('hello'));
Більшість коду ніколи не торкається тіла напряму — фабричні методи + withJson() покривають 99% випадків.
Встановлення заголовків
$res = Response::json($data)
->withHeader('Cache-Control', 'public, max-age=3600')
->withHeader('X-Total-Count', '42')
->withAddedHeader('Set-Cookie', 'a=1') // додати (не замінювати)
->withAddedHeader('Set-Cookie', 'b=2');
withHeader() замінює будь-яке наявне значення; withAddedHeader() додає (використовуйте, коли заголовок законно з’являється більше одного разу, як Set-Cookie).
Стримінг і Server-Sent Events
Для довгоживучих відповідей (Server-Sent Events, читання хвоста логів тощо) використовуйте SseResponse — див. Server-Sent Events.
Надсилання власних бінарних / файлових відповідей
Lift не постачає помічник Response::file() (це мікрофреймворк, а не CMS), але це однорядковий код:
use Lift\Http\Stream;
$path = '/storage/exports/report.csv';
return (new Response())
->withHeader('Content-Type', 'text/csv')
->withHeader('Content-Disposition', 'attachment; filename="report.csv"')
->withHeader('Content-Length', (string) filesize($path))
->withBody(Stream::fromFile($path));
(Див. Lift\Http\Stream для фабричних методів — fromString, fromFile, fromInput, empty.)
Шпаргалка з кодів стану
| Код | Використовувати для |
|---|---|
| 200 | OK — будь-що з тілом, що не створило ресурс |
| 201 | Created — POST, що створив ресурс |
| 202 | Accepted — поставлено в чергу на асинхронну обробку |
| 204 | No Content — успішний DELETE / PUT, якому нема чого повертати |
| 301 | Moved Permanently — старий URL, назавжди |
| 302 | Found — тимчасовий редирект (браузери можуть змінити метод на GET) |
| 303 | See Other — редирект POST → GET після надсилання форми |
| 307 | Temporary Redirect — як 302, але зберігає HTTP-метод |
| 308 | Permanent Redirect — як 301, але зберігає HTTP-метод |
| 400 | Bad Request — некоректний запит |
| 401 | Unauthorized — відсутні/невірні облікові дані |
| 403 | Forbidden — автентифікований, але не дозволено |
| 404 | Not Found |
| 405 | Method Not Allowed |
| 409 | Conflict — наприклад, дублікат унікального обмеження |
| 422 | Unprocessable Entity — валідація не пройшла (за замовчуванням для ValidationException Lift) |
| 429 | Too Many Requests — перевищено ліміт частоти |
| 500 | Internal Server Error |
| 502 | Bad Gateway — збій вищележного сервісу |
| 503 | Service Unavailable — обслуговування / перевантаження |
Часті підводні камені
| Симптом | Причина | Виправлення |
|---|---|---|
| Заголовки не з’являються | Ви викликали withHeader(), але не захопили результат |
Присвойте назад: $res = $res->withHeader(...);. |
JSON_THROW_ON_ERROR зривається |
Не-UTF-8 рядок у корисному навантаженні | Очистіть ввід; або Response::json($data, 200, JSON_INVALID_UTF8_IGNORE). |
Браузер ігнорує Set-Cookie |
Невірні атрибути cookie (Secure по HTTP, неузгоджений домен) |
Приберіть secure для локальної розробки, перевірте domain/path. |
Порожній JSON {} замість масиву [] |
json_encode([]) коректно; трапляється, коли ви передаєте порожній асоціативний масив |
Передавайте list<...> (наприклад, array_values($items)), коли хочете []. |
| Текст статусу нічого не каже | Ви передали порожню фразу причини | Або не передавайте нічого (Lift підставить), або вкажіть свій рядок. |
Шпаргалка
// Фабрики
Response::json($data, $status?, $flags?);
Response::html($html, $status?);
Response::text($text, $status?);
Response::redirect($url, $status?);
Response::noContent();
// Плавний стиль
(new Response())
->withStatus(201)
->withHeader('X-Foo', 'bar')
->withAddedHeader('Set-Cookie', '...')
->withJson($data);
// Cookie
$res->withCookie($name, $value, [...]);
$res->withoutCookie($name);
// Тіло
$res->getBody(); // StreamInterface
(string) $res->getBody();
$res->withBody(Stream::fromString($html));