Продовжую публікувати статті з серії «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_start – ob_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~вірного+1
OB-END: 1

Природно, даний код більше нічого не виводить, буде порожня сторінка.

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

ob_start();
{
echo “hello world”;
session_start(); // тут все буде працювати коректно
$content = ob_get_contents();
}
ob_end_clean();
echo “

“.$content.”

“;

Перед вами невеликий 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

ob_clean

ob_flush

ob_end_clean

ob_end_flush

ob_get_clean

ob_get_flush

X
X
X
X X
X X
X X X
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©

У буфера висновку є кілер-фіча – всередині буфера можна стартувати ще один буфер, а всередині нового, ще й так далі (поки пам’яті вистачає):

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, тобто даний буфер відключений

Навіщо це все?

Хороше питання — навіщо потрібна робота з буфером висновку? Наведу кілька основних сценаріїв використання:

  • Стиснення переданих даних — з використанням вже згаданої ob_gzhandler()
  • Відкладений висновок, щоб уникнути помилки headers already sent» (про помилку докладно розказано в статті Сесія)
  • Робота з чужим кодом, який намагається самостійно щось виводити
  • Робота з HTML файлами: коли вам треба підключати текстовий файл (зазвичай мова про HTML), для подальшої роботи з його вмістом
  • Сценарій обробки помилок у файлах, що підключаються — стартуєте буфер, підключаєте файли, якщо щось пішло не так, то вміст буфера можна скинути, і замість неінформативних повідомлення про помилку, виводите не менш інформативне повідомлення, що сервер прихворів, і не може більше нічого.
    Приклад роботи з критичними помилками ви можете знайти в статті Обробка помилок, і там теж згадується буфер висновку, ох бачити все це ж-ж-ж неспроста

    Системний буфер висновку

    З користувальницьким буфером виведення розібралися, тепер давайте перейдемо до системного — це такий буфер висновку, який наповнюється по ходу виконання скрипта, і відправляється в браузер по закінченню виконання. Тобто даний буфер висновку є завжди, його не потрібно створювати, але ми можемо керувати ним.

    Ось так все просто і коротко, ну а тепер про нюанси управління системним буфером виведення…

    Royal flush

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

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

    echo “

    Please waiting for 10 seconds…

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

    Спасибі!

    “;

    Відразу кидається в очі виклик функції 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
    // …
    }

    Файли шаблонів:

    Дерзайте!

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

    • Хардкорно про буфер висновку у статті PHP output buffer in deep (переклад Буфер висновку в PHP)
    • Посилання на офіційне керівництво по настройка php.ini
    • Стаття Streaming and Output Buffering

    Висновок

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