Глава 27 · Практика
Как PHP обслуживает запрос: от браузера до ответа
Разбираем GET, POST, заголовки, тело, PHP-FPM, состояние и границы одного запроса.
Открыть страницу, выполненную PHP, и увидеть результат
Нужно сообщить посетителю, когда работает мастерская. Сначала получим страницу с надписью «Сегодня работаем до 18:00». Вы напишете файл, запустите PHP и откроете адрес в браузере. Для этого опыта не нужны формы, классы, маршрутизаторы или база.
Создайте пустую папку урока. Сохраните следующий код в ней как example.php в UTF-8. <?php начинает участок PHP; $hours — имя нашей переменной; = сохраняет строку справа. Кавычки ограничивают текст, точка склеивает строки, точка с запятой завершает инструкцию. echo отдаёт текст в ответ. Теги h1 и p в этом тексте будут заголовком и абзацем HTML.
- Браузер запрашивает example.php
- PHP сохраняет «до 18:00» в $hours
- echo составляет HTML с этим значением
- Браузер получает HTML и рисует часы работы
<?php
$hours = 'до 18:00';
echo '<h1>Мастерская</h1>';
echo '<p>Сегодня работаем ' . $hours . '</p>';
Ожидаемый результат
<h1>Мастерская</h1><p>Сегодня работаем до 18:00</p>Команда php example.php напечатает именно HTML выше. Чтобы увидеть страницу, выполните из папки урока php -S 127.0.0.1:8080, затем откройте http://127.0.0.1:8080/example.php. Закрывается сервер сочетанием Ctrl+C в его терминале. Двойной щелчок по PHP-файлу не является HTTP-запуском.
В инструментах разработчика браузера откройте Network, обновите страницу и выберите example.php. Request URL — адрес, Method — GET, Status — 200, Response — созданный HTML. Исходный PHP выполняется на сервере; браузер не исполняет $hours. Если браузер получает PHP-код как текст, остановитесь и исправьте настройку сервера.
Измените в файле 18:00 на 19:00 и обновите страницу: новый запрос выполнит файл сначала. Уже открытая страница сама не изменится. Так же нажатие «Отправить» в следующем уроке создаст новый запрос; старые локальные переменные не продолжат жить в новом запросе.
Команда со встроенным сервером нужна для локального опыта и привязана к 127.0.0.1. Публичное развёртывание разбирается в главе о сервере. Дальше различим метод, заголовки и тело, затем свяжем настоящее поле формы с PHP.
Одна покупка — несколько независимых запросов
Как страница заказа проходит через PHP
Браузеру нужен заказ №42. PHP получает один запрос, читает данные и собирает ответ.
- Браузер: GET /orders/42
Отправляет адрес, заголовки и cookie. PHP ещё должен проверить, чей это заказ.
- Веб-сервер → PHP
Сервер передаёт запрос PHP-обработчику. Тот проверяет вход, пользователя и доступ.
- PHP → база
Запрашивает разрешённый заказ №42. База возвращает данные, а не готовую страницу.
- PHP → браузер
Отправляет статус, заголовки и HTML или JSON. Браузер показывает полученный результат.
Следующий переход — новый запрос. Состояние между запросами сохраняют в БД, сессии или другом хранилище.
Теперь применим показанную последовательность к магазину. Веб-сервер — программа, принимающая HTTP; PHP-FPM — один из способов поручить PHP выполнение файла. Маршрут — правило выбора обработчика по адресу. Эти названия описывают участников того же опыта, а не обязательные компоненты первой страницы.
Покупатель открывает товар, добавляет его в корзину и оформляет заказ. Внешне это один процесс, но сервер видит отдельные HTTP-запросы. Каждый содержит метод, адрес, заголовки и иногда тело. Nginx или Apache принимает соединение, обслуживает статические файлы либо передаёт PHP-запрос рабочему процессу. Приложение читает вход, проверяет пользователя, обращается к хранилищу и создаёт ответ. HTML затем разбирает браузер; PHP уже закончил работу. Нажатие кнопки не продолжает старую PHP-функцию: оно запускает новый запрос.
Обычные локальные переменные не являются корзиной между запросами. Даже если PHP-FPM повторно использовал тот же процесс, новый запрос имеет отдельное состояние пользовательского кода. Сохранение корзины требует сессии, базы или другого хранилища. У длительно работающих приложений на специальных серверах и CLI-воркеров модель отличается: объекты и статические поля могут переживать задания. Отсюда опасность положить текущего покупателя в глобальный контейнер и забыть очистить его перед следующим заданием.
Первой исполняется точка входа, обычно public/index.php. Она подключает автозагрузку, собирает зависимости, определяет маршрут и передаёт управление обработчику. После возврата результата отправляются статус, заголовки и тело. Для JSON и HTML это один жизненный цикл, но разные форматы представления. База данных не знает, какую HTML-кнопку нажал человек: ей нужны уже проверенные данные и полномочия, полученные сервером.
- Браузер: метод + URL + тело
- Веб-сервер → PHP-FPM
- Маршрут → входные данные → доступ
- Сервис → база / внешний API
- Статус + заголовки + тело
- Браузер строит интерфейс
GET читает, POST меняет состояние
Следующая модель выбирает действие по двум уже полученным строкам: методу GET/POST и пути /catalog или /orders. В первом браузерном опыте это делал единственный файл; в большом приложении нужен выбор между несколькими обработчиками.
Перед коротким маршрутизатором: function объявляет функцию route; string указывает ожидаемые строковые параметры. match сравнивает пару [$method, $path] с перечисленными парами и возвращает подходящий массив [статус, текст]. default выбирается при отсутствии совпадения. return отдаёт этот массив вызывающему коду. Запись [$status, $body] = ... раскладывает два элемента по переменным, а foreach повторяет опыт для трёх запросов. Это проверка правила маршрутизации в CLI; строка order-created сама по себе ещё не создаёт запись заказа.
GET удобен для поиска и фильтров: ссылку можно сохранить и передать. Удалять заказ через GET опасно: ссылку может открыть робот, предварительная загрузка или чужая страница. Изменение отправляют POST либо подходящим методом API, после проверки CSRF и полномочий. Само слово POST не даёт безопасности и не скрывает данные от владельца браузера; конфиденциальность канала обеспечивает HTTPS.
$_GET и $_POST — результаты разбора определённых форматов, а не универсальная модель всех запросов. JSON-тело читают из php://input и декодируют явно. Поле quantity может оказаться строкой, массивом quantity[]=2 или вообще отсутствовать. Поэтому контроллер не делает сразу (int) $_POST['quantity']: такое приведение скрывает ошибочный ввод. Cookies также приходят от клиента, а заголовки прокси заслуживают доверия только от настроенных доверенных прокси.
Статус отражает результат: 200 для чтения, 201 для создания, 303 для перехода после формы, 400 для повреждённого запроса, 403 для запрета, 404 для отсутствия, 422 для ошибок полей, 429 для ограничения частоты. Выбранный статус надо согласовать с контрактом API. Тело с текстом «ошибка» и статусом 200 вводит в заблуждение мониторинг и клиентские библиотеки.
<?php
declare(strict_types=1);
function route(string $method, string $path): array {
return match ([$method, $path]) {
['GET', '/catalog'] => [200, 'catalog'],
['POST', '/orders'] => [201, 'order-created'],
default => [404, 'not-found'],
};
}
foreach ([['GET','/catalog'], ['GET','/orders'], ['POST','/orders']] as [$method,$path]) {
[$status,$body] = route($method,$path);
echo "$status $body
";
}
Ожидаемый результат
200 catalog
404 not-found
201 order-created
Маршрутизатор здесь чистый и проверяется без сервера. В настоящем POST-обработчике создание происходит только после всех проверок; строка order-created обозначает результат учебного сценария.
Ответ сначала собирают, затем отправляют
Теперь вместо HTML нужен машинный JSON-ответ. sendJson ниже получает массив данных и HTTP-статус. json_encode превращает массив в текст; JSON_THROW_ON_ERROR означает исключение при невозможности кодирования, JSON_UNESCAPED_UNICODE сохраняет кириллицу буквами. Тип never сообщает, что функция не вернётся к следующей строке вызывающего кода: в конце стоит exit. Такая запись доступна в PHP 8.1+, значит подходит принятой базе 8.2+.
<?php
declare(strict_types=1);
function sendJson(array $payload, int $status): never {
$body = json_encode($payload, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo $body;
exit;
}
// Вызывайте из HTTP-контроллера до любого echo или HTML.
sendJson(['error' => 'quantity_invalid'], 422);
Это обработчик для HTTP SAPI, проверяется синтаксис. JSON кодируется до отправки: при ошибке сериализации ещё можно создать отдельный 500-ответ. Не выводите трассировку исключения клиенту.
header() не прекращает выполнение. После перенаправления требуется exit либо возврат из контроллера в систему, которая гарантированно завершит ответ. Пробел перед <?php, BOM и отладочный var_dump могут преждевременно отправить тело и сломать cookies. Буфер вывода помогает собирать шаблон, но не отменяет необходимость явного порядка действий.
Перенаправление должно вести на фиксированный локальный маршрут или значение из строгого разрешённого списка. Не передавайте произвольный return_url в Location: получится открытое перенаправление. Не стройте абсолютный адрес из непроверенного Host. Для страницы личного кабинета не включайте общий CDN-кеш; даже случайно закешированное имя пользователя уже раскрывает данные.
Адрес, прокси и браузерные ограничения
Фрагмент адреса после # браузер не отправляет серверу: /manual?chapter=forms#validation даёт PHP query chapter=forms, но не validation. Прокруткой и фокусом на раздел управляет браузерный слой. Query-параметры, напротив, относятся к запросу и должны проходить серверную проверку. Это различие полезно при отладке навигации: PHP не может узнать выбранный якорь без явной передачи из JavaScript.
За reverse proxy приложение может видеть внутренний HTTP, хотя посетитель использует HTTPS. Нельзя доверять произвольному X-Forwarded-Proto или X-Forwarded-For из интернета: список доверенных прокси задаёт инфраструктура. Иначе злоумышленник подменит схему, адрес клиента или абсолютную ссылку. Фреймворки обычно имеют отдельную настройку trusted proxies, которую согласуют с реальной схемой сети.
CORS определяет, разрешит ли браузер скрипту прочитать ответ другого origin. Это не авторизация API и не запрет прямого запроса через curl. Cookie-аутентификация, CSRF и проверка прав нужны независимо от CORS. Preflight OPTIONS не должен случайно создавать заказ, а ответ на HEAD обычно не содержит тела. Проверяйте конкретный метод, а не только наличие поля в $_POST.
Практика: проверьте решение, а не только успешный путь
После POST браузер повторно отправляет заказ при обновлении страницы. Что изменить и чего этого изменения ещё не хватает?
Разбор решения
Применить Post/Redirect/Get: после успешного сохранения ответить 303 на страницу результата. Дополнительно нужен уникальный ключ операции или ограничение в базе: PRG улучшает поведение браузера, но не защищает от двух параллельных запросов и повторов после сетевого сбоя.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
headerЗадать метаданные HTTP-ответа: тип содержимого, перенаправление, загрузку файла или правило кэширования.
header(string $header, bool $replace = true, int $response_code = 0): voidУказать формат ответа рядом с реальными данными. Заголовок описывает тело, но сам его не создаёт. Content-Type сообщает клиенту формат текста и кодировку; echo отдельно отдаёт строку каталога.
- Клиент делает GET /
- PHP готовит Content-Type и X-Catalog до echo
- Ответ: статус 200, два указанных заголовка
- Тело содержит Молоток 8
Этот код — HTTP-обработчик example.php. Запустите php -S 127.0.0.1:8791 example.php, затем в другом терминале php client.php из архива. Клиент покажет заголовки и тело ответа. Ниже отдельно указаны ожидаемый статус и поля; служебные Date/Host зависят от сервера.
| Часть ответа | Ожидаемое значение |
|---|---|
| status | 200 |
| content-type | text/plain; charset=UTF-8 |
| x-catalog | workshop |
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
header('X-Catalog: workshop');
echo "Молоток — 8 шт.\n";
Ожидаемый результат
Молоток — 8 шт.
UTF-8 помогает клиенту правильно прочитать русское название. X-Catalog — собственное метаданное поле примера; PHP не придумывает из него товар.
Ошибки и следующий шаг. Вывод, BOM или сообщение ошибки до header может отправить заголовки слишком рано. Никогда не склеивайте заголовок с непроверенными CR/LF. Для JSON задайте application/json и сформируйте тело json_encode, а не просто поменяйте Content-Type.
Полный официальный контракт и дополнительные варианты. Далее сохранены все параметры, типы результатов, версии и ограничения этой операции. Сначала прочитайте пояснение соответствующего следующего примера.
Задать метаданные HTTP-ответа: тип содержимого, перенаправление, загрузку файла или правило кэширования.
Здесь $name — переменная, = сохраняет результат, а имя со скобками вызывает функцию. Аргументы разделяют запятыми. echo выводит текст; var_dump также показывает тип. "\n" переводит строку. === false отличает отказ от числа 0, пустой строки и других допустимых значений.
Массив [] собирает элементы; ключ => значение подписывает ячейку. foreach берёт элементы по очереди, if выбирает действие по условию. throw new RuntimeException прекращает нормальный путь при неожиданном отказе, try/finally гарантирует завершающее действие. Имена example.com и адреса документационных диапазонов ниже описывают учебные данные, а не ваши серверы. Вызовы функций и работа с массивами объясняются отдельно.
| Часть вызова | Договор и ограничения |
|---|---|
| header | Одна строка Имя: значение; HTTP/… в начале задаёт статус. Location обычно устанавливает 302, если раньше не выбран 201 или 3xx. |
| replace | true заменяет прежний одноимённый заголовок; false добавляет ещё один. Не все HTTP-поля допускают бездумное дублирование. |
| response_code | Ненулевой код принудительно задаёт статус, если header не пуст. Для обычной установки статуса понятнее http_response_code. |
| Возврат / ошибки | Значение не возвращается. Неудачная постановка заголовка даёт E_WARNING; после фактической отправки менять его поздно. Переводы строк во входе недопустимы. |
- header задаёт Content-Type
- PHP держит заголовок до отправки
- echo начинает тело ответа
- Браузер интерпретирует тело по Content-Type
Эти примеры ответа запускаются через HTTP-сервер. Код PHP CLI не отправляет ответ браузеру: успешный вызов там не доказывает правильность HTTP-заголовков. Заголовки — метаданные ответа, echo — его тело. Сначала формируют статус и метаданные, затем тело. Пробел до <?php, UTF-8 BOM, вывод подключённого файла и сообщение об ошибке тоже могут начать тело.
Этот вариант. Тело равно ready и переводу строки; в заголовках ответа Content-Type: text/plain; charset=UTF-8. Отправка HTTP проверяется отдельно от синтаксиса.
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
echo "ready\n";
Ожидаемый результат
ready
Этот вариант. Учебное X-Lesson допускает наглядный тест. Не заменяйте так Set-Cookie с несколькими cookies и не объединяйте автоматически любые HTTP-поля.
<?php
declare(strict_types=1);
header('Content-Type: text/plain; charset=UTF-8');
header('X-Lesson: first');
header('X-Lesson: second');
header('X-Lesson: third', false);
foreach (headers_list() as $line) {
if (str_starts_with($line, 'X-Lesson:')) { echo $line, "\n"; }
}
Ожидаемый результат
X-Lesson: second
X-Lesson: third
Этот вариант. Ответ имеет статус 303 и Location: /done, тело пустое. exit прекращает PHP; сам header не прекращает выполнение. В реальном обработчике это делают после проверки и сохранения формы, до вывода.
<?php
declare(strict_types=1);
header('Location: /done', true, 303);
exit;
Ожидаемый результат
Location не переносит тело страницы на другой адрес: браузер делает следующий запрос. Не собирайте цель перенаправления из непроверенного HTTP_HOST/пользовательского URL; для локального маршрута достаточно заранее известного пути. Идентификаторы сессий не добавляют в URL: они попадут в историю и журналы.
| Задача | Как применяют |
|---|---|
| JSON | Content-Type: application/json; charset=UTF-8; тело получают json_encode, а не ручной склейкой. |
| Скачать файл | Content-Type соответствует данным, Content-Disposition: attachment; filename="report.txt" предлагает имя. Путь и имя выбираются приложением, а не произвольным вводом. |
| Приватный ответ | Cache-Control: no-store запрещает хранение. no-cache означает необходимость проверки свежести, а не безусловный запрет сохранения. Учитывайте настройки session_cache_limiter и reverse proxy. |
| HTTP/1.0 и HTTP/1.1 | Для статуса используйте http_response_code; вручную заданная HTTP/1.1-строка может не соответствовать протоколу конкретного SAPI. |
Буферизация позволяет отложить вывод, но требует памяти и не отменяет порядок HTTP. Что делает ob_start объясняет механизм.
Дальше: http_response_code; headers_sent; headers_list; header_remove.
Принимает полную строку заголовка. replace управляет заменой одноимённого заголовка, третий аргумент может задать статус. Полезного возвращаемого значения нет; после отправки тела возникает предупреждение. Вызывается в HTTP SAPI до вывода.
Этот вариант. Фиксированный локальный путь исключает произвольный return_url. Контекст: HTTP-контроллер после сохранения заказа.
<?php
declare(strict_types=1);
header('Location: /orders/complete', true, 303);
exit;
Этот вариант. Ответ с персональным состоянием не должен попадать в общий кеш. Сериализацию сложного тела лучше сделать до header.
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode(['status'=>'pending'], JSON_THROW_ON_ERROR);
Рядом по задаче. http_response_code задаёт статус, setcookie формирует cookie. Буфер помогает собрать тело.
http_response_codeУстановить или прочитать статус HTTP-ответа отдельно от его текста.
http_response_code(int $response_code = 0): int|boolВернуть понятное отсутствие товара со статусом 404. Статус отвечает на вопрос об исходе запроса, тело объясняет этот исход человеку. Установка кода не завершает скрипт и не печатает сообщение.
- Получен запрос отсутствующего товара 99
- PHP устанавливает 404
- Content-Type описывает JSON
- Тело даёт конкретную причину и запрошенный номер
Этот код — HTTP-обработчик example.php. Запустите php -S 127.0.0.1:8791 example.php, затем в другом терминале php client.php из архива. Клиент покажет заголовки и тело ответа. Ниже отдельно указаны ожидаемый статус и поля; служебные Date/Host зависят от сервера.
| Часть ответа | Ожидаемое значение |
|---|---|
| status | 404 |
| content-type | application/json; charset=UTF-8 |
<?php
declare(strict_types=1);
http_response_code(404);
header('Content-Type: application/json; charset=UTF-8');
echo json_encode(['error'=>'Товар не найден', 'id'=>99], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
Ожидаемый результат
{"error":"Товар не найден","id":99}Клиент получает одновременно 404 и JSON. Наличие тела не превращает ответ в 200; успешное сетевое чтение не означает успешную бизнес-операцию.
Ошибки и следующий шаг. При чтении без аргумента функция возвращает текущий код; при установке — предыдущий. В CLI поведение отличается, потому что HTTP-ответа нет. Для невалидного ввода и отсутствующего товара выбирают разные договорённые статусы.
Полный официальный контракт и дополнительные варианты. Далее сохранены все параметры, типы результатов, версии и ограничения этой операции. Сначала прочитайте пояснение соответствующего следующего примера.
Установить или прочитать статус HTTP-ответа отдельно от его текста.
Здесь $name — переменная, = сохраняет результат, а имя со скобками вызывает функцию. Аргументы разделяют запятыми. echo выводит текст; var_dump также показывает тип. "\n" переводит строку. === false отличает отказ от числа 0, пустой строки и других допустимых значений.
Массив [] собирает элементы; ключ => значение подписывает ячейку. foreach берёт элементы по очереди, if выбирает действие по условию. throw new RuntimeException прекращает нормальный путь при неожиданном отказе, try/finally гарантирует завершающее действие. Имена example.com и адреса документационных диапазонов ниже описывают учебные данные, а не ваши серверы. Вызовы функций и работа с массивами объясняются отдельно.
| Часть вызова | Договор и ограничения |
|---|---|
| response_code | Без аргумента/с 0 — прочитать текущее состояние; с кодом — установить его. Используйте подходящий стандартный HTTP-статус, а не произвольное число. |
| Возврат | При установке возвращается предыдущий код, при чтении — текущий. В HTTP обычно исходный 200. В CLI первоначальное чтение false, первая установка true. |
| Граница | Код не останавливает скрипт и не создаёт JSON/HTML-тело. После отправки заголовков клиентский ответ уже нельзя исправить одной записью внутреннего статуса. |
- Ответ пока 200
- Вызов http_response_code(404)
- Возвращается предыдущий 200
- Следующее чтение даёт текущий 404
Эти примеры ответа запускаются через HTTP-сервер. Код PHP CLI не отправляет ответ браузеру: успешный вызов там не доказывает правильность HTTP-заголовков. Заголовки — метаданные ответа, echo — его тело. Сначала формируют статус и метаданные, затем тело. Пробел до <?php, UTF-8 BOM, вывод подключённого файла и сообщение об ошибке тоже могут начать тело.
Этот вариант. HTTP-клиент получает 404. Даже если тело содержит красивую страницу ошибки, сам статус должен сообщать об отсутствии ресурса.
<?php
declare(strict_types=1);
$previous = http_response_code(404);
header('Content-Type: text/plain; charset=UTF-8');
echo $previous, "\n";
echo http_response_code(), "\n";
Ожидаемый результат
200
404
Этот вариант. CLI не отправляет HTTP. Сначала получаем/меняем состояние, затем выводим: ранний var_dump в PHP 8.5.11 может пометить заголовки отправленными и сорвать последующую установку кода. Изменение внутреннего значения не является тестом ответа сервера.
<?php
declare(strict_types=1);
$before = http_response_code();
$changed = http_response_code(201);
$after = http_response_code();
var_dump($before, $changed, $after);
Ожидаемый результат
bool(false)
bool(true)
int(201)
| Ситуация | Пример выбора |
|---|---|
| Ресурс создан | 201, при необходимости Location созданного ресурса. |
| После успешного POST перейти к просмотру | 303 + Location, затем exit. |
| Неавторизованный / запрещённый доступ | 401 с подходящим WWW-Authenticate либо 403 по договору API. |
| Ошибка ввода / лимит / сбой | Подходящий 400/422, 429 с Retry-After или 500/503; текст и статус должны согласовываться. |
Дальше: header; headers_sent.
Без аргумента возвращает текущий код при наличии HTTP-контекста; с кодом меняет статус и обычно возвращает прежний. В CLI результат отличается, поэтому пример проверяется как HTTP-фрагмент.
Этот вариант. HTTP API сообщает, что запрос разобран, но значение не проходит правило.
<?php
declare(strict_types=1);
http_response_code(422);
header('Content-Type: application/json');
echo json_encode(['errors'=>['quantity'=>'range']],JSON_THROW_ON_ERROR);
Этот вариант. 405 сопровождается списком разрешённых методов. Это отличается от отсутствующего маршрута 404.
<?php
declare(strict_types=1);
http_response_code(405);
header('Allow: GET, POST');
exit;
Рядом по задаче. header нужен для полей ответа, а не для хранения ошибки. 422 и ошибки полей.
headers_sentПроверить, можно ли ещё менять HTTP-заголовки, и найти место, где начался вывод.
headers_sent(string &$filename = null, int &$line = null): boolПодготовить метаданные до отправки накопленного текста. Проверяется начало отправки, а не факт вызова echo. При активном PHP-буфере текст ещё можно удержать внутри процесса и закончить подготовку заголовков.
- ob_start удерживает текст Молотка в буфере
- headers_sent пока false → можно задать заголовки
- Устанавливаем X-Report-State
- ob_end_flush передаёт тело в HTTP-ответ
Этот код — HTTP-обработчик example.php. Запустите php -S 127.0.0.1:8791 example.php, затем в другом терминале php client.php из архива. Клиент покажет заголовки и тело ответа. Ниже отдельно указаны ожидаемый статус и поля; служебные Date/Host зависят от сервера.
| Часть ответа | Ожидаемое значение |
|---|---|
| status | 200 |
| x-report-state | prepared-before-output |
<?php
declare(strict_types=1);
ob_start();
echo "Молоток — 8 шт.\n";
if (headers_sent($file, $line)) { throw new RuntimeException('Метаданные уже отправлены'); }
header('Content-Type: text/plain; charset=UTF-8');
header('X-Report-State: prepared-before-output');
ob_end_flush();
Ожидаемый результат
Молоток — 8 шт.
Клиент действительно получает X-Report-State и строку товара. Выходные переменные $file/$line заполняются по ссылке при уже начавшейся отправке и помогают искать преждевременный вывод.
Ошибки и следующий шаг. Буферизация не исправляет бесконечный объём ответа и не разрешает менять уже отправленные данные. Для поиска сбоя используйте источник раннего вывода в закрытой диагностике; не показывайте внутренние пути посетителю.
Полный официальный контракт и дополнительные варианты. Далее сохранены все параметры, типы результатов, версии и ограничения этой операции. Сначала прочитайте пояснение соответствующего следующего примера.
Проверить, можно ли ещё менять HTTP-заголовки, и найти место, где начался вывод.
Здесь $name — переменная, = сохраняет результат, а имя со скобками вызывает функцию. Аргументы разделяют запятыми. echo выводит текст; var_dump также показывает тип. "\n" переводит строку. === false отличает отказ от числа 0, пустой строки и других допустимых значений.
Массив [] собирает элементы; ключ => значение подписывает ячейку. foreach берёт элементы по очереди, if выбирает действие по условию. throw new RuntimeException прекращает нормальный путь при неожиданном отказе, try/finally гарантирует завершающее действие. Имена example.com и адреса документационных диапазонов ниже описывают учебные данные, а не ваши серверы. Вызовы функций и работа с массивами объясняются отдельно.
| Часть вызова | Договор и ограничения |
|---|---|
| filename / line | Необязательные выходные переменные по ссылке. PHP заполняет источник начавшегося вывода. При ранней startup-ошибке имя файла может быть пустым. |
| Возврат | false — заголовки ещё можно подготовить; true — отправка уже началась. Это не проверка того, вызывался ли header. |
| Буфер | echo внутри активного буфера ещё может не отправить данные наружу. Учитывают стек буферов и настройки SAPI, а не количество строк echo. |
- Текст ещё лежит в PHP-буфере
- headers_sent пока false
- Буфер передаётся SAPI
- После начала отправки headers_sent true
Эти примеры ответа запускаются через HTTP-сервер. Код PHP CLI не отправляет ответ браузеру: успешный вызов там не доказывает правильность HTTP-заголовков. Заголовки — метаданные ответа, echo — его тело. Сначала формируют статус и метаданные, затем тело. Пробел до <?php, UTF-8 BOM, вывод подключённого файла и сообщение об ошибке тоже могут начать тело.
Этот вариант. ob_start создаёт буфер, ob_end_clean выбрасывает его содержимое. До var_dump тело не отправлялось; слово temporary клиент не увидит.
<?php
declare(strict_types=1);
ob_start();
echo 'temporary';
$sent = headers_sent($file, $line);
ob_end_clean();
var_dump($sent);
Ожидаемый результат
bool(false)
Этот вариант. HTTP-fixture запускается с output_buffering=0 и без дополнительных буферов. На другом сервере flush не гарантирует немедленное отображение в браузере: остаются буферы сервера/прокси.
<?php
declare(strict_types=1);
echo "body starts\n";
flush();
$sent = headers_sent($file, $line);
var_dump($sent);
echo $sent && $file !== '' && $line > 0 ? "Output origin known\n" : "No origin\n";
Ожидаемый результат
body starts
bool(true)
Output origin known
Проверка помогает диагностировать проблему, но ветка «если уже отправлено, просто промолчим» может скрыть сломанный redirect или cookie. Исправляют ранний вывод и порядок формирования ответа. В публичную страницу не выводят полный внутренний путь файла.
Дальше: header; headers_list; setcookie.
Возвращает bool, а параметры по ссылке сообщают файл и строку начала вывода, если доступны. Полезно для диагностики раннего echo, BOM и пробела до PHP.
Этот вариант. Путь и строку можно добавить в закрытый технический журнал; не выводите их посетителю. HTTP-контекст.
<?php
declare(strict_types=1);
if (headers_sent($file,$line)) {
error_log('Response output already started');
exit;
}
header('Location: /account',true,303);
exit;
Этот вариант. Контракт ловит ошибку вызывающего слоя раньше попытки setcookie. Сама функция не чинит уже отправленное тело.
<?php
declare(strict_types=1);
function requireWritableHeaders(): void {
if (headers_sent()) throw new LogicException('Response already started');
}
Рядом по задаче. ob_get_level показывает стек пользовательских буферов. Разбор буферизации.
setcookieСохранить небольшую настройку браузера и получить её в следующем запросе, понимая границу доверия к клиенту.
setcookie(string $name, string $value = "", int|array $expires_or_options = 0, string $path = "", string $domain = "", bool $secure = false, bool $httponly = false): boolСохранить тему и увидеть её только в следующем запросе. Сервер ставит Set-Cookie в свой ответ. Клиент сохраняет cookie и затем посылает Cookie в следующем запросе; текущий $_COOKIE от этого задним числом не меняется. setcookie кодирует пробел в значении blue sky при формировании заголовка.
- Первый входящий запрос не содержит theme
- Ответ устанавливает theme и Path/HttpOnly/SameSite
- Cookie engine cURL сохраняет значение
- Следующий запрос содержит theme → сервер читает blue sky
В архиве есть server.php с известным каталогом, base.php и этот клиент example.php. В первом терминале запустите php -S 127.0.0.1:8791 server.php, во втором — php example.php. Полная подготовка и первые два файла.
<?php
declare(strict_types=1);
require __DIR__ . '/base.php';
$client = curl_init($base . '/product?id=1');
if ($client === false) { throw new RuntimeException('cURL не создал клиент'); }
curl_setopt_array($client, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 2, CURLOPT_CONNECTTIMEOUT => 1]);
curl_setopt($client, CURLOPT_COOKIEFILE, '');
curl_setopt($client, CURLOPT_URL, $base . '/cookie/set');
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
echo $body;
curl_setopt($client, CURLOPT_URL, $base . '/cookie/read');
$body = curl_exec($client);
if ($body === false) { throw new RuntimeException('Нет ответа: '.curl_error($client)); }
if (curl_getinfo($client, CURLINFO_RESPONSE_CODE) !== 200) { throw new RuntimeException('Сервис отклонил запрос'); }
echo $body;
Ожидаемый результат
Тема входящего запроса: не выбрана
Тема следующего запроса: blue sky
Полный обработчик соответствующего маршрута находится в server.php. В реальном браузере ту же работу хранения выполняет браузер. CURLOPT_COOKIEFILE пустой строкой включает собственный cookie-engine cURL.
Ошибки и следующий шаг. Локальная лаборатория HTTP не ставит Secure: для HTTPS-приложения используйте Secure, подходящие Path/SameSite/HttpOnly и корректное удаление с той же областью. Пользователь может изменить cookie: это настройка интерфейса, не доказательство права доступа.
Полный официальный контракт и дополнительные варианты. Далее сохранены все параметры, типы результатов, версии и ограничения этой операции. Сначала прочитайте пояснение соответствующего следующего примера.
Сохранить небольшую настройку браузера и получить её в следующем запросе, понимая границу доверия к клиенту.
Здесь $name — переменная, = сохраняет результат, а имя со скобками вызывает функцию. Аргументы разделяют запятыми. echo выводит текст; var_dump также показывает тип. "\n" переводит строку. === false отличает отказ от числа 0, пустой строки и других допустимых значений.
Массив [] собирает элементы; ключ => значение подписывает ячейку. foreach берёт элементы по очереди, if выбирает действие по условию. throw new RuntimeException прекращает нормальный путь при неожиданном отказе, try/finally гарантирует завершающее действие. Имена example.com и адреса документационных диапазонов ниже описывают учебные данные, а не ваши серверы. Вызовы функций и работа с массивами объясняются отдельно.
| Часть вызова | Договор и ограничения |
|---|---|
| name / value | Имя и строковое значение. Данные хранит клиент: он может их изменить. Cookie не подходит для пароля, доверенной роли или суммы заказа. Пустое value в PHP формирует удаление; для false используют явную строку "0", а не неявное преобразование. |
| expires_or_options | Unix timestamp истечения либо массив параметров. 0 задаёт session cookie без явного Expires; восстановление сессии браузера может сохранить её дольше одного закрытия окна. |
| path | Область пути, например /. Если не задана, применяется вычисляемый браузером default-path. Это правило отправки cookie, а не надёжная изоляция данных от скриптов одного origin. |
| domain | Без Domain cookie привязана к текущему host. Явный домен распространяет её на подходящие поддомены; произвольный чужой домен браузер не принимает. Ведущая точка для современных клиентов не нужна. |
| secure / httponly | Secure ограничивает передачу защищённым соединением; HttpOnly скрывает cookie от document.cookie. Они не заменяют проверку входа, CSRF-защиту и предотвращение XSS. PHP может сформировать Secure даже в локальном HTTP-тесте; принятие решает браузер. |
| options, PHP 7.3+ | expires,path,domain,secure,httponly,samesite; с 8.5 также partitioned. Передаём массив третьим позиционным аргументом; отдельного именованного параметра options у сигнатуры нет. Не смешиваем его с последующими позиционными настройками. |
| samesite | Strict/Lax/None управляют отправкой в cross-site сценариях. Для None нужен Secure, иначе клиент отклоняет cookie. Отсутствие атрибута не означает отсутствие браузерной политики по умолчанию. |
| partitioned, PHP 8.5+ | Разделённое клиентское хранилище CHIPS. true требует secure=true, иначе PHP бросает ValueError. Совместимость и фактическое поведение зависят от браузера. |
| Возврат / ошибки | true означает, что PHP подготовил заголовок, а не что браузер принял cookie. После отправки заголовков — предупреждение и false. Неверное имя/недопустимые опции вызывают ValueError; до PHP 8 неизвестные ключи давали предупреждение. |
- Первый запрос ещё без theme
- PHP формирует Set-Cookie: theme=dark
- Браузер принимает и хранит настройку
- Второй запрос приносит Cookie, PHP получает $_COOKIE[theme]
Сначала браузер отправляет запрос с уже имеющимися cookies; PHP заполняет $_COOKIE. Затем setcookie добавляет Set-Cookie в ответ. Если браузер принял его, значение вернётся в следующем подходящем запросе. Поэтому новый вызов сам по себе не переписывает текущий $_COOKIE. Для текущего вычисления используйте свою переменную.
Этот вариант. HTTP-пример первого запроса без Cookie. В fixture проверяют Set-Cookie, затем повторяют запрос с присланной cookie. На HTTPS-сайте добавляют secure=true; здесь локальный учебный HTTP без пользовательских секретов.
<?php
declare(strict_types=1);
$theme = 'dark';
$queued = setcookie('lesson_theme', $theme, [
'path' => '/', 'httponly' => true, 'samesite' => 'Lax',
]);
header('Content-Type: text/plain; charset=UTF-8');
var_dump($queued);
echo $_COOKIE['lesson_theme'] ?? 'not received yet', "\n";
Ожидаемый результат
bool(true)
not received yet
Этот вариант. Массив cookies — fixture входящего $_COOKIE. ?? задаёт значение при отсутствии/null, in_array со strict=true проверяет разрешённый набор. Случайный текст клиента не попадает в имя файла шаблона.
<?php
declare(strict_types=1);
$cookies = ['lesson_theme' => 'unexpected'];
$candidate = $cookies['lesson_theme'] ?? 'light';
$theme = is_string($candidate) && in_array($candidate, ['light', 'dark'], true)
? $candidate : 'light';
echo $theme, "\n";
Ожидаемый результат
light
Этот вариант. Ответ просит браузер удалить cookie. Имя, Path и Domain должны соответствовать установленной cookie; host-only не подменяют произвольным Domain. Для partitioned учитывают ту же клиентскую область. Текущий $_COOKIE не очищается автоматически.
<?php
declare(strict_types=1);
setcookie('lesson_theme', '', [
'expires' => time() - 3600, 'path' => '/', 'httponly' => true, 'samesite' => 'Lax',
]);
echo "Deletion requested\n";
Ожидаемый результат
Deletion requested
Этот вариант. Второй случай требует PHP 8.5. Мы выводим накопленные результаты после всех вызовов, чтобы не спутать ошибки опций с ранним выводом.
<?php
declare(strict_types=1);
$results = [];
try { setcookie('lesson', 'x', ['unknown' => true]); }
catch (ValueError $error) { $results[] = 'Unknown option'; }
try { setcookie('lesson', 'x', ['partitioned' => true, 'secure' => false]); }
catch (ValueError $error) { $results[] = 'Partitioned requires Secure'; }
echo implode("\n", $results), "\n";
Ожидаемый результат
Unknown option
Partitioned requires Secure
Этот вариант. Значение кодируется для транспорта и декодируется PHP на входе. Кодирование не является шифрованием/подписью. Смотрите именно заголовок HTTP-fixture.
<?php
declare(strict_types=1);
setcookie('lesson_text', 'A B+', ['path' => '/']);
foreach (headers_list() as $line) {
if (str_starts_with($line, 'Set-Cookie: lesson_text=')) { echo $line, "\n"; }
}
Ожидаемый результат
Set-Cookie: lesson_text=A%20B%2B; path=/
Можно назвать отдельные cookies вида prefs[color], и PHP соберёт их в массив, но в браузере останутся отдельные cookies. Для небольшого составного значения допустим JSON с проверкой размера и структуры; не используйте unserialize для недоверенной cookie. Размеры/число cookies ограничены браузерами, а отправляются они с каждым подходящим запросом: большие данные хранят на сервере.
С PHP 8.2 формат даты Expires изменён на вариант с пробелами, но вы всё равно передаёте Unix timestamp. Несколько setcookie формируют отдельные строки в порядке вызова. Буферизация может отложить отправку, но не сообщает, принял ли браузер cookie. Откуда берётся $_COOKIE и проверка данных и безопасность развивают тему.
Дальше: setrawcookie; header; headers_sent.
Вариант с options принимает expires, path, domain, secure, httponly и samesite. Возвращает успешность подготовки заголовка, но не подтверждение принятия браузером. Неверные параметры могут вызвать исключение.
Этот вариант. HTTPS-страница сохраняет несекретное предпочтение. Если JavaScript должен читать тему, HttpOnly меняют осознанно.
<?php
declare(strict_types=1);
setcookie('theme','dark',['expires'=>time()+86400,'path'=>'/','secure'=>true,'httponly'=>true,'samesite'=>'Lax']);
Этот вариант. Истёкший cookie с тем же путём просит браузер удалить значение. Это не удаляет данные серверной сессии.
<?php
declare(strict_types=1);
setcookie('theme','',['expires'=>time()-3600,'path'=>'/','secure'=>true,'httponly'=>true,'samesite'=>'Lax']);
Рядом по задаче. Для идентификатора сессии используйте session_set_cookie_params/session_start; для состояния корзины — серверное хранилище.