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));