PHP для начинающих. Буфер вывода

Продолжаю публиковать статьи из серии «PHP для начинающих», в этот раз речь пойдёт о буфере вывода.

Если захотите, то кратко о том как работает стандартный буфер вывода в PHP можно почитать на страницах официального руководства, но это не мой метод, я буду рассказывать на примерах, да ещё для закрепления материала задание выдам.

Для начала, даю установку — буферов вывода в PHP несколько, плюс ещё модули web-сервера могут выполнять буферизацию, да ещё и браузеры могут играться с выводом и не сразу отображать полученный результат (надо бы тут освежить память, а то за упоминание Netscape могут освежевать).

Вот теперь буду рассказывать о буферизации в PHP.

Пользовательский буфер вывода

Работа с буфером вывода начинается с функции ob_start() — у данной функции есть три опциональных параметра, но о них я расскажу чуть позже, а пока запоминаем — для включения буфера вывода используем функцию ob_start():

// включаем буфер
ob_start();

// этот, и весь последующий вывод, будет попадать в буфер вывода
echo "hello world";

Если же нам надо сохранить данные, или ещё как обработать вывод то нам потребуется функция ob_get_contents(). Сохранив данные, можно очистить и отключить буфер — для этого воспользуемся функцией ob_end_clean(), если свести всё перечисленное до кучи, то в результате получим следующий код:

// включаем буфер
ob_start();

// выводим информацию
echo "hello world";

// сохраняем всё что есть в буфере в переменную $content
$content = ob_get_contents();

// отключаем и очищаем буфер
ob_end_clean();

Практически все нужные нам функции имеют префикс «ob_», как не трудно догадаться это сокращение от «output buffer»

Функцию ob_get_contents() можно вызывать множество раз, на практике с таким не сталкивался:

// включаем буфер
ob_start();

// выводим информацию
echo "hello ";

// сохраняем всё что есть в буфере в переменную
// на данный момент там только `hello `
$a = ob_get_contents();

// выводим информацию
echo "world ";

// повторный вызов
// теперь буфер содержит `hello world `
$b = ob_get_contents();

Если вы стартанули буфер вывода, но по какой-то причине не закрыли его, то PHP это сделает за вас и в конце работы скрипта выполнит «сброс» буфера вывода в браузер пользователя

Если внутри блока ob_startob_end вы отправляете заголовок, то он не попадает в буфер, а сразу будет отправлен в браузер:

header("OB-START: 1");
ob_start();

echo "Never saw";
header("PHP-VERSION: ". PHP_VERSION);

ob_end_clean();
header("OB-END: 1");

В результате выполнения данного кода в http-пакете появятся следующие заголовки:

OB-START: 1
PHP-VERSION: 5.6.11-1+deb.sury.org~trusty+1
OB-END: 1

Естественно, данный код больше ничего не выводит, будет пустая страница.

Данное правило по отправке заголовков верно как для непосредственного вызова функции header(), так и для неявного при вызове session_start():

ob_start();
{
    echo "hello world";
    session_start();  // тут всё будет работать корректно
    $content = ob_get_contents();
}
ob_end_clean();
echo "<h1>".$content."</h1>";

Перед вами небольшой life-hack – в PHP вы можете использовать скобочки {} для выделения некой логики в блоки, при этом никакой функциональной нагрузки они не несут, а вот читаемость кода – повышают

Чуть-чуть прояснили ситуацию — теперь в копилке наших знаний есть информация о том, как включить буфер, как получить из него данные, и как выключить. Что ещё интересное можно с ним вытворять? Да с ним практически ничего толком и не сделать — его можно отправить (сбросить) в браузер (ключевое слово flush), очистить (clean), отключить (end). Ну и скомбинировать это всё до кучи тоже можно:

  • ob_clean() — читаем название функции как «очищаем буфер вывода»
  • ob_flush() — «отправляем буфер вывода»
  • ob_end_clean() — «буфер вывода отключаем и очищаем»
  • ob_end_flush() — «буфер вывода отключаем и отправляем в браузер»
  • ob_get_clean() — «получаем буфер вывода, очищаем и отключаем» — тут небольшой отступление от правила, эта функция должна именоваться как ob_get_end_clean(), но решили упростить, и выкинули end
  • ob_get_flush() — «отправляем буфер вывода, очищаем и отключаем», ob_get_end_flush()

Что можно из перечисленного делать с буфером вывода, определяется третьим опциональным параметром $flags при вызове функции ob_start(), используется крайне редко

Для простого запоминания вот вам наглядная табличка по данному семейству функций:

вернёт очистит отправит отключит
ob_get_contents X
ob_clean X
ob_flush X
ob_end_clean X X
ob_end_flush X X
ob_get_clean X X X
ob_get_flush X X X

Задание
Дополните приведенный ниже код вызовом одной функции, чтобы он корректно вывел «hello world»:

ob_start();
{
    echo "hello";
    $a = ob_get_contents();
    echo "world";
    $b = ob_get_contents();
}
ob_end_clean();

echo $a .' '. $b;

Обработчик буфера

Пора вернуться к функции ob_start() и её первому параметру — $output_callback — обработчик буфера вывода. В качестве обработчика буфера должна быть указана callback-функция, которая принимает содержимое буфера как входной параметр и должна вернуть строку после обработки:

/**
 * @param  string  $buffer Содержимое буфера
 * @param  integer $phase  Битовая маска из значений PHP_OUTPUT_HANDLER_*
 * @return string
 */
function ob_handler ($buffer, $phase) {
    return "Length of string '$buffer' is ". strlen($buffer);
}

ob_start('ob_handler');
echo "hello world";
ob_end_flush();

В данном примере функция обработчик вернёт строку «Length of string ‘hello world’ is 11».

Важный момент — с этими функциями нужно быть поосторожней, обработали строки и ладненько, но не пытайтесь вывести либо сохранить данные, не пытайтесь стартовать другой буфер вывода внутри функции, и да есть функции которые создают буфер вывода внутри себя, вот print_r() и highlight_file() тому пример

Из стандартных же обработчиков можете повстречать ob_gzhandler(), но лучше сжатие страничек оставлять на плечах web-сервера, и не вешать это на PHP.

Ещё момент, второй параметр $phase callback-функции может включать в себя флаги из семейства PHP_OUTPUT_HANDLER_*, но вам эта информация никогда не понадобится, я даже пример не смог придумать, зачем оно надо.

We need to go deeper©

Inception

У буфера вывода есть килер-фича – внутри буфера можно стартовать ещё один буфер, а внутри нового ещё и так далее (пока памяти хватает):

echo ob_get_level();              // 1
ob_start();
    echo ob_get_level();          // 2
    ob_start();
        echo ob_get_level();      // 3
        ob_start();
            echo ob_get_level();  // 4
        ob_end_flush();
    ob_end_flush();
ob_end_flush();

В данном примере функция ob_flush() и производные от неё, будут «выбрасывать» содержимое буфера на более высокий уровень.

Данный подход поможет в случае, когда вам нужно подключить сторонний код, а он вдруг может что-то взять и вывести — было бы разумно обернуть его вывод в буфер, даже если весь ваш код уже обёрнут в другой буфер.

Если вы не знаете точно на какой «глубине» находитесь – то воспользуйтесь функцией ob_get_level(), а чтобы «проснуться» вам пригодится следующий код:

while (ob_get_level()) {
    ob_end_clean();
}

Задание
Внесите изменения в код с вложенными вызовами ob_start() таким образом, чтобы цифры выводились в обратном порядке, для этого надо переставить три строчки кода.

Буфер «по умолчанию»

Если захотите создать обёртку над всем кодом, то для этого можно воспользоваться решением «из коробки» — буфер вывода «по умолчанию», за активацию оного отвечает директива output_buffering, её можно выставить как в On, так и указать размер буфера который нам потребуется (при достижении лимита, буфер будет отправлен в браузер пользователю). Данная директива должна быть проставлена либо в php.ini, либо в .htaccess (для апача), попытка выставить данное значение с использование ini_set() ни к чему не приведёт, т.к. PHP уже стартанул, и буфер вывода уже сконфигурирован согласно настроек:

php_value output_buffering 4096

Если при включенном буфере проверить уровень вложенности и вызвать функцию ob_get_level(), то получим 1:

if (ini_get('output_buffering')) {
    echo ob_get_level(); // 1
}

Т.е. если включить данный буфер, то можно будет избежать ошибок вида «headers already sent»? Да, пока буфера хватит, но никогда так не делайте, ведь понадеявшись на данный метод, вы фактически заложите бомбу замедленного действия, и неизвестно когда она «рванёт» и посыпит ошибками:

// сохраняем значение буфера
$buffer = ini_get('output_buffering');

// "выводим" текст на байт меньше буфера
echo str_pad('', $buffer - 1);

// отправляем заголовок
header("TAG-A: ". PHP_VERSION);

// ещё байт
echo " ";

// а второй заголовок уже не отправляется
// получите ошибку
header("TAG-B: ". PHP_VERSION);

Запомните, для CLI приложений директива output_buffering всегда 0, т.е. данный буфер отключен

Зачем это всё?

Хороший вопрос — зачем нужна работа с буфером вывода? Приведу несколько основных сценариев использования:

  1. Сжатие передаваемых данных — с использованием уже упомянутой ob_gzhandler()
  2. Отложенный вывод, чтобы избежать ошибки «headers already sent» (о данной ошибке подробно рассказано в статье Сессия)
  3. Работа с чужим кодом, который пытается самостоятельно что-то выводить
  4. Работа с HTML файлами: когда вам надо подключать текстовый файл (обычно речь о HTML), для дальнейшей работы с его содержимым

Сценарий обработки ошибок в подключаемых файлах — стартуете буфер, подключаете файлы, если что-то пошло не так, то содержимое буфера можно скинуть, и вместо неинформативного сообщения об ошибке, выводите не менее информативное сообщение, что сервер приболел, и не может больше ничего.
Пример работы с критическими ошибками вы можете найти в статье Обработка ошибок, и там тоже упоминается буфер вывода, ох видать всё это ж-ж-ж неспроста

Системный буфер вывода

С пользовательским буфером вывода разобрались, давайте теперь к системному перейдём — это такой буфер вывода, который наполняется по ходу выполнения скрипта, и отправляется в браузер по окончанию выполнения. Т.е. данный буфер вывода есть всегда, его не нужно создавать, но мы можем им управлять.

Вот так всё просто и кратко, ну а теперь о нюансах управления системным буфером вывода…

Royal flush

10 секунд вашего внимания…

Flush it!

А теперь из академического интереса давайте рассмотрим реализацию данного «экшена» – там совсем чуть-чуть кода, и небольшая горстка полезных знаний по PHP:

echo "<h3>Please waiting for 10 seconds...</h3>";

for ($i = 1; $i <= 10; $i++) {
    echo $i;
    flush();
    sleep(1);
}

echo "<h3>Thx!</h3>";

Сразу бросается в глаза вызов функции flush() — вызвав данную функцию вы даёте указание PHP «сбросить» системный буфер, т.е. отправить всё что там есть в браузер пользователю (но учтите, если у вас стартован пользовательский буфер, то для начала надо будет «сбросить» его, и уже потом вызвать flush()). Т.е. происходящее можно описать как:

цикл на 10 итераций:
    - выводим число, вывод попадает в системный буфер
    - отправляем буфер пользователю в браузер
    - ждём секунду

Ещё одна особенность, о которой нужно помнить — директива implicit_flush, отвечает за то, чтобы после каждого вывода автоматически вызывался flush(), поэтому следующая комбинация сработает аналогично предыдущему примеру:

php_flag implicit_flush on
for ($i = 1; $i <= 10; $i++) {
    echo $i;
    sleep(1);
}

Данную директиву можно изменять «на лету», для этого достаточно вызвать функцию ob_implicit_flush() (удивительное рядом, данную функцию стоило всё же назвать implicit_flush(), т.к. к пользовательскому буферу вывода она имеет опосредственное отношение — после вызова ob_flush() будет вызван flush()):

ob_implicit_flush();
for ($i = 1; $i <= 10; $i++) {
    echo $i;
    sleep(1);
}

Данные примеры работают только при выключенном output_buffering, иначе вам нужно будет его принудительно выключить и очистить в самом скрипте. Если же вы работаете в CLI, то знайте implicit_flush всегда включён, а output_buffering выключен, следовательно весь вывод будет без промедления попадать в консоль

Задание

Для выполнения данного задания вам потребуется освежить знания по подключению файлов

Для закрепления изученного материала, вам нужно реализовать простейший шаблонизатор, который оживит следующий код:

$header = template('header', ['title' => 'Hello World!']);
$content = template('content', ['content' => "Lorem ipsum...", 'meta' => 'Author info']);
$footer = template('footer', ['copy' => "Copyright ". date('Y')]);

// ...skipped logic

echo $header, $content, $footer;

/**
 * @param  string $template
 * @param  array  $vars
 * @return string
 */
function template($template, $vars) {
    // place your code here
    // ...
}

Файлы шаблонов:

<!-- header.phtml -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><?=$title?></title>
</head>
<body>
<!-- content.phtml -->
<div class="container">
    <p><?=$content?></p>
    <p><?=$meta?></p>
</div>
<!-- footer.phtml -->
<footer>
    <?=$copy?>
</footer>
</body>
</html>

Дерзайте!

Рекомендованная литература

В заключение

К теме буферизации вы ещё не раз вернётесь во время работы над PHP проектами, и поверьте, нужно знать и понимать на каком этапе «застрял» ваш вывод, чтобы не тратить время зря на диагностику работающего кода, а переходить непосредственно к причинам возникновения проблем.

12 thoughts on “PHP для начинающих. Буфер вывода”

  1. Спасибо за труд, но в статье совсем не описано в чем разница между пользовательским буфером и системным. После прочтения у меня осталось впечетление отсутствия полноты картини по изложеному материалу. Это все, конечно, можно найти в документации, но как по мне материал должен быть самодостаточным, чтобы не портить удовольствия при чтении, так буде пожалуй кошернее. :)

  2. Это все хорошо только для случаев когда надо “обернуть” чужой код, за все остальное.. вложенность.. внезапные “смывы” руки бы оборвать.. видел этот буфер только в одном проекте.. он был образцом говнокодинга. Не пойму зачем в любом нормальном фреймворке это может понадобится.

  3. Я буфер использую для дебага, так как существующие средства очень неудобны. var_dump криво работает при выводе в верстку страницы, а иногда надо вывести много данных.

    Пример

    	$log_dir='_debug_log';
        if (!file_exists($log_dir)) mkdir($log_dir,0777, true);
    	ob_start();
        
    	print_r($selected_items);
        
    	$o=ob_get_clean();
    	file_put_contents($log_dir . '/' .__FUNCTION__.'.txt',$o) ;
    
    1. xDebug попробуй
      Хотя если это дебаг на прод-сервере ( что не очень хорошо ), то имеет право на жизнь…

  4. Товарищи, как использовать буферизацию в последнем задании, ведь и без неё работает?

  5. Не хватает ответов к заданием, без них, стать полезна только на 50%

  6. Антон, спасибо!

    Есть идея как PHP Info засунуть в буффер и отработать через tidy? Не могу через интерефейс вывести нормально.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.