Глава 101 · От основ к практике
Управление выводом: буферы, обработчики и отправка ответа
Полный раздел Output Control: от захвата шаблона до уровней, фаз обработчика, ограничений памяти и HTTP-границ.
Сначала полезный результат: превратить печать в строку
Печатать в строку, а не сразу в ответ
Шаблон использует echo, но приложению нужно сначала получить результат, проверить его и затем выбрать дальнейшее действие.
- ob_start
Создаёт верхний буфер пользовательского вывода.
- Шаблон печатает
echo складывает байты в активный буфер, а не обязательно отправляет их браузеру.
- ob_get_clean
Возвращает накопленную строку и завершает удаляемый буфер.
- Приложение решает
Полученный текст можно использовать в письме, сохранить или вывести позже.
PHP-буфер, SAPI, сервер и браузер — разные ступени. Clean удаляет содержимое, flush передаёт его дальше; это не взаимозаменяемые действия.
Старый шаблон уже умеет печатать квитанцию через echo. Теперь ту же квитанцию нужно передать в письмо или сохранить в кэше как строку. Переписывать каждую печать не обязательно: PHP может временно собирать вывод в буфере — области памяти — и затем отдать собранный текст переменной. Сначала получим этот результат всего тремя действиями.
- ob_start() открывает временный приёмник
- echo отправляет текст в этот приёмник
- ob_get_clean() возвращает текст и закрывает приёмник
- $receipt хранит строку, с которой дальше работает приложение
<?php
declare(strict_types=1);
ob_start();
echo 'Квитанция: чай, 350 рублей';
$receipt = ob_get_clean();
echo 'Для письма: ', $receipt, "\n";
Ожидаемый результат
Для письма: Квитанция: чай, 350 рублей
Первые echo не исчезли: до закрытия они пополняли буфер. $receipt — обычная строка, знак = сохраняет возвращённое значение. Последнее echo печатает уже после закрытия. Здесь нет отправки письма, файлов или сети: виден именно переход «печать → значение».
| Элемент записи | Что произошло |
|---|---|
| ob_start() | Вызов функции без аргументов: круглые скобки запускают действие, а не только называют его. |
| echo | Конструкция языка печатает выражения; возвращаемого текста, как у функции, у неё нет. |
| $receipt = ob_get_clean() | Вызов справа возвращает строку; присваивание слева сохраняет её. Закрывается текущий верхний буфер. |
| ; | Завершение инструкции. Строки в кавычках являются данными, а не названиями команд. |
Если вы сами пишете небольшую функцию, вернуть строку через return зачастую проще. Буфер особенно полезен на границе с уже печатающим шаблоном, диагностической функцией или старой библиотекой. Он не нужен вокруг каждого echo и не делает приложение автоматически быстрее. Перед дальнейшими опытами полезны вызов и результат функции и практика шаблонов и ответа.
Пользовательский буфер, SAPI и браузер — разные ступени
У запроса нет единственного волшебного буфера. PHP-код может открыть пользовательские буферы; ниже работает системный вывод выбранного SAPI — CLI, FastCGI, Apache и так далее. Ещё дальше свои решения принимают веб-сервер, прокси, транспорт и браузер. Операция на одном уровне не доказывает, что человек уже увидел текст.
- Шаблон вызывает echo
- Внутренний пользовательский буфер принимает байты
- Обработанные байты попадают во внешний пользовательский буфер, если он есть
- Без внешнего буфера данные переходят в PHP/SAPI
- За SAPI возможны собственные буферы сервера и клиента
Пользовательский буфер — не только строка: с ним связан обработчик, разрешённые операции и состояние. ob_start() без callback использует обработчик по умолчанию, который передаёт текст дальше без преобразования. Настройки php.ini могут открыть буфер ещё до первой инструкции файла; ob_start действует только с места своего вызова. Поэтому библиотека должна считать уже существующие уровни и не закрывать чужие.
Кроме захвата шаблона, буфер используют для повторного использования дорогого результата, выборочной обработки вывода готового компонента и подключения преобразований, которых нет на веб-сервере. Возможна ранняя отправка HTML head, чтобы браузер начал загружать ресурсы, пока строится body; такой маршрут допустим лишь когда дальнейшая работа уже не потребует изменить отправленный HTTP-статус или заголовки. Обычному приложению сначала полезнее собрать целый управляемый ответ.
При обычном окончании скрипта и при exit PHP закрывает оставшиеся уровни в обратном порядке, передавая их вывод наружу. Запрет REMOVABLE относится к операциям пользовательского кода и не предотвращает завершение буфера при shutdown. Если данные нельзя отправлять автоматически, приложение должно своевременно владеть их очисткой и завершением. Обработчик, возвращающий пустую строку, может подавлять обычный вывод при завершении, но ошибки обработчика имеют отдельные правила: это не граница конфиденциальности.
Установка, пять настроек и различия CLI / веб
Управление выводом входит в PHP Core: отдельный PECL-пакет для этих 16 функций не устанавливают. Конкретные преобразующие обработчики могут зависеть от расширений mbstring, iconv или zlib. Проверьте конфигурацию именно того SAPI, где работает сайт: CLI и веб могут читать разные ini-файлы. Области действия INI_PERDIR и INI_ALL нельзя заменить произвольным ini_set в любой точке приложения.
| Директива, значение по умолчанию | Где задаётся и что означает |
|---|---|
| output_buffering = 0 | INI_PERDIR. Off/0 отключает автоматический буфер; On включает без порога; положительное количество байтов задаёт порог передачи. В CLI директива всегда Off; явный ob_start в CLI работает. Порог не является жёстким лимитом памяти. |
| output_handler = null | INI_PERDIR. Встроенный обработчик/поддерживаемый внутренний alias перенаправляет вывод и автоматически включает буферизацию. Пользовательскую функцию подключают через ob_start, не через эту директиву. |
| implicit_flush = 0 | INI_ALL. Просьба системному слою выполнять flush после каждого непустого блока печати. CLI по умолчанию включает этот режим; пользовательские ob_start он не опустошает. |
| url_rewriter.tags = "form=" | INI_ALL. Пары tag=attribute через запятую, без пробелов; form= добавляет hidden inputs. Первая запись повторённого тега выигрывает. С PHP 7.1 настройка относится к output_add_rewrite_var; раньше обслуживала и transparent session ID. |
| url_rewriter.hosts = HTTP_HOST по умолчанию | INI_ALL, директива появилась в PHP 7.1. Список host через запятую без пробелов. В реализации до PHP 8.4 для пользовательского rewrite фактически применялся session.trans_sid_hosts; в современных версиях — url_rewriter.hosts. Для доверенного списка задавайте собственную конфигурацию. |
PHP также имеет специальный URL-Rewriter: максимум два внутренних экземпляра одновременно, отдельно для пользовательских пар и прозрачной передачи session ID. Расширение zlib может включить свой буфер через zlib.output_compression. Они используют тот же механизм пользовательских буферов, поэтому появляются в стеке рядом с вашим кодом. Значение On для zlib обычно соответствует порогу 16 KiB; это не общая настройка всех ob_start.
Кроме двух внутренних обработчиков default output handler и URL-Rewriter, документация перечисляет обработчики расширений: mb_output_handler, ob_gzhandler, ob_iconv_handler и ob_tidyhandler. Их наличие зависит от сборки/расширений, а имена не означают взаимозаменяемость. У URL-Rewriter порог равен 0 и через ob_start для него не выбирается: до обработки текст ограничен доступной PHP памятью.
<?php
declare(strict_types=1);
echo extension_loaded('Core') ? "Core доступен\n" : "Core недоступен\n";
echo function_exists('ob_start') ? "Захват вывода доступен\n" : "Нет функции\n";
$before = ob_get_level();
ob_start();
$opened = ob_get_level() === $before + 1;
ob_end_clean();
echo $opened ? "Свой уровень открыт и закрыт\n" : "Проверьте конфигурацию\n";
Ожидаемый результат
Core доступен
Захват вывода доступен
Свой уровень открыт и закрыт
Диагностика отвечает на узкий вопрос и не публикует настройки, пути или окружение сервера. Уровень сравнивается с исходным, а не предполагается равным нулю.
Что попадает в буфер, а что проходит другим путём
Буфер перехватывает вывод, проходящий через обычный механизм вывода PHP: echo, print, printf, печать var_dump, HTML вне PHP-тегов и запись в php://output. Возвращаемое функцией значение само по себе не печать. HTTP-заголовки и cookies также не становятся частью текстового буфера. В CLI прямая запись в STDOUT или php://stdout обходит пользовательские ob-буферы.
<?php
declare(strict_types=1);
function makeLabel() { return 'Чай'; }
ob_start();
$name = makeLabel();
echo 'Товар: ', $name;
file_put_contents('php://output', ' / готов');
$text = ob_get_clean();
echo $text, "\n";
Ожидаемый результат
Товар: Чай / готов
makeLabel вернула значение в $name, но не напечатала его. В буфер попали только последующие echo и запись в поток вывода. Здесь php://output — специальный поток PHP, не внешний файл.
<?php
declare(strict_types=1);
ob_start();
echo 'Собранный текст';
fwrite(STDOUT, "Прямое сообщение CLI\n");
$text = ob_get_clean();
echo 'Буфер: ', $text, "\n";
Ожидаемый результат
Прямое сообщение CLI
Буфер: Собранный текст
STDOUT доступен в CLI. Этот опыт не предназначен для вставки в HTML-страницу: прямая запись сознательно обходит пользовательскую буферизацию.
Диагностические сообщения PHP тоже могут попасть в обычный вывод, если это разрешено display_errors и error_reporting. Поэтому захват шаблона не гарантирует «чистый HTML»: предупреждение о неверном поле способно загрязнить письмо или JSON. Исправляйте ошибку и настраивайте отдельный журнал; не прячьте её через ob_clean как способ лечения. Разделение ошибок, показа и журналирования объясняет эту границу подробнее.
Вложенные шаблоны: кто владеет каким уровнем
Отчёт может открыть внешний буфер, а небольшой шаблон внутри него — свой. В каждый момент активен последний открытый уровень. Только когда внутренний уровень передал результат, внешний его получает. Это стек: последний вошёл — первый вышел. ob_get_level считает количество уровней начиная с одного; поле level в ob_get_status нумерует их начиная с нуля.
<?php
declare(strict_types=1);
ob_start();
echo 'Каталог: ';
ob_start();
echo 'чай';
$card = ob_get_clean();
echo '[', $card, ']';
$page = ob_get_clean();
echo $page, "\n";
Ожидаемый результат
Каталог: [чай]
Внутренний get_clean не отправил чай во внешний буфер: он вернул строку в $card. Код сам решил, когда и с какими скобками вставить карточку в страницу. Если бы он вызвал end_flush, текст передался бы наружу сразу.
- Внешний уровень хранит «Каталог: »
- Внутренний получает «чай»
- get_clean закрывает внутренний, строка переходит в $card
- Внешний получает составленную карточку
- Его get_clean возвращает страницу
Функции clean/end/get работают с активным уровнем, не ищут «буфер моей библиотеки» по имени. Не используйте while (ob_get_level()) ob_end_clean() внутри подключаемого шаблона: это уничтожает вывод владельца запроса. Проверка исходного уровня полезна, но не изолирует код, который сам закрывает чужой буфер и открывает новый на той же глубине. Договор о владении обязателен; полную песочницу счётчик не создаёт.
Порог обработки, байты, память и границы порций
chunk_size — порог запуска обработки по количеству байтов, а не вместимость строки. Значение 0 означает «не запускать автоматическую обработку из-за размера». При положительном значении PHP передаёт накопленное содержимое, когда оно достигло порога или превысило его. Один большой echo может дать обработчику порцию заметно больше порога; это не разбиение строго на одинаковые блоки.
<?php
declare(strict_types=1);
$sizes = [];
ob_start();
ob_start(function ($text, $phase) use (&$sizes) {
$sizes[] = strlen($text);
return strtoupper($text);
}, 4);
echo 'abcdef';
ob_end_flush();
$result = ob_get_clean();
echo $result, "\n", implode(',', $sizes), "\n";
Ожидаемый результат
ABCDEF
6,0
Обработчик получил шесть байтов одной порцией, затем финальный вызов с пустой строкой. Объяснение callback и use находится в следующих разделах; пока следите за размером и результатом. Финальный вызов нужен даже без оставшегося текста.
Единица здесь байт: UTF-8 буква может занимать несколько байтов. Буфер может закончиться посреди символа, HTML-тега или JSON-токена. Преобразование, рассчитанное на целый документ, нельзя автоматически применять независимо к каждой порции. Для такого преобразования нужны сохранение состояния между вызовами, корректная финализация и понимание формата; регулярная замена по любому чанку может пропустить границу.
Стандартный пользовательский буфер без положительного порога первоначально выделяет около 16 KiB. При явном пороге начальное выделение округляется вверх до кратного 4 KiB; затем при необходимости растёт. Это детали реализации PHP 8.5.11, а не бюджет памяти приложения. Снимок ob_get_contents, результат преобразования и внешний буфер могут одновременно удерживать дополнительные строки. Для огромного экспорта предпочтительнее поэлементная запись в файл/поток с подходящим API, чем сборка всего документа в RAM.
Clean, flush, end и get: таблица без путаницы
Сначала отделите три решения. Clean удаляет накопленное содержимое. Flush передаёт результат обработчика следующему уровню и опустошает активный буфер. End закрывает сам уровень. Get возвращает исходные накопленные байты в переменную, а не автоматически результат преобразующего callback. Названия комбинируют эти действия, но требования к разрешениям отличаются.
| Функция | Что делает / что требует |
|---|---|
ob_get_contents | Снимок исходного содержимого, без обработки и очистки; разрешающие флаги не нужны. |
ob_clean | Вызывает обработчик с CLEAN, игнорирует возвращённый им текст, очищает; требует CLEANABLE. |
ob_flush | Вызывает с FLUSH, передаёт результат наружу, очищает; требует FLUSHABLE. |
ob_end_clean | CLEAN | FINAL, результат обработчика отбрасывается, уровень закрывается; требует REMOVABLE, не CLEANABLE. |
ob_end_flush | FINAL, результат передаётся наружу, уровень закрывается; требует REMOVABLE, не FLUSHABLE. |
ob_get_clean | Возвращает исходный текст + пытается end_clean; для закрытия нужен REMOVABLE. |
ob_get_flush | Возвращает исходный текст + пытается end_flush; для закрытия нужен REMOVABLE. |
<?php
declare(strict_types=1);
ob_start();
ob_start(fn($text) => strtoupper($text));
echo 'tea';
$raw = ob_get_flush();
$processed = ob_get_clean();
echo $raw, '/', $processed, "\n";
Ожидаемый результат
tea/TEA
Внутренний get_flush вернул исходную строку tea и одновременно передал преобразованную TEA во внешний буфер. Условия «вернул» и «отправил» относятся к разным каналам результата.
Callback: регистрация, вызов, два параметра и return
Callback — функция, которую позже вызывает PHP. Для обработчика вывода ей передают накопленный текст $buffer и битовую маску этапа $phase. Сам ob_start регистрирует обработчик; обычно он не вызывает его сразу. Обработчик возвращает новый текст, а не печатает его. Следующий пример преобразует маленький целый фрагмент, когда буфер закрывается.
- Ваш код регистрирует функцию в ob_start
- echo накапливает данные
- Закрытие или порог вызывает callback(buffer, phase)
- Callback возвращает результат
- PHP передаёт результат на внешний уровень
<?php
declare(strict_types=1);
function markReceipt($buffer, $phase) {
return '[' . $buffer . ']';
}
ob_start();
ob_start('markReceipt');
echo 'Квитанция';
ob_end_flush();
$text = ob_get_clean();
echo $text, "\n";
Ожидаемый результат
[Квитанция]
Строка markReceipt обозначает вызываемую функцию. $buffer получает текст, $phase — этап; во втором параметре этого простого преобразования пока нет необходимости. Точка соединяет строки. return отдаёт результат механизму буфера. Внешний уровень нужен только для наглядной проверки итогового текста.
Далее допустимы замыкание, статический метод, callable-массив или вызываемый объект. Конкретный callback должен принимать строку и целочисленный этап; типизированный прикладной контракт удобно ограничить string|false, чтобы не зависеть от неявных преобразований. Если состояние нужно между порциями, используйте явно принадлежащее обработчику состояние. Не применяйте глобальный массив, случайно общий для нескольких экземпляров.
Если обработчик вызывается при shutdown, рабочая папка под некоторыми веб-серверами может отличаться от ожидаемой. Для файлов конфигурации/шаблонов заранее фиксируют абсолютный путь, например через __DIR__, и не выполняют в callback платёж или отправку заявки. Обработчик может запускаться несколько раз и на пустой строке: ни один его вызов не равен подтверждению бизнес-операции.
Этапы обработчика: START, WRITE, CLEAN, FLUSH, FINAL
Флаги этапа — независимые биты в числе. Оператор | объединяет флаги, & проверяет присутствие бита. START добавляется к первому фактическому вызову, FINAL — к последнему; между ними возможны WRITE, CLEAN и FLUSH. Поэтому сравнение phase === START теряет случай START | FINAL. WRITE равен нулю: проверять его через phase & WRITE бессмысленно.
| Константа / значение | Когда встречается |
|---|---|
| PHP_OUTPUT_HANDLER_START = 1 | Первый вызов обработчика, а не момент создания буфера. Может сочетаться с CLEAN, FLUSH или FINAL. |
| PHP_OUTPUT_HANDLER_WRITE = 0 | Обычная обработка при достижении порога; на первой порции дополнительно START. Для отдельной обычной порции после начала phase может быть ровно 0. |
| PHP_OUTPUT_HANDLER_CLEAN = 2 | Очистка; end_clean / get_clean также добавляют FINAL. |
| PHP_OUTPUT_HANDLER_FLUSH = 4 | Явный ob_flush. End/get_flush и shutdown финализируют через FINAL, без обязательного бита FLUSH. |
| PHP_OUTPUT_HANDLER_FINAL = 8 | Последний вызов при закрытии/завершении. |
| PHP_OUTPUT_HANDLER_CONT = 0; END = 8 | Исторические имена для WRITE и FINAL. Предпочитайте более ясные современные имена. |
<?php
declare(strict_types=1);
$events = [];
ob_start();
ob_start(function ($text, $phase) use (&$events) {
$events[] = [$text, $phase];
return strtoupper($text);
});
echo 'discard';
ob_clean();
echo 'send';
ob_flush();
echo 'last';
ob_end_flush();
$output = ob_get_clean();
foreach ($events as [$text, $phase]) {
echo $text, ':', $phase, "\n";
}
echo 'Итог: ', $output, "\n";
Ожидаемый результат
discard:3
send:4
last:8
Итог: SENDLAST
3 — START | CLEAN. Функция сохранила события в $events через use (&$events), а напечатали их уже после закрытия буферов. CLEAN вызвал обработчик, но его преобразованный результат был отброшен. События показывают реальный порядок, а не предполагают его.
Строка, false и неявные преобразования результата
Лучший прикладной обработчик явно возвращает строку. Но язык допускает и другие результаты, и у false особый смысл: исходный текст передаётся дальше, а сам обработчик помечается DISABLED. Это не команда «ничего не выводить». Пустая строка подавляет данную порцию, сохраняя обработчик активным.
| Возврат callback | Результат механизма вывода |
|---|---|
| string | Эта строка передаётся наружу при flush; при clean отбрасывается. Пустая строка означает пустую порцию. |
| false | Исходная порция проходит дальше; обработчик отключается для последующих операций. |
| true или null | Пустая строка. Отсутствие return даёт null и может незаметно подавить вывод. |
| int / float | Преобразование в строку. Для формата ответа лучше делать его явно. |
| array | В PHP 8.5.11 строка Array и E_WARNING «Array to string conversion». Краткая документация, обещающая отсутствие предупреждения, не совпала с проверенным runtime. |
| object | При доступном строковом преобразовании используется __toString; без него возникает Error. Не используйте объект как неявный протокол ответа. |
| resource | Применяются обычные правила приведения ресурса к строке с его внутренним идентификатором. Это не чтение содержимого файла/потока и не полезный формат ответа. |
<?php
declare(strict_types=1);
$calls = 0;
ob_start();
ob_start(function ($text) use (&$calls) {
$calls++;
return false;
});
echo 'first';
ob_flush();
echo 'second';
ob_end_flush();
$text = ob_get_clean();
echo $text, '/', $calls, "\n";
Ожидаемый результат
firstsecond/1
После первого false PHP передал first без изменения и больше не вызвал callback: second тоже прошла без преобразования. Обработчик редактирования/маскирования секретов нельзя делать единственной защитой содержимого.
Если обработчик заменяет персональные данные или очищает HTML, ошибка не должна открывать путь к выдаче сырого содержимого. Удаляйте недопустимые данные до помещения в вывод и используйте подходящее экранирование в шаблоне. Буферизация меняет маршрут байтов, но не правила проверки входа и формирования безопасного ответа.
Разрешения, состояние и диагностика стека
У буфера есть ещё одна маска — flags из ob_get_status. В ней соединяются разрешения операций и состояние обработчика. Это другой набор значений, чем phase, который PHP передаёт callback. Разрешения задаются при ob_start; состояние меняет движок по мере работы.
| Константа / значение | Назначение |
|---|---|
| PHP_OUTPUT_HANDLER_CLEANABLE = 16 | Разрешает ob_clean. |
| PHP_OUTPUT_HANDLER_FLUSHABLE = 32 | Разрешает ob_flush. |
| PHP_OUTPUT_HANDLER_REMOVABLE = 64 | Разрешает end/get-операции, закрывающие уровень. |
| PHP_OUTPUT_HANDLER_STDFLAGS = 112 | 16 | 32 | 64: все три разрешения по умолчанию. |
| PHP_OUTPUT_HANDLER_STARTED = 4096 | Обработчик уже вызывался. |
| PHP_OUTPUT_HANDLER_DISABLED = 8192 | Обработчик отключён после false/исключения; это не обязательно означает, что уровень удалён. |
| PHP_OUTPUT_HANDLER_PROCESSED = 16384 | Доступна с PHP 8.4. Обработчик успешно обработал порцию с результатом, отличным от false. |
С PHP 8.4 третий аргумент ob_start больше не задаёт статусные STARTED / DISABLED / PROCESSED: это не поддерживаемый способ пропустить инициализацию. Также с 8.4 DISABLED учитывается при ob_clean и ob_flush; до этого эти две операции могли снова вызывать отключённый обработчик. Не стройте новый код на старой лазейке. Поле flags может содержать и внутренние биты типа обработчика: проверяйте нужный бит, не сравнивайте всю маску с одним значением.
ob_get_status(false) описывает активный уровень: name, type, flags, level, chunk_size, buffer_size и buffer_used. true возвращает массив всех уровней от внешнего к внутреннему. buffer_used — байты текста; buffer_size — выделенная ёмкость. Имена closure в PHP 8.5.11 включают место определения и не обязаны быть одинаковыми между файлами. Для стабильного мониторинга нужны ваши собственные метки, а не парсинг отладочного имени.
Исключения, shutdown и пределы восстановления
Исключение в callback не откатывает уже переданное тело. Даже если вызывающий код поймал Throwable вокруг ob_flush, исходная порция может уже уйти во внешний буфер, а обработчик — стать DISABLED. Первое действие при ошибке — не обещать новый целый HTTP-ответ, а определить, что ещё принадлежит приложению и что действительно было отправлено.
<?php
declare(strict_types=1);
ob_start();
ob_start(function ($text) {
throw new RuntimeException('Учебный сбой');
});
echo 'pending';
$caught = false;
try { ob_flush(); }
catch (RuntimeException $error) { $caught = true; }
$disabled = (ob_get_status()['flags'] & PHP_OUTPUT_HANDLER_DISABLED) !== 0;
echo 'after';
ob_end_flush();
$text = ob_get_clean();
echo $caught ? "Поймано\n" : "Не поймано\n";
echo $disabled ? "Обработчик отключён\n" : "Активен\n";
echo $text, "\n";
Ожидаемый результат
Поймано
Обработчик отключён
pendingafter
Это изолированный CLI-опыт PHP 8.5.11. pending не исчезла из-за исключения; after прошла через отключённый обработчик без повторного вызова. Внешний захват позволяет увидеть результат, не делая сетевых выводов.
| Ситуация по официальному описанию | Что важно учитывать |
|---|---|
| Непойманное исключение / фатальная ошибка вне shutdown | Выполнение прекращается, запускается завершение. При flush/end_flush/get_flush исходные байты могут оказаться перед сообщением об ошибке. Ошибка в callback не равна гарантированной очистке. |
| Непойманное исключение / фатальная ошибка в callback во время shutdown | Ни его накопленные данные, ни сообщение этой ошибки не обязаны пройти в тело через данный handler; отдельный журнал/stderr остаётся другим каналом. Не диагностируйте успех по пустому телу. |
| Нефатальная ошибка при обработке | Выполнение может продолжиться. При false наружу могут попасть исходный буфер и вывод сообщения; при другом результате передаётся возвращённый текст. На видимость влияют настройки диагностики и SAPI. |
| Вывод из callback | Исторические правила смешивания вывода не стоит использовать как API: в PHP 8.5 такое поведение deprecated. Возвращайте строку, отдельно записывайте диагностические данные. |
Точные форматы фатальных сообщений, путей и stderr различаются между средами. Учебные проверки отдельно фиксируют статус процесса, фазу и поток, не объявляя единый HTML ошибки переносимым. Журналируйте идентификатор операции и тип сбоя, не весь буфер с токенами, cookies или платёжными данными. В output handler не размещают транзакцию БД, списание денег или надёжную отправку уведомления: его жизненный цикл служит выводу.
Отправка заголовков и тела: где кончаются гарантии PHP
В CLI длительная команда может печатать этапы и вызывать flush. Для веба один такой вызов не доказывает немедленный показ: сначала нужно передать данные из всех принадлежащих вам пользовательских уровней, затем PHP/SAPI, затем инфраструктура. ob_implicit_flush автоматизирует только системную просьбу после печати. Пустая строка и один header не являются непустым блоком вывода; управляющий байт вроде перевода строки является.
Веб-SAPI обычно отправляет сначала заголовки, затем тело. Даже когда тело ещё удерживается внешним ob_start, явный flush может уже отправить заголовки: это отдельно подтверждено встроенным cli-server. С PHP 8.4 FastCGI поддерживает flush одних заголовков без тела. Слишком ранний сброс способен помешать сжимающему handler добавить нужные заголовки. Оставляйте Content-Type, статус и cookies на управляемом этапе до отправки; буферизация тела не возвращает заголовки назад после фактической отправки.
В изолированном тесте встроенного PHP-сервера можно проверить, что печать внутри активного буфера ещё позволяет задать заголовок, а после end_flush + flush заголовки уже отправлены. Это проверка конкретного cli-server, без reverse proxy, TLS или браузерного рендеринга. Для production streaming нужен отдельный тест реальной цепочки. Примеры карточек flush и ob_implicit_flush намеренно не имитируют скорость sleep или лишними пробелами.
Готовый обработчик на практике: ссылки и формы
Исторический URL-Rewriter полезен для понимания готового обработчика: приложение регистрирует пару name/value, печатает HTML, а handler меняет подходящие ссылки и формы при обработке. Это не парсер произвольного современного фронтенда и не механизм безопасности. При собственной генерации шаблона обычно яснее построить нужную ссылку явно и затем экранировать её в HTML-контексте.
- output_add_rewrite_var создаёт живой URL-Rewriter
- HTML ждёт в его буфере
- Flush применяет актуальные пары к подходящим URL
- reset очищает пары, сохраняя handler
- End закрывает handler; повторный add уже не перезапустит его в том же запросе
Важен момент обработки: reset до flush уберёт пары и для ранее напечатанного, но ещё ожидающего HTML. После flush reset не меняет уже обработанную строку во внешнем буфере. Одноимённые пары добавляются, а не заменяют друг друга. Hidden input читатель может изменить; метка в URL попадает в историю и журналы. Примеры output_add_rewrite_var и output_reset_rewrite_vars используют вымышленные домены и несекретные метки, а не session ID.
Относительные ссылки переписываются по tag/attribute; абсолютные дополнительно ограничиваются host-настройкой. form= создаёт по скрытому input на пару. Типовой пример из официального раздела rewrite показывает именно такую обработку выхода; он не обещает, что любая строка, содержащая URL, будет изменена. В JS, CSS или текстовом письме такой механизм не заменяет явное построение данных.
Рабочая граница шаблона: результат, ошибка и владение
Сделаем границу шаблона предсказуемой: шаблон фиксирован разработчиком, данные передаются явно, функция возвращает строку. При исключении она закрывает собственный буфер, затем пробрасывает ошибку вызывающему коду. Договор важен: подключаемый файл сам не открывает и не закрывает буферы, не отправляет ответ и не выполняет внешние действия.
<?php
declare(strict_types=1);
function renderReceipt($product) {
ob_start();
try {
require __DIR__ . '/receipt.php';
} catch (Throwable $error) {
ob_end_clean();
throw $error;
}
return ob_get_clean();
}
$before = ob_get_level();
try { renderReceipt(''); }
catch (InvalidArgumentException $error) { echo "Квитанция не готова\n"; }
echo ob_get_level() === $before ? "Уровень восстановлен\n" : "Остался буфер\n";
echo renderReceipt('Чай & мёд'), "\n";
Ожидаемый результат
Квитанция не готова
Уровень восстановлен
<p>Чай & мёд</p>
Путь задан кодом, не входным параметром пользователя. Ошибка не проглатывается и незавершённая квитанция не возвращается как успешная. Для шаблонов, нарушающих договор о буферах, эта простая обёртка недостаточна; изоляцию создаёт архитектура, не один try/catch.
Кэширование такого текста — отдельное решение: ключ должен учитывать нужный контекст пользователя, языка и прав, а время жизни — изменения данных. Сам ob_get_clean не делает строку безопасной для общего кэша. Аналогично захваченная HTML-квитанция ещё не отправлена почтой и не превращена в PDF: это следующий явный шаг приложения.
- Для новой небольшой функции начните с return string, если перехватывать печать не требуется.
- Для готового печатающего шаблона захватывайте ровно его область и держите договор о владении уровнями.
- Для огромного экспорта не храните весь результат в памяти без обоснованного ограничения объёма.
- Для HTTP-прогресса тестируйте реальный серверный маршрут; не заменяйте договор протокола вызовом flush.
Все функции категории и следующий шаг
В категории 16 функций. Девять ранее разобранных карточек сохраняют свои адреса в главе «Буферизация вывода»; здесь приведён полный контракт и связанный маршрут. Выберите операцию по намерению, а затем проверьте её разрешения, побочные эффекты и обработку false.
| Функция | Задача |
|---|---|
flush | Системная просьба о сбросе |
ob_clean | Очистить текущий уровень |
ob_end_clean | Очистить и закрыть |
ob_end_flush | Передать и закрыть |
ob_flush | Передать без закрытия |
ob_get_clean | Забрать raw-текст и закрыть с очисткой |
ob_get_contents | Снять raw-содержимое |
ob_get_flush | Забрать raw-текст и передать результат наружу |
ob_get_length | Число накопленных байтов |
ob_get_level | Количество уровней |
ob_get_status | Состояние активного или всех уровней |
ob_implicit_flush | Автоматический системный flush |
ob_list_handlers | Имена зарегистрированных обработчиков |
ob_start | Создать пользовательский уровень |
output_add_rewrite_var | Добавить rewrite-пару |
output_reset_rewrite_vars | Удалить пользовательские rewrite-пары |
Дальше: шаблоны и HTTP-ответ · системная обработка ошибок · callback и замыкания.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
ob_get_lengthПроверить количество накопленных байтов без получения копии текста.
ob_get_length(): int|falseПроверить количество накопленных байтов без получения копии текста.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. |
| Возврат | Байты исходного активного буфера; 0 для пустого; false при отсутствии. |
| Границы | Не число Unicode-символов, не размер памяти allocation и не длина результата handler. |
- Чай лежит в UTF-8 буфере
- Символов 3, байтов 6
- ob_get_length возвращает 6 без получения HTML
Если нужно контролировать объём накопленной печати, ob_get_length читает число байтов активного буфера. Кириллица и emoji занимают несколько байтов; это размер данных до преобразования callback, не итоговый размер gzip и не число видимых знаков.
<?php
declare(strict_types=1);
var_dump(ob_get_length());
ob_start();
$empty = ob_get_length();
echo 'Чай';
$used = ob_get_length();
ob_end_clean();
var_dump($empty, $used);
Ожидаемый результат
bool(false)
int(0)
int(6)
Первый false относится к изолированному CLI без начального буфера. Ноль — существующий пустой буфер. Для отличия используйте === false, а не if (!$length).
<?php
declare(strict_types=1);
ob_start();
echo '<p>Чай с мятой</p>';
$bytes = ob_get_length();
if ($bytes === false) { throw new RuntimeException('Буфер отсутствует'); }
if ($bytes > 16) {
ob_end_clean();
echo "Карточка превысила учебный бюджет\n";
} else {
$html = ob_get_clean();
echo $html;
}
Ожидаемый результат
Карточка превысила учебный бюджет
Число 16 намеренно мало для опыта. Проверка после echo обнаруживает превышение, но не предотвращает пиковое выделение памяти самого echo. Для большого потока нужны ограничения источника/порционная обработка, а не обещание hard cap от ob_get_length.
Соседние операции: ob_get_contents, ob_get_status.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
ob_get_statusУвидеть стек и состояние обработки, отличив занятые байты от порога и выделенной памяти.
ob_get_status(bool $full_status = false): arrayУвидеть стек и состояние обработки, отличив занятые байты от порога и выделенной памяти.
| Параметр / результат | Контракт |
|---|---|
| full_status | false — активный уровень; true — все уровни от внешнего к внутреннему. |
| Возврат | Один массив состояния либо массив таких массивов; [] при отсутствии буферов. Все7полей разобраны ниже. |
| Версии | API с PHP4.2.0; PROCESSED с8.4, с8.4 DISABLED учитывается также ob_clean/ob_flush. |
| Ошибки | Неподходящий PHP-тип full_status при strict_types даёт TypeError; само чтение состояние не меняет. |
- Внешний буфер ждёт результат
- Внутренний накопил6байт
- get_status(true) возвращает два описания
- Вывод и содержимое сохраняются
Для диагностики «куда делся мой вывод» одного количества уровней мало. ob_get_status показывает состояние активного буфера, а с true — весь стек от внешнего к внутреннему. Это диагностический снимок: его чтение не вызывает handler и не отправляет содержимое клиенту.
<?php
declare(strict_types=1);
ob_start(null, 4096);
echo 'Чай';
$state = ob_get_status();
ob_end_clean();
echo $state['chunk_size'], '/', $state['buffer_used'], '/', $state['buffer_size'], "\n";
echo $state['name'], '/', $state['type'], "\n";
Ожидаемый результат
4096/6/4096
default output handler/0
chunk_size — порог запуска обработки, buffer_used — реальные 6 байт, buffer_size — выделенная ёмкость. Равенство ёмкости порогу здесь не делает её жёстким максимумом: большой echo может потребовать расширения до обработки.
| Ключ одного состояния | Значение |
|---|---|
| name | Имя обработчика; формат callable/Closure разбирает ob_list_handlers. Может включать путь/строку исходника, не публикуйте весь dump посетителю. |
| type | 0 — внутренний, 1 — пользовательский обработчик. |
| flags | Сочетает признаки типа, разрешённые операции и runtime status. Нельзя интерпретировать всю маску как phase callback. |
| level | Номер уровня с нуля; ob_get_level() считает с единицы. |
| chunk_size | Настроенный порог автоматической обработки в байтах, 0 — без порога. |
| buffer_size | Выделенная ёмкость в байтах: начально 16384 при chunk_size=0, иначе округление вверх до кратного4096; может увеличиваться порциями4096. |
| buffer_used | Количество накопленных исходных байтов, как ob_get_length. |
<?php
declare(strict_types=1);
ob_start();
ob_start(static fn(string $buffer, int $phase): string => strtoupper($buffer));
echo 'tea';
$before = ob_get_status();
ob_flush();
$after = ob_get_status();
$all = ob_get_status(true);
ob_end_flush();
$output = ob_get_clean();
var_dump(($before['flags'] & PHP_OUTPUT_HANDLER_STARTED) !== 0);
var_dump(($after['flags'] & PHP_OUTPUT_HANDLER_STARTED) !== 0);
var_dump(($after['flags'] & PHP_OUTPUT_HANDLER_PROCESSED) !== 0);
echo count($all), '/', $output, "\n";
Ожидаемый результат
bool(false)
bool(true)
bool(true)
2/TEA
STARTED означает, что callback уже вызывался, а не только что буфер создан. PROCESSED добавлен в PHP8.4. Внешний буфер собирает изменённый ответ; чтение all ничего не закрывает.
full_status=false возвращает один ассоциативный массив, true — массив состояний с числовыми ключами уровней. Если буферов нет, оба режима дают []. STARTED и PROCESSED означают успешную обработку; при false/ошибке handler получает DISABLED. Отключённый handler в PHP8.4+ не вызывается повторно при clean/flush/закрытии, а исходные данные могут пойти дальше без преобразования. Поэтому DISABLED — важный сигнал отказа, особенно если обработчик должен скрывать или преобразовывать содержимое.
Не сравнивайте flags с одним целым: используйте битовое & и нужную константу. Не смешивайте status STARTED с phase START и не пытайтесь вывести состояние callback из type. Объём памяти всего процесса больше buffer_size: существуют исходные строки, снимки, результат handler и внешние уровни. Полный контекст — модель статусов.
Соседние операции: ob_get_level, ob_list_handlers, ob_get_length, ob_start.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
ob_list_handlersУзнать, какие обработчики участвуют в цепочке, и найти лишний уровень при диагностике.
ob_list_handlers(): arrayУзнать, какие обработчики участвуют в цепочке, и найти лишний уровень при диагностике.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. |
| Возврат | Массив имён от первого открытого до активного; [] без буферов. |
| Имена | default output handler, именованный callable, Class::__invoke или version-dependent имя Closure. Это диагностические имена, не обязательно callable. |
| Границы | Не исполняет callbacks, не читает текст и не заменяет status/flags. |
- Первым открыт default output handler
- Затем receiptOutput
- Список: [default, receiptOutput]
- Обработка при flush идёт изнутри наружу
В CMS одновременно могут работать шаблонный буфер, сжатие и библиотечный обработчик. ob_list_handlers возвращает их имена в порядке открытия: внешние раньше, текущий внутренний последним. Это список для диагностики, а не набор callable, которые нужно вызывать самостоятельно.
<?php
declare(strict_types=1);
function receiptOutput(string $buffer, int $phase): string { return $buffer; }
ob_start();
ob_start('receiptOutput');
$names = ob_list_handlers();
ob_end_clean(); ob_end_clean();
echo implode(' -> ', $names), "\n";
Ожидаемый результат
default output handler -> receiptOutput
Имя default output handler соответствует пропуску callback/null. Именованная пользовательская функция видна по своему имени; внешний уровень стоит первым.
<?php
declare(strict_types=1);
final class ReceiptOutput
{
public function __invoke(string $buffer, int $phase): string { return $buffer; }
}
ob_start(new ReceiptOutput());
$names = ob_list_handlers();
ob_end_clean();
echo $names[0], "\n";
var_dump(ob_list_handlers());
Ожидаемый результат
ReceiptOutput::__invoke
array(0) {
}
__invoke позволяет объекту работать как callable. В последнем выводе стек изолированного CLI уже пуст. Для понимания объекта сначала прочитайте callable/магические методы, а для обычного захвата объекта не требуется.
Для именованных функций/методов указывается полное имя, для invokable-объекта — Class::__invoke. Формат имени Closure менялся: в PHP8.5.11 он может содержать {closure:путь:строка}, хотя краткая страница всё ещё говорит Closure::__invoke. Не пишите прикладное условие на точном строковом имени анонимного handler; используйте известную конфигурацию и собственную границу владения. Список не раскрывает содержимое, flags или факт успешной обработки — за ними обращаются к ob_get_status.
Соседние операции: ob_get_status, ob_get_level, ob_start.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
ob_get_flushЗавершить буфер с отправкой результата handler и одновременно получить снимок его исходного текста.
ob_get_flush(): string|falseЗавершить буфер с отправкой результата handler и одновременно получить снимок его исходного текста.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. |
| Разрешение | REMOVABLE; FLUSHABLE не требуется. |
| Возврат | Исходный текст активного буфера; при отсутствии false+E_NOTICE. |
| Два результата | Return содержит исходник; flush передаёт изменённый handler-результат. При запрете закрытия PHP8.5 может вернуть строку+notice без удаления. |
| Эффект / ошибки | Handler FINAL, нормальное закрытие уровня; callback может бросить исключение. |
- Исходный буфер содержит tea
- Return get_flush → tea в переменной
- Handler strtoupper → TEA наружу
- Активный уровень закрывается
Иногда нужно одновременно передать результат генератора дальше и сохранить его исходную печать для локального повторного анализа. ob_get_flush делает оба действия: возвращает исходный текст, но наружу отправляет результат handler. При преобразовании эти две строки могут различаться. Без обработчика они совпадут, но это частный случай.
<?php
declare(strict_types=1);
ob_start();
ob_start(static fn(string $buffer, int $phase): string => strtoupper($buffer));
echo 'tea';
$raw = ob_get_flush();
$sent = ob_get_clean();
echo 'Возвращено: ', $raw, "\n";
echo 'Передано: ', $sent, "\n";
Ожидаемый результат
Возвращено: tea
Передано: TEA
Внешний обычный буфер изображает следующую ступень. Если повторно echo $raw после get_flush без такого приёмника, можно случайно продублировать уже отправленный фрагмент.
<?php
declare(strict_types=1);
ob_start();
$empty = ob_get_flush();
$notice = false;
set_error_handler(static function(int $level, string $message) use (&$notice): bool {
if ($level === E_NOTICE) { $notice = true; return true; }
return false;
});
try { $missing = ob_get_flush(); }
finally { restore_error_handler(); }
var_dump($empty, $missing, $notice);
Ожидаемый результат
string(0) ""
bool(false)
bool(true)
Первый пустой буфер успешно закрыт. Повторный вызов уже без буфера даёт false и notice. Это отличается от ob_get_clean без буфера, который возвращает false без notice.
Требуется REMOVABLE, но не FLUSHABLE. Handler получает FINAL (и START при первом вызове), без FLUSH. Преобразование и его ошибки не меняют правила о том, какая строка была захвачена для возврата. Для большинства render-функций нужен get_clean: отправку лучше делать в одном известном месте. get_flush выбирают только когда двойное действие необходимо и дальнейшая обработка понимает, что байты уже ушли в следующий уровень.
Соседние операции: ob_get_clean, ob_end_flush, ob_get_contents, ob_flush.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
ob_implicit_flushВключить или выключить автоматические системные flush после печати, сохранив ясную политику приложения.
ob_implicit_flush(bool $enable = true): voidВключить или выключить автоматические системные flush после печати, сохранив ясную политику приложения.
| Параметр / результат | Контракт |
|---|---|
| enable | true включает, false отключает; пропуск означает true. С PHP 8.0 тип bool, прежде int. |
| Возврат | void; предыдущую политику не возвращает. |
| Слой | Автоматический системный flush после непустого блока вывода; пользовательские буферы не очищает. |
| Границы | Не гарантия немедленного показа; частые сбросы могут ухудшить веб-производительность. |
- enable=true
- Каждый непустой блок печати инициирует flush
- Активный ob_start по-прежнему собирает текст
- Владелец восстанавливает выбранную политику
В диагностической CLI-команде хочется не писать flush после каждого echo. ob_implicit_flush(true) включает такие автоматические просьбы системному слою; false выключает. Это экономия повторяющегося вызова, но не ускорение самой обработки и не отключение пользовательских буферов.
<?php
declare(strict_types=1);
ob_implicit_flush(true);
ob_start();
echo 'Чай';
$length = ob_get_length();
$text = ob_get_clean();
ob_implicit_flush(false);
echo $length, '/', $text, "\n";
Ожидаемый результат
6/Чай
Несмотря на включённую политику, в активном пользовательском буфере сохранились 6 UTF-8 байт. Изменение относится к этому изолированному процессу; в библиотеке нельзя без согласования менять политику общего вывода.
<?php
declare(strict_types=1);
$applicationFlushPolicy = false;
ob_implicit_flush($applicationFlushPolicy);
try {
ob_implicit_flush(true);
echo "Диагностика: первый этап\n";
echo "Диагностика: второй этап\n";
} finally {
ob_implicit_flush($applicationFlushPolicy);
}
Ожидаемый результат
Диагностика: первый этап
Диагностика: второй этап
Политика здесь известна владельцу приложения и хранится явно. У функции нет getter и нет возврата предыдущего состояния; чтение ini_get не следует считать надёжным журналом всех предыдущих вызовов ob_implicit_flush. Итоговый вывод проверен, сетевые задержки не измерялись.
Непустой блок вывода — echo/print, печать функций, HTML вне PHP и php://output. Даже управляющие байты вроде или NUL могут вызвать автоматический flush; пустая строка и один header без тела такими блоками не считаются. Эта политика не освобождает активные пользовательские буферы. Веб-запрос может сильно пострадать от частых мелких сбросов; обычно такой режим нужен при осознанной диагностике, а не включён для любого сайта.
Аргумент enable с PHP 8.0 имеет bool вместо прежнего int; по умолчанию true. Возврат void. Не сохраняйте результат как прежнюю настройку. В CLI implicit flush обычно включён SAPI по умолчанию, в веб-конфигурации директива implicit_flush по умолчанию 0. Различайте initial ini-конфигурацию и состояние, изменённое кодом в ходе выполнения.
Соседние операции: flush, ob_start, ob_flush.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
output_add_rewrite_varДополнить ссылки/формы старого HTML-шаблона несекретной парой на ступени URL-Rewriter.
output_add_rewrite_var(string $name, string $value): boolДополнить ссылки/формы старого HTML-шаблона несекретной парой на ступени URL-Rewriter.
| Параметр / результат | Контракт |
|---|---|
| name / value | Строковые имя и значение. Все пары добавляются; одноимённые не заменяются. |
| Возврат | Документированный bool успеха/отказа регистрации; true не подтверждает совпадение ссылок или перезапуск уже закрытого handler. |
| Эффекты | Запускает отдельный URL-Rewriter при первом использовании, затем обработка при flush/закрытии по tags/hosts. |
| Версии | С PHP4.3; отдельный от session буфер с7.1; host-настройка фактически url_rewriter.hosts с8.4. |
| Ошибки / границы | Неверные строгие PHP-типы дают TypeError. Результат переписывания следует проверять; это не механизм авторизации/валидации HTML. |
- Добавили campaign=tea
- Шаблон напечатал items.php
- URL-Rewriter при flush дописал ?campaign=tea
- Во внешнем буфере лежит уже изменённый HTML
У старого печатающего шаблона много относительных ссылок, и к ним нужно добавить несекретную метку кампании. output_add_rewrite_var запоминает пару name/value и при необходимости запускает отдельный URL-Rewriter буфер. При обработке HTML этот handler дописывает query-параметры и скрытые поля форм по настройкам. Это исторический инструмент; в новом шаблоне явное формирование нужных URL обычно легче контролировать.
<?php
declare(strict_types=1);
$oldTags = ini_get('url_rewriter.tags');
ini_set('url_rewriter.tags', 'a=href,form=');
ob_start();
output_add_rewrite_var('campaign', 'tea set');
echo '<a href="items.php">Товары</a><form action="save.php" method="post"></form>';
ob_end_flush(); // Закрывается внутренний URL-Rewriter.
$html = ob_get_clean();
ini_set('url_rewriter.tags', $oldTags);
echo $html, "\n";
Ожидаемый результат
<a href="items.php?campaign=tea%20set">Товары</a><form action="save.php" method="post"><input type="hidden" name="campaign" value="tea set" /></form>
Два уровня открыты в известном порядке: внешний обычный и внутренний, созданный add. Параметр query кодируется для URL; значение hidden input — для HTML. Скрытое поле доступно пользователю и не подтверждает его права или подлинность.
<?php
declare(strict_types=1);
$oldTags = ini_get('url_rewriter.tags');
$oldHosts = ini_get('url_rewriter.hosts');
ini_set('url_rewriter.tags', 'a=href');
ini_set('url_rewriter.hosts', 'shop.example.invalid');
ob_start();
output_add_rewrite_var('campaign', 'tea');
echo '<a href="https://shop.example.invalid/items">Own</a>';
echo '<a href="https://other.example.invalid/items">Other</a>';
ob_end_flush();
$html = ob_get_clean();
ini_set('url_rewriter.tags', $oldTags);
ini_set('url_rewriter.hosts', $oldHosts);
echo $html, "\n";
Ожидаемый результат
<a href="https://shop.example.invalid/items?campaign=tea">Own</a><a href="https://other.example.invalid/items">Other</a>
Домены .invalid вымышлены, сетевых запросов нет. В PHP8.4+ для этого используется url_rewriter.hosts; прежде фактически использовалась session.trans_sid_hosts. Не берите доверенный whitelist из произвольного HTTP_HOST входящего запроса.
<?php
declare(strict_types=1);
$oldTags = ini_get('url_rewriter.tags');
ini_set('url_rewriter.tags', 'a=href');
ob_start();
output_add_rewrite_var('campaign', 'first');
echo '<a href="one.php">One</a>';
ob_end_flush();
$first = ob_get_clean();
$accepted = output_add_rewrite_var('campaign', 'second');
$active = in_array('URL-Rewriter', ob_list_handlers(), true);
ini_set('url_rewriter.tags', $oldTags);
var_dump($accepted, $active);
echo $first, "\n";
Ожидаемый результат
bool(true)
bool(false)
<a href="one.php?campaign=first">One</a>
В PHP8.5.11 после закрытия URL-Rewriter повторный add сохраняет пару и может вернуть true, но не создаёт заново буфер. Продолжать прежний маршрут нужно через reset/смену значений при живом handler, а не через close/reopen. Наличие true также не доказывает, что в HTML вообще найдена подходящая ссылка.
Все пары накапливаются до выключения handler или reset. Повторное имя не заменяет старое: можно получить одинаковые query-ключи и несколько input с одним name, включая дубли уже существующих в HTML. Не передавайте через этот механизм session ID, пароль или токен: URL попадает в историю/журналы и может раскрыться другим участникам цепочки. Несекретную метку тоже проверяют на сервере по её бизнес-правилу.
url_rewriter.tags по умолчанию form=; чтобы переписывать a href, явно добавьте a=href. Формат — пары tag=attribute через запятую без пробелов; form= или любой form-атрибут включает скрытые поля. Повтор одного tag использует первую запись. url_rewriter.hosts — список host через запятую без пробелов, по умолчанию ориентируется на HTTP_HOST; относительные ссылки обрабатываются без выбора внешнего host. Полный список настроек — конфигурация.
С PHP7.1 пользовательский rewrite имеет отдельный буфер от session trans_sid, максимум два внутренних URL-Rewriter — по одному на каждую роль. После окончательного закрытия пользовательский handler заново не запускается в том же запросе. Если активен другой вложенный буфер, ob_end_flush закроет его, а не автоматически URL-Rewriter: всегда учитывайте реальный стек.
Соседние операции: output_reset_rewrite_vars, ob_list_handlers, ob_flush, ob_end_flush.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
output_reset_rewrite_varsУбрать зарегистрированные rewrite-пары, сохранив живой обработчик для следующих фрагментов.
output_reset_rewrite_vars(): boolУбрать зарегистрированные rewrite-пары, сохранив живой обработчик для следующих фрагментов.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет; сбрасываются все пользовательские пары целиком. |
| Возврат | true при успешном сбросе, false при отказе; пустой набор в PHP8.5.11 также даёт true. |
| Эффект | Не выключает handler, не меняет уже обработанный HTML; pending текст зависит от актуального набора при обработке. |
| Версии | С PHP4.3; с PHP7.1 отдельные пользовательские rewrite-пары не смешиваются с session trans_sid. |
- One обработана с campaign
- reset удалил зарегистрированную пару
- Two обрабатывается без пары
- Уже переданная One не изменяется
Одна часть отчёта должна содержать метку кампании, следующая — уже без неё. output_reset_rewrite_vars убирает зарегистрированные пользовательские пары, но не закрывает handler и не отменяет уже обработанный/отправленный HTML. Важно различать время печати и время преобразования: ещё лежащий в буфере текст будет обработан с актуальным набором пар.
<?php
declare(strict_types=1);
$oldTags = ini_get('url_rewriter.tags');
ini_set('url_rewriter.tags', 'a=href');
ob_start();
output_add_rewrite_var('campaign', 'tea');
echo '<a href="one.php">One</a>';
ob_flush();
$reset = output_reset_rewrite_vars();
echo '<a href="two.php">Two</a>';
ob_end_flush();
$html = ob_get_clean();
ini_set('url_rewriter.tags', $oldTags);
var_dump($reset);
echo $html, "\n";
Ожидаемый результат
bool(true)
<a href="one.php?campaign=tea">One</a><a href="two.php">Two</a>
Первый ob_flush успел переписать One и передать её во внешний буфер. reset не изменяет уже готовый внешний HTML; вторая ссылка сформирована без пользовательских пар.
<?php
declare(strict_types=1);
$oldTags = ini_get('url_rewriter.tags');
ini_set('url_rewriter.tags', 'a=href');
ob_start();
output_add_rewrite_var('campaign', 'tea');
echo '<a href="one.php">One</a>';
output_reset_rewrite_vars();
ob_end_flush();
$html = ob_get_clean();
ini_set('url_rewriter.tags', $oldTags);
echo $html, "\n";
Ожидаемый результат
<a href="one.php">One</a>
echo ещё не равен выполненному rewrite. Без промежуточного flush ссылка ждала в буфере; на момент обработки зарегистрированных пар уже нет. Сбрасывайте/меняйте набор в предсказуемой точке конвейера.
Возврат bool: true при успешном сбросе, false при отказе по документированному API. В проверенной PHP8.5.11 очистка пустого набора также успешна; TypeError для аргументов тут не нужен, у функции их нет. С PHP7.1 сброс касается только пар output_add_rewrite_var, а не session trans_sid. Он не удаляет соответствующие данные из сессии/БД, не очищает сам HTML-буфер и не закрывает URL-Rewriter.
Соседние операции: output_add_rewrite_var, ob_flush, ob_end_flush.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
Источники и обсуждения
- PHP: book.outcontrol
- PHP: outcontrol.setup
- PHP: outcontrol.configuration
- PHP: outcontrol.constants
- PHP: outcontrol.output-buffering
- PHP: outcontrol.flushing-system-buffers
- PHP: outcontrol.user-level-output-buffers
- PHP: outcontrol.what-output-is-buffered
- PHP: outcontrol.nesting-output-buffers
- PHP: outcontrol.buffer-size
- PHP: outcontrol.operations-on-buffers
- PHP: outcontrol.output-handlers
- PHP: outcontrol.working-with-output-handlers
- PHP: outcontrol.flags-passed-to-output-handlers
- PHP: outcontrol.output-handler-return-values
- PHP: outcontrol.examples
- PHP: outcontrol.examples.basic
- PHP: outcontrol.examples.rewrite
- PHP: ref.outcontrol