You are here:Home»KB»Programming»PHP»Compressing PHP output with gzip or deflate
Friday, 12 May 2017 07:45

Compressing PHP output with gzip or deflate

Written by

In this article we will be discussing the compression of output from a php, not normal assets such as JS and css. Because the nature of PHP files is dynamic we have to use another method. 

After needing to compress my PHP output for QWcrm I started researching on the internet after thinking it would just be a case of a couple of lines of code. What I discovered is there are several ways to compress PHP output and each has their pros and cons. I also found that some people were incorrectly using the wrong or outdated methods. Below I will go through each method I found and then I will sum up my thoughts at the end so you can easily workoout what method you want to use.

One other thing to note is that everyone goes on about how they gzip their content, but in these modern times there are 2 compression methos GZIP and DEFLATE, deflate being the newer method and can offer better compression.

.htaccess - (for static assets and PHP sometimes)

I have discovered that on some server installs that the following code (in particular text/html) will compress PHP output. Obviously you have to have mod_deflate installed. Worth a try and add other file types as needed.

<IfModule mod_deflate.c>
    <IfModule mod_filter.c>
        AddOutputFilterByType DEFLATE text/html
    </IfModule>
</IfModule>

Below is the normal way of compressing with deflate (gzip replacement) and the legacy gzip. I have added these for references only as it keeps coming up but will not actually compress PHP output but as a combined effect can help reduce the download footprint of the webpage and its assets.

Example 1 - Enable compression via .htaccess (mod_deflate)

This seems to be the prefered .htaccess compression method now because it gets better ratios.

<IfModule mod_deflate.c>
	<IfModule mod_filter.c>
		AddOutputFilterByType DEFLATE text/html text/css text/javascript application/javascript application/x-javascript
	</IfModule>
</IfModule>

Example 2 - Enable compression via .htaccess (mod_gzip)
http://www.awesomeinfolab.com/enable-gzip-compression/

I have never come across this method before in htaccess and might not work on all apache installs. I am thing of PHP as Fast-CGI/'Apache Module'

<ifModule mod_gzip.c>
    mod_gzip_on Yes
    mod_gzip_dechunk Yes
    mod_gzip_item_include file .(html?|txt|css|js|php|pl)$
    mod_gzip_item_include handler ^cgi-script$
    mod_gzip_item_include mime ^text/.*
    mod_gzip_item_include mime ^application/x-javascript.*
    mod_gzip_item_exclude mime ^image/.*
    mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.*
</ifModule>

Links

ob_start("ob_gzhandler") - (Simple 1 liner)

This takes advantage of Php's buffered output and buffers all output of the script until the Php engine has completed, and then runs all the output through a special function that gzip compresses it before sending it on to the browser.

Just place this at the very top of all your PHP pages and it will send gzip-compressed output to the browsers with the correct headers.

ob_start("ob_gzhandler");

The function basically says start buffering PHP content and tag it to says the outputted content should be gzipped. The procedure should also send the correct headers so the browser knows it is compressed. I also think that unless the browser has told the server that it supports compression that the content will be returned uncompressed. All modern browsers send the 'I support compression' headers.

Some say once the script is finished it will flush the buffer and output the content automatically and that is why you can get away with 1 line, however this article from magicmonster tells you to add the flush command at the end to flush the cache.

ob_end_flush();

The method mentioned above is quick and easy, but the downfalls are that it only works on Apache with mod_gzip and according to the Php manual this is not the preferred method for gzipping.

You can increase the compression ratio by altering the php.ini or add the following to the script before ob_start("ob_gzhandler")

ini_set('zlib.output_compression_level', 4);

Another example

<?php if (substr_count($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip')) ob_start("ob_gzhandler"); else ob_start(); ?>

This example adds a check to see if the browser send the “Accept-Encoding: gzip” or “deflate” header before enabling the compression, however the buffer is still enabled even if compression is not required.

(From the Php manual at www.php.net)
note that using zlib.output_compression is preferred over ob_gzhandler().

Links

Full PHP buffer

Create a buffer, fill the buffer, compress the buffer data and send the content to the client

This is similiar to ob_start("ob_gzhandler") but there is a little more to it.

Example 1
http://www.webcodingtech.com/php/gzip-compression.php

This example utilises the PHP buffer correctely and manual takes the buffered content, sends the compress data headers relevant to the compression method, then it compresses the blob before returning it to the buffer. The buffers is then dispaly or returns to the browser as compress content. I dont think this method is dependant on having Apache mod_gzip installed for the auto detection of the compression headers from  the browser or to send the correct output headers.

This example has some issues an seems dated to use for compression. I think PHP has moved on. I would not use print as echo is quicker and I have no idea what print("\x1f\x8b\x08\x00\x00\x00\x00\x00"); does.

// Include this function on your pages
function print_gzipped_page() {

    global $HTTP_ACCEPT_ENCODING;
    if( headers_sent() ){
        $encoding = false;
    }elseif( strpos($HTTP_ACCEPT_ENCODING, 'x-gzip') !== false ){
        $encoding = 'x-gzip';
    }elseif( strpos($HTTP_ACCEPT_ENCODING,'gzip') !== false ){
        $encoding = 'gzip';
    }else{
        $encoding = false;
    }

    if( $encoding ){
        $contents = ob_get_contents();
        ob_end_clean();
        header('Content-Encoding: '.$encoding);
        print("\x1f\x8b\x08\x00\x00\x00\x00\x00");
        $size = strlen($contents);
        $contents = gzcompress($contents, 9);
        $contents = substr($contents, 0, $size);
        print($contents);
        exit();
    }else{
        ob_end_flush();
        exit();
    }
}

// At the beginning of each page call these two functions
ob_start();
ob_implicit_flush(0);

// Then do everything you want to do on the page
echo 'Hello World';

// Call this function to output everything as gzipped content.
print_gzipped_page();

Links

zlib.output_compression = on - (automatic and invisible)

This is the prefered method for gzipping over ob_gzhandler()

The zlib extension can be used to transparently compress PHP pages on-the-fly if the browser sends an “Accept-Encoding: gzip” or “deflate” header. Compression with zlib.output_compression seems to be disabled on most hosts by default, but can be enabled with a custom php.ini file:

The zlib extension is the undelying technology and the zlib.output_compression = On is a switch that enables transparent/invisible compression on all PHP content. The zlib extension is also the library that is used for other compressions such as ob_gzhandler. I would need to check which compressions operations were covered by it. 

This method is installed on most servers but left of by default. It will automatically send the output back to the client's browser in a compressed form if the 'allow compressed content' header is sent with the page request. If enabled in the php.ini then no further coding is required in any php (or other) script, or for that matter any assets.

Enable via php.ini

Add or alter the following line in the php.ini 

zlib.output_compression = On

Enable via a PHP script 

If zlib.output_compresssion is disabled but installed, you can enable either by editing the php.ini (as above) or you can add the following in to your PHP script. This will only enable compression for the script it is included in. and you must add this code before any headers or output is sent from the script. You should also note that having this method of compression will cause errors if further compression methods are implemented in your scripts.

if (extension_loaded("zlib") && (ini_get("output_handler") != "ob_gzhandler"))
{
    @ob_end_clean();
    @ini_set("zlib.output_compression", 1);
}

As you can see this simple script makes a few checks, that the libary is present and that ob_gzhandler() has not been set. This is perhaps optional depending on your script, the ob_end_clean() just makes sure any buffers are emptied, I am not sure this is needed either. The @ symbol just surpresses errors and again these could be removed

Links

gzencode / gzcompress / gzdeflate - (for a single blob)

There is a difference between these 3 functions even thought they all compress a blob you supply to them. However the concesous is to use gzencode() as it outputs in the correct format including the required checksums. This function is also supports both gzip and deflate compression algorithyms.

Joomla Example

I have included this to see how these guys do it as they have more experience than me. This process happens in 2 distinct sections, the first check to see if the gzip function is enabled whilst check to see if the server supports zlib compression and that ob_gzhandler has not already been set.

/**
 * Execute the application.
 *
 * @return  void
 *
 * @since   3.2
 *
 * From {Joomla}libraries/cms/application/cms.php
 */
 
 /**
 * @package     Joomla.Libraries
 * @subpackage  Application
 *
 * @copyright   Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
 
public function execute()
{
    // Perform application routines.
    $this->doExecute();

    // If we have an application document object, render it.
    if ($this->document instanceof JDocument)
    {
        // Render the application output.
        $this->render();
    }

    // If gzip compression is enabled in configuration and the server is compliant, compress the output.
    if ($this->get('gzip') && !ini_get('zlib.output_compression') && (ini_get('output_handler') != 'ob_gzhandler'))
    {
        $this->compress();

        // Trigger the onAfterCompress event.
        $this->triggerEvent('onAfterCompress');
    }

    // Send the application response.
    $this->respond();

    // Trigger the onAfterRespond event.
    $this->triggerEvent('onAfterRespond');
}

The second section actually does the compression and further checking, and yes there is some duplication of these checks. This fucntion supports the use of gzip or deflate compression algorithyms which is great for compatability. If you look at the code you can see that gzencode() is used and that if performs compression on a single variable (blob) rather than a page. gzencode() is able to use both algorithyms where as the other 2 functions this section covers cannot.

The code has been compress but because of the nature of the function you have to manually send the 'Content-Encoding' header so the browser knows the payload is compressed and how it has been compressed.

/**
 * Checks the accept encoding of the browser and compresses the data before
 * sending it to the client if possible.
 *
 * @return  void
 *
 * @since   11.3
 *
 * From {Joomla}libraries/joomla/application/web.php
 */
 
 /**
 * @package     Joomla.Platform
 * @subpackage  Application
 *
 * @copyright   Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE
 */

protected function compress()
{
    // Supported compression encodings.
    $supported = array(
        'x-gzip' => 'gz',
        'gzip' => 'gz',
        'deflate' => 'deflate'
    );

    // Get the supported encoding.
    $encodings = array_intersect($this->client->encodings, array_keys($supported));

    // If no supported encoding is detected do nothing and return.
    if (empty($encodings))
    {
        return;
    }

    // Verify that headers have not yet been sent, and that our connection is still alive.
    if ($this->checkHeadersSent() || !$this->checkConnectionAlive())
    {
        return;
    }

    // Iterate through the encodings and attempt to compress the data using any found supported encodings.
    foreach ($encodings as $encoding)
    {
        if (($supported[$encoding] == 'gz') || ($supported[$encoding] == 'deflate'))
        {
            // Verify that the server supports gzip compression before we attempt to gzip encode the data.
            // @codeCoverageIgnoreStart
            if (!extension_loaded('zlib') || ini_get('zlib.output_compression'))
            {
                continue;
            }
            // @codeCoverageIgnoreEnd

            // Attempt to gzip encode the data with an optimal level 4.
            $data = $this->getBody();
            $gzdata = gzencode($data, 4, ($supported[$encoding] == 'gz') ? FORCE_GZIP : FORCE_DEFLATE);

            // If there was a problem encoding the data just try the next encoding scheme.
            // @codeCoverageIgnoreStart
            if ($gzdata === false)
            {
                continue;
            }
            // @codeCoverageIgnoreEnd

            // Set the encoding headers.
            $this->setHeader('Content-Encoding', $encoding);

            // Header will be removed at 4.0
            if ($this->get('MetaVersion'))
            {
                $this->setHeader('X-Content-Encoded-By', 'Joomla');
            }

            // Replace the output with the encoded data.
            $this->setBody($gzdata);

            // Compression complete, let's break out of the loop.
            break;
        }
    }
}

So that is how Joomla does compression and it should be noted that this method is probably the most compatible way of doing compression.

Links

What I used in QWcrm

I wrote my own function bas heavily of the Joomla function to enable gzip. In QWcrm I was able to do this becaus emy page is stored in a single varible/blob that I can manipulate.

In the main index.php I have the following code

################################################
#         Page Compression                     #
################################################

// Compress page and send correct compression headers
if ($gzip == true && $VAR['theme'] !== 'print') {

    $BuildPage = compress_page_output($BuildPage);
    
}

This is the function code stored in include.php

###########################################
#  Compress page output and send headers  #
###########################################

/**
 * Checks the accept encoding of the browser and compresses the data before
 * sending it to the client if possible.
 *
 * @return  void
 *
 * @since   11.3
 *
 * From {Joomla}libraries/joomla/application/web.php
 */

/**
 * @package     Joomla.Platform
 * @subpackage  Application
 *
 * @copyright   Copyright (C) 2005 - 2016 Open Source Matters, Inc. All rights reserved.
 * @copyright   Copyright (C) 2017 - Jon Brown / Quantumwarp.com
 * @license     GNU General Public License version 2 or later; see LICENSE
 */

function compress_page_output($BuildPage)
{
    // Supported compression encodings.
    $supported = array(
        'x-gzip'    => 'gz',
        'gzip'      => 'gz',
        'deflate'   => 'deflate'
    );

    // Get the supported encoding.
    $encodings = array_intersect(browserSupportedCompressionEncodings(), array_keys($supported));

    // If no supported encoding is detected do nothing and return.
    if (empty($encodings))
    {
        return $BuildPage;
    }

    // Verify that headers have not yet been sent, and that our connection is still alive.
    if (headers_sent() || (connection_status() !== CONNECTION_NORMAL))
    {
        return $BuildPage;
    }

    // Iterate through the encodings and attempt to compress the data using any found supported encodings.
    foreach ($encodings as $encoding)
    {
        if (($supported[$encoding] == 'gz') || ($supported[$encoding] == 'deflate'))
        {
            // Verify that the server supports gzip compression before we attempt to gzip encode the data.            
            if (!extension_loaded('zlib') || ini_get('zlib.output_compression'))
            {
                continue;
            }           

            // Attempt to gzip encode the page with an optimal level 4.            
            $gzBuildPage = gzencode($BuildPage, 4, ($supported[$encoding] == 'gz') ? FORCE_GZIP : FORCE_DEFLATE);

            // If there was a problem encoding the data just try the next encoding scheme.            
            if ($gzBuildPage === false)
            {
                continue;
            }            

            // Set the encoding headers.
            header("Content-Encoding: $encoding");

            // Replace the output with the encoded data.            
            return $gzBuildPage;
            
        }
    }
}

####################################################################
#  Get the supported compression algorithms in the client browser  #
####################################################################

function browserSupportedCompressionEncodings() {
        
    return array_map('trim', (array) explode(',', $_SERVER['HTTP_ACCEPT_ENCODING']));

}

In future I will try and see if the mod_deflate option is working on other servers as this might be built into newer version of the module to treat PHP output as normal files and compress them.

Other Links

Read 7121 times Last modified on Saturday, 13 May 2017 12:35