Глава 37 · Практика
ob_start: шаблоны, вложенные буферы и отправка ответа
Зачем перехватывать echo, как вернуть HTML строкой и почему flush не обещает немедленный показ в браузере.
ob_start: как получить напечатанный HTML в переменную
Шаблон страницы уже выводит HTML через echo, а теперь тот же результат нужен как строка для письма. Буфер вывода временно собирает напечатанное: вы сможете забрать строку, не отправляя её сразу посетителю.
Что уже пригодится. return и echo, соединение строк
- ob_start(): открыть буфер
- echo печатает в буфер
- ob_get_clean(): получить строку и закрыть буфер
- $html содержит результат
- Использовать строку там, где она нужна
ob_start() начинает сбор вывода. ob_get_clean() возвращает накопленный текст и удаляет этот буфер. Присваивание сохраняет возвращённую строку.
<?php
declare(strict_types=1);
ob_start();
echo 'Заказ ';
echo '№42';
$message = ob_get_clean();
echo 'Собрано: ' . $message . "\n";
Ожидаемый результат
Собрано: Заказ №42
Первые два echo не появляются отдельно: их текст попал в буфер. Последний echo выполняется после закрытия буфера и показывает собранную строку. Для обычной склейки строк буфер не нужен; он полезен, когда существующий шаблон уже печатает HTML.
Следующая ступень. Ниже помещаем сборку шаблона в функцию, обеспечиваем очистку при исключении и разбираем вложенные буферы. Буфер не заменяет HTML-экранирование.
Буфер превращает вывод шаблона в значение
Как echo шаблона превращается в строку HTML
Одну карточку товара нужно использовать на странице и в письме. Сначала получим её разметку как значение.
- ob_start()
PHP открывает свой уровень буфера: следующий вывод пока накапливается в нём.
- Подключить шаблон
Шаблон получает подготовленные данные и печатает разметку через echo и HTML-вставки.
- Байты попали в буфер
Это тело будущего HTML. Буфер сам не экранирует данные и не хранит HTTP-заголовки.
- ob_get_clean()
Возвращает накопленный HTML строкой и закрывает текущий буфер.
- Использовать строку
Передать HTML в письмо или вернуть в ответе. Сам ob_start письмо не отправляет.
При исключении рендерер убирает открытые им уровни буфера. flush не гарантирует немедленный показ на экране браузера.
Представьте карточку товара, которую нужно показать на странице, положить в письмо и сохранить в краткий кеш. Старый PHP-шаблон печатает HTML через echo и участки разметки. Без буфера include сразу отправляет вывод дальше, а вызывающий код не получает строку. ob_start создаёт буфер: последующий вывод накапливается, затем ob_get_clean возвращает его содержимое и закрывает буфер. Это позволяет рассматривать шаблон как функцию, которая возвращает представление.
Такой приём полезен в небольшом серверном рендерере, при адаптации старой библиотеки с echo и в тесте HTML-компонента. Он не обязателен для каждого include: файл с объявлениями классов вообще не должен выводить данные. Современный шаблонизатор может уже возвращать строку или управлять собственным буфером. Прежде чем добавлять ещё один, узнайте контракт существующего слоя.
Буфер содержит тело, а не HTTP-заголовки. Пока тело удерживается и ничего не отправлено ниже, ещё можно установить header или cookie. Но использовать ob_start как универсальное лечение «headers already sent» опасно: источник раннего вывода остаётся, а ограничение размера или вложенный flush могут проявить проблему снова. Правильный порядок — сначала решение и заголовки, затем отправка представления.
- ob_start: создать уровень
- Шаблон печатает HTML
- Буфер хранит байты тела
- ob_get_clean: получить и закрыть
- Контроллер выбирает статус/заголовки
- echo готового тела
Рендерер должен убрать свой буфер при исключении
<?php
declare(strict_types=1);
function renderCard(string $title, bool $fail = false): string {
$level = ob_get_level();
ob_start();
try {
echo '<h2>'.htmlspecialchars($title,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8').'</h2>';
if ($fail) throw new RuntimeException('template-failed');
$html = ob_get_clean();
if ($html === false) throw new RuntimeException('buffer-missing');
return $html;
} finally {
while (ob_get_level() > $level) ob_end_clean();
}
}
echo renderCard('Tea & Coffee')."
";
$before = ob_get_level();
try { renderCard('Broken',true); } catch (RuntimeException $e) { echo $e->getMessage()."
"; }
echo ob_get_level() === $before ? "buffer-restored
" : "leaked
";
Ожидаемый результат
<h2>Tea & Coffee</h2>
template-failed
buffer-restored
finally удаляет только уровни, созданные внутри функции; чужой внешний буфер не уничтожается. Контракт шаблона запрещает закрывать чужие уровни или создавать неудаляемые буферы. Частичный HTML ошибочного шаблона не попадает в ответ.
Буферы образуют стек, а не один глобальный мешок
<?php
declare(strict_types=1);
ob_start();
echo 'page:';
ob_start();
echo 'badge';
$badge = ob_get_clean();
echo strtoupper($badge);
$page = ob_get_clean();
echo $page."
";
echo 'saved-component:'.$badge."
";
Ожидаемый результат
page:BADGE
saved-component:badge
Внутренний вывод не потерян и не отправлен немедленно: он получен строкой, преобразован и помещён во внешний уровень. ob_end_flush вместо ob_get_clean отправил бы содержимое во внешний буфер, а не вернул бы его как строку.
ob_get_contents копирует содержимое, но не закрывает уровень. ob_clean очищает текущий уровень, оставляя его активным. ob_end_clean выбрасывает содержимое и закрывает уровень. ob_flush передаёт накопленное ниже, но продолжает буферизацию; ob_end_flush передаёт и завершает. Выбор зависит от того, нужен ли результат как значение и кто владеет уровнем.
В callback обработчика буфера нельзя бездумно вызывать функции управления тем же стеком: PHP запрещает ряд таких операций и может завершить выполнение ошибкой. Для обычного шаблона callback не требуется. Большой буфер удерживает всё тело в памяти, а получение строки и последующие преобразования могут увеличить пиковое потребление. Для гигабайтного экспорта лучше потоковое чтение и выдача, а не сборка одной строки.
От PHP до экрана есть ещё несколько буферов
После PHP данные могут буферизовать веб-сервер, FastCGI, сжатие, обратный прокси и браузер. Поэтому echo, ob_flush и flush не гарантируют немедленное появление текста на экране. Для потокового ответа нужно согласовать всю цепочку, отключить несовместимую буферизацию и протестировать реальный сервер. Локальный CLI показывает другой путь и не доказывает поведение браузера.
Если клиенту нужен прогресс долгой задачи, часто проще вернуть ID задания и опрашивать отдельный endpoint. SSE подходит для однонаправленных событий, но требует правильного Content-Type, таймаутов, освобождения сессии и контроля количества соединений. Удерживать множество PHP-FPM процессов открытыми ради прогресса может быть дорого. Выбор транспорта связан с инфраструктурой, а не только с наличием flush.
Не кладите персонализированную карточку в общий кеш под ключом одного товара, если в ней есть имя покупателя или его скидка. ob_start лишь помогает получить строку; он не делает кеш безопасным. Ключи, срок жизни и инвалидирование остаются отдельными решениями. Тестируйте экранирование, отсутствие раннего вывода, очистку при исключении и независимость вложенных компонентов.
Шаблон не должен получать весь контекст приложения
Если рендерер подключает PHP-файл, тот исполняется в области видимости подключения. Не передавайте ему автоматически все глобальные переменные или произвольный массив через extract с перезаписью имён. Лучше задать явные данные представления и ограничить доступные зависимости. Имя шаблона выбирают из кода или разрешённого списка, а не превращают параметр URL в путь include.
Ошибка PHP warning не всегда становится исключением. Поэтому пример с finally гарантирует очистку при Throwable, но не обещает автоматически распознать каждое предупреждение как неуспех. Политика error handler задаётся приложением и тестируется отдельно. В production display_errors не должен добавлять технический текст в HTML или JSON.
В тестах сравнивайте смысл разметки: экранированный заголовок, обязательную ссылку, отсутствие секретного поля. Снимок всей строки полезен для маленького стабильного компонента, но становится хрупким для большой страницы с временными значениями. Буфер помогает получить результат для проверки, а качество проверки определяется выбранными утверждениями.
Практика: проверьте решение, а не только успешный путь
Библиотека печатает отчёт и иногда бросает исключение. Как не допустить утечки половины отчёта в JSON API?
Разбор решения
Обернуть вызов собственным буфером, запомнить исходный уровень, получить строку только после успеха, в finally убрать свои уровни. Заголовки JSON устанавливать после успешного формирования тела. Если библиотека сама flush-ит данные наружу, её контракт нужно изменить: внешний буфер не гарантирует перехват произвольного поведения.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
ob_startПерехватить вывод печатающего шаблона, чтобы получить его как строку или обработать на одной границе.
ob_start(?callable $callback = null, int $chunk_size = 0, int $flags = PHP_OUTPUT_HANDLER_STDFLAGS): boolПерехватить вывод печатающего шаблона, чтобы получить его как строку или обработать на одной границе.
| Параметр / результат | Контракт |
|---|---|
| callback | null — неизменяющий обработчик; callable — обработчик (string buffer,int phase). Для отдельных внутренних alias действуют специальные правила. |
| chunk_size | Порог автоматической обработки в байтах; 0 — без автоматического порога, 1 — после каждого непустого блока. Не hard memory cap. |
| flags | Маска CLEANABLE/FLUSHABLE/REMOVABLE; по умолчанию STDFLAGS. С 8.4 status flags через неё не задаются. |
| Возврат / отказ | true при запуске; false и warning/notice при невозможности создать обработчик. Некорректные типы параметров могут дать TypeError. |
| PHP 8.5 | Вывод из пользовательского callback deprecated; предупреждение обходит handler. |
- ob_start открывает приёмник
- Шаблон echo печатает HTML в него
- ob_get_clean возвращает HTML и закрывает приёмник
- Далее строка передаётся по нужному маршруту
Старый шаблон печатает карточку через echo, а её HTML нужен как строка для письма. ob_start создаёт временный приёмник вывода. Далее шаблон исполняется обычным образом; меняется место, куда попадает печать. Затем ob_get_clean забирает содержимое и закрывает именно этот буфер. Вычисления, запросы в БД и другие побочные действия шаблона при этом выполняются по-настоящему.
<?php
declare(strict_types=1);
ob_start();
echo '<p>Заказ готов</p>';
$html = ob_get_clean();
echo 'Получено: ', $html, "\n";
Ожидаемый результат
Получено: <p>Заказ готов</p>
Никакого браузера для первого опыта не нужно. echo между вызовами попал в буфер, а первый видимый вывод сделан после его закрытия. Если код сразу умеет вернуть строку, дополнительный буфер может быть не нужен.
<?php
declare(strict_types=1);
function renderReceipt(string $product): string
{
ob_start();
try {
require 'receipt.php';
} catch (Throwable $error) {
ob_end_clean();
throw $error;
}
$html = ob_get_clean();
if ($html === false) { throw new RuntimeException('Буфер потерян'); }
return $html;
}
echo renderReceipt('Чай <мятный>'), "\n";
Ожидаемый результат
<p>Чай <мятный></p>
Файл fixture известен разработчику, не приходит из запроса. htmlspecialchars защищает вставку в HTML; сам буфер ничего не экранирует. В примере договор шаблона запрещает ему открывать/закрывать чужие буферы. Обработка ошибки очищает принадлежащий этой функции буфер; общий случай нарушения уровней разобран отдельно.
callback=null/пропуск использует встроенный default output handler, который пропускает содержимое без изменения. Пользовательский callback получает строку buffer и битовую маску phase; возвращает новый текст или false по описанному контракту. Его вызывают при обработке/очистке/завершении, а не сразу от одного ob_start. Проще сначала освоить захват без callback, затем обработчики. Некоторые расширения регистрируют специальные имена alias, например ob_iconv_handler; проверка is_callable сама по себе не описывает этот особый механизм.
chunk_size=0 — автоматического порога нет, остаётся ограничение доступной памяти. Положительное значение — порог в байтах: после очередной операции печати буфер обрабатывается, если достиг или превысил его. Это не жёсткий предел памяти и не разбиение большого echo ровно по N байт. chunk_size=1 запускает обработку после каждого непустого блока вывода; отрицательное значение реализация PHP 8.5 приводит к 0, но прикладная конфигурация должна требовать число ≥0.
<?php
declare(strict_types=1);
$sizes = [];
ob_start(static function (string $buffer, int $phase) use (&$sizes): string {
$sizes[] = strlen($buffer);
return '';
}, 3);
echo 'ab';
echo 'cdef';
echo 'g';
ob_end_flush();
echo implode(', ', $sizes), "\n";
Ожидаемый результат
6, 1
Первые два echo вместе накопили шесть байт, хотя порог равен трём. Последний один байт обработан при завершении. Возврат пустой строки сознательно не передаёт исходный текст наружу; callback только записывает длины в локальный массив.
flags — отдельная маска разрешённых операций: CLEANABLE для ob_clean, FLUSHABLE для ob_flush, REMOVABLE для функций закрытия. Стандартная STDFLAGS объединяет все три. Начиная с PHP 8.4 этим аргументом нельзя заранее выставить runtime status STARTED/DISABLED/PROCESSED. flags=0 оставляет чтение содержимого, но запрещает ручную очистку/flush/удаление; окончательное завершение скрипта всё равно обработает буфер.
Успешный запуск возвращает true; при отказе создать буфер — false и диагностику. Например, несуществующее имя callback в PHP 8.5.11 даёт warning о callable, затем notice об отказе, а не универсальный TypeError для этого случая. Типы chunk_size/flags проверяются как int. Внутри output handler запрещено менять стек через ob_start/clean/flush/end/get_clean/get_flush: это fatal error. С PHP 8.5 печать echo внутри пользовательского handler deprecated; возвращайте строку, диагностируйте вне обработки.
Соседние операции: ob_get_clean, ob_get_contents, ob_get_status.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
Создаёт новый уровень буфера, возвращает успешность. Вывод echo/HTML удерживается до извлечения или передачи ниже. callback может преобразовать данные; chunk_size задаёт порог обработки, а flags — доступные операции.
<?php
declare(strict_types=1);
ob_start();
echo '<strong>Sale</strong>';
$html=ob_get_clean();
echo strlen($html)." bytes\n";
Ожидаемый результат
21 bytes
Компонент не отправлен непосредственно, его можно включить в письмо или проверить тестом.
<?php
declare(strict_types=1);
function legacyBadge(): void { echo 'stock:3'; }
ob_start();
legacyBadge();
$badge=ob_get_clean();
echo strtoupper($badge)."\n";
Ожидаемый результат
STOCK:3
Адаптер превращает печатающую функцию в значение. Сама legacy-функция не должна управлять внешними буферами.
Рядом по задаче. ob_get_clean возвращает строку и закрывает уровень; в главе есть очистка через finally.
ob_get_cleanЗавершить захват шаблона: забрать исходный текст в переменную и закрыть буфер.
ob_get_clean(): string|falseЗавершить захват шаблона: забрать исходный текст в переменную и закрыть буфер.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. |
| Разрешение | REMOVABLE; CLEANABLE не требуется. |
| Возврат | Исходная строка буфера; false без notice при отсутствии. Пустая строка означает пустой буфер. |
| PHP8.5 edge case | При запрете удаления может вернуть снимок строки вместе с E_NOTICE, сохранив буфер; подробный опыт ниже. |
| Handler | CLEAN|FINAL, возвращённое преобразование отбрасывается. Исключения handler возможны. |
- echo заполнил буфер HTML
- get_clean копирует исходный текст в результат
- Удаляет этот уровень без отправки наружу
- Переменная html используется дальше
Шаблон закончил печать. ob_get_clean возвращает накопленный исходный текст, стирает буфер и закрывает его: строка теперь принадлежит переменной. Это обычный завершающий шаг превращения echo-шаблона в функцию render. Передачи этих байтов наружу при успешном clean не происходит.
<?php
declare(strict_types=1);
ob_start();
echo '<p>Чай</p>';
$html = ob_get_clean();
echo $html, ' | ', $html, "\n";
Ожидаемый результат
<p>Чай</p> | <p>Чай</p>
Один раз сформировали строку и дважды использовали её. Повторной работы генератора не было. Печатаем после закрытия, поэтому случайно не дописываем копию в исходный буфер.
<?php
declare(strict_types=1);
$phases = [];
ob_start(static function (string $buffer, int $phase) use (&$phases): string {
$phases[] = $phase;
return strtoupper($buffer);
});
echo 'tea';
$raw = ob_get_clean();
echo $raw, "\n";
var_dump(($phases[0] & PHP_OUTPUT_HANDLER_CLEAN) !== 0,
($phases[0] & PHP_OUTPUT_HANDLER_FINAL) !== 0);
Ожидаемый результат
tea
bool(true)
bool(true)
Callback действительно исполнился, но его TEA отброшено. Возвращается снимок исходного tea. Фраза «handler не работает с get_clean» неверна: он работает, но результат его преобразования не является результатом функции.
<?php
declare(strict_types=1);
$notices = 0;
set_error_handler(static function (int $level, string $message) use (&$notices): bool {
if ($level === E_NOTICE) { ++$notices; return true; }
return false;
});
$base = ob_get_level();
ob_start(static fn(string $buffer, int $phase): string => '', 0, 0);
echo 'kept';
$result = ob_get_clean();
$stillActive = ob_get_level() === $base + 1;
restore_error_handler();
fwrite(STDOUT, ($result === 'kept' && $stillActive && $notices > 0)
? "Строка получена, но буфер не закрыт\n" : "Иное поведение\n");
Ожидаемый результат
Строка получена, но буфер не закрыт
Узкий опыт PHP8.5.11: функция сначала получает текст, затем не может удалить non-removable буфер. fwrite(STDOUT) намеренно обходит его, чтобы показать результат; при shutdown handler возвращает пустую строку и ничего лишнего не печатает. Так защищённые буферы не создают в обычном render: это опыт границы контракта.
Операция требует REMOVABLE, но не CLEANABLE. Пустой успешный результат — "", не false. Callback получает CLEAN|FINAL (и START при первом вызове); его side effects/исключения не исчезают вместе с отбрасываемой строкой. Если нужен обработанный результат, заверните внутренний handler во внешний обычный буфер и завершите внутренний через flush, либо явно преобразуйте уже захваченную строку.
Соседние операции: ob_start, ob_get_contents, ob_end_clean, ob_get_flush.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
Возвращает содержимое активного удаляемого буфера, затем закрывает его. При отсутствии подходящего буфера возвращает false.
<?php
declare(strict_types=1);
ob_start();
$html=ob_get_clean();
echo $html === '' ? "valid empty component\n" : "unexpected\n";
Ожидаемый результат
valid empty component
Отсутствие разметки может быть правильным результатом условного блока.
<?php
declare(strict_types=1);
ob_start();
echo "sku,quantity\nbook,2\n";
$csv=ob_get_clean();
echo substr_count($csv,"\n")." rows\n";
Ожидаемый результат
2 rows
Подходит только небольшому ограниченному экспорту. Для больших файлов выбирайте поток.
Рядом по задаче. ob_get_contents читает без закрытия; ob_end_flush отправляет ниже без возврата строки.
ob_get_contentsПолучить снимок исходного активного буфера, сохранив сам буфер и его содержимое.
ob_get_contents(): string|falseПолучить снимок исходного активного буфера, сохранив сам буфер и его содержимое.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. Работает только с верхним активным уровнем. |
| Возврат | Строка содержимого, включая пустую; false, если буфера нет. |
| Эффекты | Не очищает/не закрывает/не выполняет преобразование callback; не возвращает всю вложенную цепочку. |
- В буфере Заказ
- Снимок=Заказ
- После echo буфер=Заказ готов
- Первый снимок по-прежнему Заказ
Нужно посмотреть, что уже напечатал генератор отчёта, но дать ему продолжить работу. ob_get_contents возвращает снимок исходного содержимого активного буфера: не очищает, не закрывает, не вызывает обработчик ради преобразования. Если потом напечатать снимок в тот же буфер, текст добавится второй раз.
<?php
declare(strict_types=1);
ob_start();
echo 'Заказ';
$first = ob_get_contents();
echo ' готов';
$second = ob_get_contents();
ob_end_clean();
echo $first, ' | ', $second, "\n";
Ожидаемый результат
Заказ | Заказ готов
Сначала сохранены обе строки, потом буфер закрыт без передачи наружу. Снимок не заменяет живой буфер и не обновляется после следующего echo.
<?php
declare(strict_types=1);
$calls = 0;
ob_start(static function (string $buffer, int $phase) use (&$calls): string {
++$calls;
return strtoupper($buffer);
});
echo 'tea';
$snapshot = ob_get_contents();
$callsBeforeClean = $calls;
ob_end_clean();
echo $snapshot, '/', $callsBeforeClean, '/', $calls, "\n";
Ожидаемый результат
tea/0/1
Чтение не вызвало callback; очистка при закрытии вызвала, но отбросила его результат. Поэтому снимок содержит tea, а не TEA. Для получения обработанного результата используйте внешний буфер и flush внутреннего, либо явную функцию преобразования строки.
Без активного буфера возвращается false, пустой активный буфер даёт "". Функция не возвращает совокупность всех уровней; внешний буфер может хранить другие данные. Чтение длинного содержимого и удержание снимков увеличивает расход памяти; для размера используйте ob_get_length, не копию ради strlen. Это не механизм безопасного просмотра секрета: содержимое может включать токен, персональные данные или диагностический HTML.
Соседние операции: ob_get_clean, ob_get_length, ob_get_status.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
Возвращает текущие байты или false без активного буфера. Содержимое не очищается и уровень не закрывается.
<?php
declare(strict_types=1);
ob_start();
echo 'A';
$first=ob_get_contents();
echo 'B';
$all=ob_get_clean();
echo $first.'|'.$all."\n";
Ожидаемый результат
A|AB
Чтение снимка не остановило дальнейшее накопление.
<?php
declare(strict_types=1);
ob_start();
echo 'catalog';
$length=strlen(ob_get_contents());
ob_end_clean();
echo $length." bytes\n";
Ожидаемый результат
7 bytes
Буфер отброшен после измерения. Для большого потока лучше не получать полную копию ради подсчёта.
Рядом по задаче. ob_get_clean завершает сборку; ob_clean стирает текущие байты.
ob_get_levelУзнать число активных уровней, чтобы сохранить границу владения буферами.
ob_get_level(): intУзнать число активных уровней, чтобы сохранить границу владения буферами.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. |
| Возврат | Количество активных уровней; 0 означает отсутствие. Первый активный уровень даёт 1. |
| Сравнение | ob_get_status()["level"] нумерует с 0, поэтому для активного буфера на единицу меньше. |
| Ограничение | Количество не является идентификатором/владельцем буфера. |
- Внешний буфер: уровень1
- Внутренний: уровень2
- Закрываем только внутренний → снова1
- Внешнее содержимое сохранено
Функция библиотеки может быть вызвана, когда приложение уже открыло свой буфер. ob_get_level показывает число активных уровней; оно позволяет сохранить точку входа и проверить, что библиотека вернула управление с тем же стеком. Закрывать все буферы сайта ради очистки одного шаблона нельзя: это вмешается в сжатие, middleware и другой код.
<?php
declare(strict_types=1);
$base = ob_get_level();
ob_start(); $outer = ob_get_level();
ob_start(); $inner = ob_get_level();
$statusLevel = ob_get_status()['level'];
ob_end_clean(); ob_end_clean();
echo $outer - $base, '/', $inner - $base, "\n";
var_dump($statusLevel === $inner - 1, ob_get_level() === $base);
Ожидаемый результат
1/2
bool(true)
bool(true)
ob_get_level считает первый уровень как 1, status[level] — как 0. Разность с base не зависит от того, был ли внешний буфер до вызова.
<?php
declare(strict_types=1);
ob_start();
echo 'Внешняя часть; ';
$base = ob_get_level();
ob_start();
try {
echo 'Незавершённая карточка';
throw new RuntimeException('Учебный отказ');
} catch (RuntimeException) {
if (ob_get_level() !== $base + 1) { throw new LogicException('Шаблон нарушил договор буферов'); }
ob_end_clean();
echo 'Карточка недоступна';
}
$page = ob_get_clean();
echo $page, "\n";
Ожидаемый результат
Внешняя часть; Карточка недоступна
Мы точно знаем: шаблон не управляет буферами и наш уровень removable. Проверка обнаруживает нарушение числа уровней, но не доказывает identity буфера: чужой код мог закрыть один и создать другой. Не используйте это как sandbox для недоверенного PHP.
Соседние операции: ob_get_status, ob_list_handlers, ob_end_clean.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
Возвращает глубину стека буферизации. Уровень, созданный настройкой output_buffering, тоже может влиять на начальное значение.
<?php
declare(strict_types=1);
$base=ob_get_level();
ob_start();
ob_start();
$depth=ob_get_level()-$base;
ob_end_clean();
ob_end_clean();
echo $depth." own levels\n";
Ожидаемый результат
2 own levels
Сравнение с исходным уровнем работает и внутри чужого рендерера.
<?php
declare(strict_types=1);
$base=ob_get_level();
try { ob_start(); echo 'partial'; throw new RuntimeException(); }
catch (RuntimeException) {}
finally { while (ob_get_level()>$base) ob_end_clean(); }
echo "clean response\n";
Ожидаемый результат
clean response
Частичный вывод не просочился в ответ. Контракт запрещает неудаляемые или чужие буферы внутри блока.
Рядом по задаче. ob_get_status даёт сведения о буферах; ob_end_clean закрывает один уровень.
ob_cleanСтереть ещё накопленный фрагмент и продолжить печатать в тот же буфер.
ob_clean(): boolСтереть ещё накопленный фрагмент и продолжить печатать в тот же буфер.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. |
| Нужное разрешение | Активный буфер с CLEANABLE. |
| Возврат / ошибка | true при очистке; false и E_NOTICE при отсутствии/запрете. Исключение пользовательского handler может выйти наружу. |
| Эффект | Вызывает handler с CLEAN, отбрасывает его результат и содержимое; сам буфер остаётся. |
- В активном буфере Черновик
- ob_clean стирает его
- Буфер остаётся открытым
- Следующий echo записывает Готово
Предпросмотр ещё не отправлен, но его первая попытка больше не нужна. ob_clean очищает активный буфер и оставляет его открытым: следующая печать попадёт туда же. Это подходит для замены предварительного фрагмента в принадлежащем вам буфере, но не отменяет выполненные SQL-запросы, отправленные письма или данные, уже прошедшие во внешний уровень.
<?php
declare(strict_types=1);
ob_start();
echo 'Черновик';
$cleaned = ob_clean();
echo 'Готово';
$text = ob_get_clean();
var_dump($cleaned);
echo $text, "\n";
Ожидаемый результат
bool(true)
Готово
Буфер после ob_clean продолжает существовать. Мы забрали новую надпись отдельным ob_get_clean; старая не попала в результат.
<?php
declare(strict_types=1);
$notices = 0;
set_error_handler(static function (int $level, string $message) use (&$notices): bool {
if ($level === E_NOTICE) { ++$notices; return true; }
return false;
});
ob_start(null, 0, PHP_OUTPUT_HANDLER_REMOVABLE);
echo 'Содержимое';
$cleaned = ob_clean();
$saved = ob_get_contents();
$ended = ob_end_clean();
restore_error_handler();
var_dump($cleaned, $ended);
echo $saved, '/', $notices > 0 ? 'Был notice' : 'Без notice', "\n";
Ожидаемый результат
bool(false)
bool(true)
Содержимое/Был notice
CLEANABLE отсутствует, поэтому ob_clean отказал и не стёр данные. REMOVABLE присутствует: ob_end_clean затем закрывает и очищает буфер. Это два разных разрешения, а не общий флаг «можно удалить текст».
Успех — true, отсутствие активного буфера или CLEANABLE — false и E_NOTICE. При очистке вызывается handler с CLEAN (и START, если это первый вызов), но возвращённая им строка отбрасывается. Побочные действия callback всё же происходят; handler может бросить исключение. Не используйте callback для оплаты/другого необратимого действия, которое «должно случиться лишь при успешном показе».
Соседние операции: ob_end_clean, ob_get_clean, ob_get_contents.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
Очищает текущий очищаемый уровень и возвращает bool. Без подходящего активного буфера операция неуспешна и может выдать notice.
<?php
declare(strict_types=1);
ob_start();
echo 'draft';
ob_clean();
echo 'final';
$result=ob_get_clean();
echo $result."\n";
Ожидаемый результат
final
После очистки тот же уровень продолжает собирать данные.
<?php
declare(strict_types=1);
ob_start();
echo 'debug noise';
ob_clean();
$stillActive=ob_get_level()>0;
ob_end_clean();
echo $stillActive ? "buffer remained active\n" : "closed\n";
Ожидаемый результат
buffer remained active
Показано отличие очистки от завершения. Лучше удалить отладочный echo из самой функции.
Рядом по задаче. ob_end_clean очищает и закрывает, ob_get_clean возвращает накопленное.
ob_end_cleanОтменить ещё не переданный фрагмент и закрыть принадлежащий вам активный буфер.
ob_end_clean(): boolОтменить ещё не переданный фрагмент и закрыть принадлежащий вам активный буфер.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. |
| Нужное разрешение | Активный буфер с REMOVABLE; CLEANABLE для этой операции не требуется. |
| Возврат / ошибка | true при закрытии; false и E_NOTICE при отсутствии/запрете; callback может бросить исключение. |
| Эффект | CLEAN|FINAL, результат handler и исходный текст отбрасываются, активный уровень удаляется. |
- Внешняя страница содержит шапку
- Внутренний буфер содержит плохую карточку
- ob_end_clean удаляет только внутренний
- Внешняя страница продолжает собираться
Генератор карточки начал печатать HTML, затем обнаружил ошибку. Если этот фрагмент не нужно ни возвращать, ни показывать, ob_end_clean очищает и закрывает активный буфер. Вы снова работаете с тем внешним уровнем, который был до его открытия. Возвращаемое значение — bool, не удалённый HTML.
<?php
declare(strict_types=1);
ob_start();
echo '<p>Незавершённая карточка';
$discarded = ob_end_clean();
var_dump($discarded);
echo "Показать запасной блок\n";
Ожидаемый результат
bool(true)
Показать запасной блок
Внешнему потребителю не достался незакрытый тег. Это локальная подготовка текста; уже выполненные действия генератора буфер не отменяет.
<?php
declare(strict_types=1);
ob_start();
echo 'Шапка | ';
ob_start();
echo 'Ошибочная карточка';
ob_end_clean();
echo 'Запасная карточка';
$page = ob_get_clean();
echo $page, "\n";
Ожидаемый результат
Шапка | Запасная карточка
Закрыт только последний буфер. Внешняя шапка не была стёрта; последующий echo попал во внешний уровень. Поэтому while (ob_get_level()) без знания владельцев — неверный универсальный cleanup библиотеки.
Требуется REMOVABLE; отсутствие CLEANABLE не мешает этой операции. При нормальном вызове handler получает CLEAN|FINAL и дополнительно START, если ещё ни разу не вызывался; результат handler отбрасывается. false/E_NOTICE сообщают о невозможности закрыть буфер. Если callback кидает исключение, не считайте очистку успешно завершённой: разберите оставшееся состояние и политику ошибки. Невозможность удалить защищённый уровень не решается бесконечным циклом повторных ob_end_clean.
Соседние операции: ob_clean, ob_get_clean, ob_end_flush, ob_get_level.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
Закрывает удаляемый уровень без отправки накопленных байтов. Возвращает bool; отсутствие/неподходящие флаги уровня приводят к неуспеху.
<?php
declare(strict_types=1);
ob_start();
echo '<p>unfinished';
ob_end_clean();
echo "fallback\n";
Ожидаемый результат
fallback
Клиент получает только альтернативный ответ.
<?php
declare(strict_types=1);
ob_start(); echo 'page:';
ob_start(); echo 'private debug';
ob_end_clean();
echo 'ok';
$html=ob_get_clean();
echo $html."\n";
Ожидаемый результат
page:ok
Внешний буфер сохранён, внутренний отброшен.
Рядом по задаче. ob_get_clean нужен, если результат надо сохранить; ob_end_flush — если отправить ниже.
ob_flushПередать подготовленную порцию дальше и оставить активный буфер для следующей.
ob_flush(): boolПередать подготовленную порцию дальше и оставить активный буфер для следующей.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. |
| Разрешение | Активный буфер с FLUSHABLE. |
| Возврат / ошибка | true при flush; false и E_NOTICE при отсутствии/запрете; callback может бросить исключение. |
| Эффект | FLUSH, результат handler идёт следующему уровню, исходное содержимое очищается, уровень остаётся. |
| Ограничение | Не закрывает буфер и не гарантирует показ/доставку в браузере. |
- Внутренний буфер: Часть1
- ob_flush → внешнему буферу
- Внутренний теперь пуст, но открыт
- Следующий echo собирает Часть2
Генератор уже подготовил одну часть ответа, а затем будет печатать следующую в тот же буфер. ob_flush вызывает handler, передаёт его результат следующему внешнему уровню и опустошает текущий буфер. Он продолжает существовать. Слово «передаёт» не означает «человек уже увидел в браузере»: впереди могут быть ещё буферы PHP, сервера и клиента.
<?php
declare(strict_types=1);
ob_start();
ob_start();
echo 'Часть1; ';
$flushed = ob_flush();
$remaining = ob_get_length();
echo 'Часть2';
ob_end_flush();
$whole = ob_get_clean();
var_dump($flushed, $remaining);
echo $whole, "\n";
Ожидаемый результат
bool(true)
int(0)
Часть1; Часть2
После первого flush данные уже во внешнем буфере, а внутренний пуст. Следующий echo остаётся в том же внутреннем уровне. Внешний буфер позволил увидеть весь результат без HTTP.
<?php
declare(strict_types=1);
ob_start();
ob_start(static fn(string $buffer, int $phase): string => strtoupper($buffer));
echo 'tea'; ob_flush();
echo 'mint'; ob_end_flush();
echo ob_get_clean(), "\n";
Ожидаемый результат
TEAMINT
Во внешний буфер поступили TEA и MINT, а не исходные строки. Это показывает отличие flush от get_clean. Многословное преобразование может сломаться на границе порции: например, str_replace полного слова не найдёт слово, разрезанное между двумя echo.
Нужен активный буфер с FLUSHABLE. Успех true, отсутствие/запрет false+E_NOTICE. Handler получает FLUSH и при первом вызове также START; FINAL здесь нет. Обработчик может вернуть false/бросить исключение и отключиться: исходный текст тогда может пройти наружу. Не используйте flush как транзакционное подтверждение безопасной фильтрации.
Соседние операции: ob_end_flush, ob_get_flush, flush, ob_clean.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
Очищает содержимое текущего flushable-буфера после передачи ниже и сохраняет уровень. Возвращает bool; требует подходящего активного буфера.
<?php
declare(strict_types=1);
ob_start();
ob_start(); echo 'A'; ob_flush(); echo 'B'; ob_end_flush();
$result=ob_get_clean();
echo $result."\n";
Ожидаемый результат
AB
Обе части попали в внешний уровень, который ещё можно получить строкой.
<?php
declare(strict_types=1);
$base=ob_get_level();
ob_start(); ob_flush();
$active=ob_get_level()===$base+1;
ob_end_clean();
echo $active ? "still active\n" : "closed\n";
Ожидаемый результат
still active
Пустой flush показывает жизненный цикл без сетевых эффектов.
Рядом по задаче. ob_end_flush завершает уровень; flush просит нижележащую систему вывода отправить данные.
ob_end_flushПередать окончательный обработанный фрагмент дальше и закрыть активный уровень.
ob_end_flush(): boolПередать окончательный обработанный фрагмент дальше и закрыть активный уровень.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. |
| Разрешение | REMOVABLE; FLUSHABLE отдельно не требуется. |
| Возврат / ошибка | true при завершении, false+E_NOTICE при отсутствии/запрете; возможны исключения callback. |
| Эффект | FINAL (не FLUSH), результат handler передаётся дальше, буфер удаляется. |
- Внутренний буфер готов
- Handler получает FINAL
- Результат передан внешнему уровню
- Внутренний уровень удалён
Генератор закончил работу, и готовый фрагмент должен пройти дальше по цепочке. ob_end_flush вызывает handler, отправляет его результат и удаляет активный уровень. Теперь следующий echo попадёт в прежний внешний буфер либо в системный вывод PHP. Возвращается bool, а не отправленный текст.
<?php
declare(strict_types=1);
ob_start();
echo 'Шапка; ';
ob_start();
echo 'Карточка; ';
$ended = ob_end_flush();
echo 'Подвал';
$page = ob_get_clean();
var_dump($ended);
echo $page, "\n";
Ожидаемый результат
bool(true)
Шапка; Карточка; Подвал
Карточка добавлена к внешнему буферу, внутренний закрыт. Подвал уже печатается прямо во внешний уровень. Очистить внутреннюю карточку после этого можно только изменив внешнюю строку, не ob_clean старого уровня.
<?php
declare(strict_types=1);
ob_start();
ob_start(null, 0, PHP_OUTPUT_HANDLER_REMOVABLE);
echo 'Готово';
$ended = ob_end_flush();
$text = ob_get_clean();
var_dump($ended);
echo $text, "\n";
Ожидаемый результат
bool(true)
Готово
REMOVABLE разрешает завершающую операцию. FLUSHABLE управляет отдельным ob_flush, поэтому его отсутствие не запрещает ob_end_flush. Не путайте эти разрешения в проверке прав буфера.
При завершении handler получает FINAL, а не FLUSH; START добавляется, если handler вызван впервые. Нужен REMOVABLE; отсутствие/запрет даёт false+E_NOTICE. Handler может бросить исключение. После конца скрипта PHP сам последовательно завершает оставшиеся буферы изнутри наружу, даже если ручное удаление было запрещено; это не причина забывать о нормальном завершении принадлежащего функции уровня.
Соседние операции: ob_flush, ob_get_flush, ob_end_clean, ob_get_level.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
Завершает текущий удаляемый буфер с передачей его содержимого. Возвращает bool, а не строку HTML.
<?php
declare(strict_types=1);
ob_start(); echo 'page:';
ob_start(); echo 'badge'; ob_end_flush();
$html=ob_get_clean();
echo $html."\n";
Ожидаемый результат
page:badge
Внутренний компонент передан внешнему без отдельной переменной.
<?php
declare(strict_types=1);
$base=ob_get_level();
ob_start();
$ok=ob_end_flush();
echo $ok && ob_get_level()===$base ? "closed\n" : "failed\n";
Ожидаемый результат
closed
Пустой пример проверяет bool и восстановление глубины.
Рядом по задаче. ob_get_clean возвращает строку; ob_flush оставляет уровень активным.
flushПопросить PHP/SAPI передать системный вывод дальше, понимая границы этой просьбы.
flush(): voidПопросить PHP/SAPI передать системный вывод дальше, понимая границы этой просьбы.
| Параметр / результат | Контракт |
|---|---|
| Аргументы | Нет. |
| Возврат | void, при присваивании получится null; это не подтверждение доставки. |
| Слой | Не влияет на пользовательские ob_start/URL-Rewriter буферы. Они должны передать данные наружу отдельно. |
| SAPI / версия | Веб обычно headers→body; CLI — данные. FastCGI headers-only flush поддержан с PHP 8.4. |
| Ограничения | Сервер, прокси и браузер могут буферизовать дальше; ранние headers мешают некоторым handler. |
- Пользовательские буферы уже передали текст
- flush вызывает системный/SAPI сброс
- Дальше действуют собственные правила получателя
- Функция не возвращает статус показа
CLI-команда сообщает этапы импорта, а разработчик хочет передавать печать следующему слою вывода после каждого этапа. flush просит PHP/SAPI сбросить системный вывод. Он не исполняет обрабатывающий callback вашего ob_start и не опустошает пользовательский буфер. Сначала нужно понять, в каком именно слое лежит текст.
<?php
declare(strict_types=1);
ob_start();
echo 'Отчёт';
$before = ob_get_length();
$returned = flush();
$after = ob_get_length();
$text = ob_get_clean();
var_dump($returned, $before === $after);
echo $text, "\n";
Ожидаемый результат
NULL
bool(true)
Отчёт
Возврат void не сообщает об успешной доставке. Текст остался в пользовательском буфере; системному слою здесь ещё нечего отправлять из него. Не заменяйте ob_flush вызовом flush.
<?php
declare(strict_types=1);
foreach (['Файл проверен', 'Отчёт подготовлен'] as $stage) {
echo $stage, "\n";
flush();
}
Ожидаемый результат
Файл проверен
Отчёт подготовлен
Вместо настоящей обработки здесь только два сообщения, без sleep и сети. Проверен итоговый CLI-вывод. Момент отображения в терминале/браузере или прохождения прокси этим опытом не измеряется.
- echo → активный пользовательский буфер
- ob_flush/завершение → внешний пользовательский уровень
- После всех уровней → системный вывод PHP/SAPI
- flush → просьба backend отправить данные
- Веб-сервер, прокси, браузер решают свои буферизации
В CLI и embed сбрасываются данные; веб-SAPI обычно сначала отправляет заголовки, затем тело. С PHP 8.4 FastCGI поддерживает flush заголовков без тела. Нельзя распространять этот пункт на гарантированное поведение всех веб-серверов: действие реализует конкретный SAPI и цепочка инфраструктуры. Отправка заголовков заранее может помешать gzip/другому handler, которому ещё нужно добавить свои заголовки.
flush не управляет буфером браузера, рендерингом страницы или прокси/CDN. Дописывание произвольных 4096 пробелов — историческая попытка обойти порог отдельного клиента, а не переносимый контракт. Для прогресса веб-задачи сначала выбирают протокол и серверную конфигурацию (например, отдельный status endpoint или согласованный streaming), затем проверяют реальную цепочку. Как только заголовки/часть тела действительно ушли, прежний ответ уже нельзя полностью заменить новой ошибкой.
Соседние операции: ob_flush, ob_end_flush, ob_implicit_flush.
Начать с захвата текста в переменную · Сравнить clean, flush и закрытие.
Не возвращает значение и не очищает пользовательские ob-буферы автоматически. Поведение зависит от SAPI, веб-сервера, прокси и клиента.
<?php
declare(strict_types=1);
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
echo "event: progress\ndata: {\"done\":1}\n\n";
flush();
HTTP-фрагмент требует настройки буферизации всей цепочки и закрытой сессии. Не обещает мгновенную доставку.
<?php
declare(strict_types=1);
echo "batch 1 complete\n";
flush();
Ожидаемый результат
batch 1 complete
В CLI нет браузерного рендера; вывод в терминал или pipe имеет собственные условия буферизации.
Рядом по задаче. Для прогресса часто проще очередь и polling; ob_flush сначала передаёт пользовательский буфер ниже.