Ранее использовал простенькую конструкцию следующего вида: PHP: <?php // Использовать сжатие только в том случае, если оно поддерживается // браузером клиента. // if(strpos(getenv('HTTP_ACCEPT_ENCODING'), 'gzip') !== false) { function ki_compress($data) { // Максимальная степень сжатия. // return gzencode($data, 9); } ob_start('ki_compress'); header('Content-encoding: gzip'); } else { ob_start(); } ?> Основной недостаток данного подхода заключается в том, что заголовок Content-Encoding отсылается в момент старта и в случае отмены буферизации искажает восприятие потока клиентом. Необходимо решение, которое способно отследить момент непосредственной передачи сжатого содержимого. Если рассмотреть ситуацию применительно к ООП, то здесь всё решается довольно красиво: Предположим, что определена функция автозагрузки классов и существует некоторый класс Compressor. Допустим, что существует некоторый сценарий runtime.php, который подключается директивой php.ini к каждому вызванному на выполнение конечному сценарию: auto_prepend_file = 'runtime.php'; В этом сценарии определена точка запуска для сервиса void Compressor::start(void). Для отключения буфера и компрессора достаточно произвести в конечном сценарии вызов void Compressor::stop(bool $flush = false); Примечание: в случае, если необходимо отправить данные до завершения сценария, следует использовать вызов Compressor::stop(true) runtime.php: PHP: <?php Compressor::start(); # Фоновая служба сжатия вывода. Session::start(); # Фоновая служба защиты от перехвата сеанса. ?> Реализация класса Пусть существует скрытый синглтон компрессора, который может уничтожить только среда CLR в конце жизненного цикла страницы. В момент высвобождения ресурсов вызывается метод класса __destruct, который мы используем для отправки нашего заголовка Content-Encoding - ведь доподлинно известно, что дальше последней строчки кода конструкций echo быть не может. PHP: <?php // Copyright © Pran via PHP.ru, 2011 // Free for commercial and private usage. // class Compressor { private static $supported; # Сжатое содержимое поддерживается клиентом. private static $enabled; # Буферизация включена для очередной порции данных. protected function __construct() { if (self::$supported = (strpos(getenv('HTTP_ACCEPT_ENCODING'), 'gzip') !== false)) { ob_start(array($this, 'compress')); } else { ob_start(); } self::$enabled = true; } final protected function __clone() { trigger_error('Clonning is not allowed in class ' . get_called_class(), E_USER_WARNING); } final public static function compress($data) { return gzencode($data, 9); } final public static function start() { static $instance; if (!$instance) { $instance = new self(); } else trigger_error('The compressor has been started earlier', E_USER_NOTICE); } final protected static function flush() { // Если сжатие поддерживается клиентом, // то будет отправлен соответствующий заголовок. // if (self::$supported) { header('Content-Encoding: gzip'); } ob_end_flush(); } final public static function stop($flush = false) { // Предотвращает сбой при многократном вызове метода. // if (self::$enabled) { if(!$flush) { ob_end_clean(); } else { self::flush(); } self::$enabled = false; } } final public function __destruct() { if (self::$enabled) self::flush(); } } ?> Применение в проекте Автоматически, ко всем сценариям. Сервис загрузки файлов попадает под исключение - при отдаче из базы (особенно при размере 100-200 МБ и более) компрессор отключается методом Compressor::stop, иначе либо выделенная оперативная память закончится, либо размер секции на выходе не будет соответствовать заголовку Content-Length. Проверка (test.php): PHP: <?php // Защита сценария сработает только если start был вызван после stop, причём повторно. // Два и более вызовов подряд не имеют принципиального значения. // Compressor::start(); echo 'section one - compressed and/or buffered'; // Очистить первую секцию. // Эффект будет только в том случае, если stop был вызван после start. // Два и более вызовов подряд не имеют принципиального значения. // Compressor::stop(); # true - отправить первую секцию клиенту. echo 'section two - plain text'; ?>
Обнаружил неувязку - если возникает какая-либо ошибка, то метод __destruct не вызывается в конце выполнения и, в итоге, сжатое содержимое (в том числе и описание сбоя) отправляется без заголовка Content-Encoding.
Модификация, использующая register_shutdown_function, успешно работает под IIS (PHP через FastCGI). Буду признателен за проведение тестов под Apache. PHP: <?php // Copyright © Pran via PHP.ru, 2011 // Free for commercial and private usage. // class Compressor { private static $supported; # Сжатое содержимое поддерживается клиентом. private static $enabled; # Буферизация включена для очередной порции данных. protected function __construct() { if (self::$supported = (strpos(getenv('HTTP_ACCEPT_ENCODING'), 'gzip') !== false)) { ob_start(array(__CLASS__, 'compress')); // Функция финализации будет поставлена в очередь выполнения только в том случае, // если клиент поддерживает сжатое содержимое. // register_shutdown_function(array(__CLASS__, 'finalize')); } else { ob_start(); } self::$enabled = true; } final protected function __clone() { trigger_error('Clonning is not allowed in class ' . get_called_class(), E_USER_WARNING); } final public static function compress($data) { return gzencode($data, 9); } final public static function start() { static $instance; if (!$instance) { $instance = new self(); } else trigger_error('The compressor has been started earlier', E_USER_NOTICE); } final protected static function flush() { // Если сжатие поддерживается клиентом, // то будет отправлен соответствующий заголовок. // if (self::$supported) { header('Content-Encoding: gzip'); } ob_end_flush(); } final public static function stop($flush = false) { // Предотвращает сбой при многократном вызове метода. // if (self::$enabled) { if(!$flush) { ob_end_clean(); } else { self::flush(); } self::$enabled = false; } } public static function finalize() { self::stop(true); } } ?>
Замена классу Compressor, которую следует расположить в начале сценария (в моей практике - auto_prepend_file): PHP: <?php if (strpos(getenv('HTTP_ACCEPT_ENCODING'), 'gzip') !== false) { ob_start(function($data) { if ($data) { header('Content-Encoding: gzip'); return gzencode($data, 9); } }); } else { ob_start(); } # При необходимости останавливаем через вызов ob_end_clean ?> ob_end_clean загадочным образом вызывает обработчик ob_start, передавая ему пустое содержимое (выявлено в PHP 5.3), поэтому введено условие проверки $data. В процессе работы не следует забывать про уровни вложения ob_start: PHP: <?php ob_start(function(...)) # Блок из примера выше, сжатие на уровне 2 ob_start(); # Уровень 3, несжатый «карман» require_once ... $tmp = ob_get_clean(); # Завершение уровня 3, вывод echo в переменную ob_start(); # Уровень 3, несжатый «карман» ob_end_clean(); # Завершение уровня 3 // ob_end_clean(); # Отмена сжатия ?> Отменить сжатие можно только в том случае, если ob_end_clean вызван для уровня 2