Merge files in order to minimize HTTP requests

cereal 1 Tallied Votes 331 Views Share

Hi!

Minimize HTTP requests. One of the suggestion I always read is to combine files, so I made a little Class that merges files on the fly, the supported formats are CSS and Javascript. With little efforts, I think, it can work fine also with JSON and CSV.

Usage is simple:

  • create a file that will serve data and in which the class is included
  • set config variables: method, path and type
  • use a link in this format: /merge/?css&first.css&second.css&third.css

Basically:

<?php

$list = $_SERVER['QUERY_STRING'];
$config['path']['css']  = '/css/';
$config['path']['js']   = '/js/';

include 'merger.class.php';

try
{
    $z = new Merger($list,$config);
    $exp = 60*60*24;
    header("Cache-Control: private");
    header("Cache-Control: maxage=".$exp);
    header('Expires: ' . gmdate('D, d M Y H:i:s', time()+$exp) . ' GMT');
    header('Accept-Ranges: bytes');
    header('Content-Type:' . $z->output_mime());
    header('Content-Length: '. $z->output_sizes());
    echo $z->output_data();
}

catch(Exception $e)
{

    echo $e->getMessage();
    exit();

}

?>

And in the file:

<link rel="stylesheet" href="/merge/?css&a.css&b.css&d.css" type="text/css" media="all" charset="utf-8" />
<script type="text/javascript" src="/merge/?js&jquery-1.7.2.min.js&include.js"></script>

Method and Path are setted in the script and passed as array in the second argument:

$config['method'] = 'get';
$config['path']['css'] = '/css/';

$z = new Merger($list, $config);

get is the default value for method, so it can be omitted, the alternative is cli. Type is setted in the link as first argument:

/merge/?css

this will select the path and the mimes to match, after the first argument append all the files that you want to merge:

/merge/?css&alfa.css&beta.css&gamma.css&...
/merge/?js&javascript_framework.js&calc.js&form.js&...

the string can be served as encoded through urlencode/rawurlencode or not. The class will check for duplicates names, and will remove them so you won't merge same data. At the moment there no check about contents, but this is simple to achieve, it's enough to use md5_file().

As I wrote, methods are: cli and get. The script can ran from CLI, for example to merge files and save them, just add file_put_contents() in the try{} block and change $list as below:

$list = $argv;

# ...

file_put_contents(md5($z->output()).'.'.$z->type,$z->output_data(),LOCK_EX);

Instead of $argv for CLI input or $_SERVER['QUERY_STRING'] for GET, you can pass a simple array:

$list = array('css','alfa.css','beta.css','gamma.css');

but in this case you need a second serving script for js or a switch statement. I'm attaching also a complete example, with css and js files, just place them in the root.

H OOPs

Hope it is useful and easy to understand. This is also my first snippet here, hope I didn't breach rules :D

Requirements

PHP >= 4.3.0, PHP 5

Few Notes

Finfo and linux command file output text/plain on ubuntu and, in some cases, these files can also be detected as text/x-c or text/x-c++. In order to work, just leave the correct mime type at the end of each array.

Array_filter() is used to prevent errors, given a&b&, the last "&" will generate a key with an empty value:

$s = 'a&b&';
print_r(explode('&',$s));

# outputs
Array
(
   [0] => a
   [1] => b
   [2] => 
)
<?php

class Merger
{
    private $config;
    private $data;
    private $ext;
    private $fbase;
    private $fpath;
    private $file;
    private $files;
    private $mime;
    private $result;

    public function __construct($files,$config)
    {
        $this->jump = 0;
        
        if(is_array($files)) { $this->jump = 1; }
        if(empty($config['method'])) { $config['method'] = 'get'; } # default method
        if($config['method'] == 'cli') { array_shift($files); }
        if($config['method'] == 'get' && $this->jump == 0) { $files = explode('&',urldecode($files)); }
        
        $this->type     = strtolower(array_shift($files));
        $data['files']  = $this->check_uniqueness($files);
        $data['mimes']  = array(
            'css'   => array('text/x-c','text/plain','text/css'),
            'js'    => array('text/x-c++','text/plain','application/x-javascript')
        );
        
        $this->files = array();
        $this->count = count($data['files']);
        $this->path  = $_SERVER['DOCUMENT_ROOT'] . $config['path'][$this->type];
        $this->mimes = $data['mimes'];
                
        for($i = 0; $i < $this->count; $i++)
        {
            $ext    = $this->check_ext($data['files'][$i]);
            $fbase  = $this->check_basename($data['files'][$i]);
            $fpath  = $this->path . $fbase;
            
            if(!is_dir($this->path) || !file_exists($this->path))
            {
                throw new Exception('directory '.$fbase.' is not available!');
            }
        
            if(!file_exists($fpath))
            {
                throw new Exception('file '.$fbase.' is missing!');
            }
            
            if(!in_array($this->check_mime($fpath),array_values($this->mimes[$this->type])))
            {
                throw new Exception('file '.$fbase.' is not allowed!');
            }
            
            if(!in_array($ext,array_keys($this->mimes)) || $ext != $this->type)
            {
                throw new Exception('file '.$fbase.' is not allowed!');
            }
            
            $this->files[]  = $fpath;
        }
        
    }
    
    
    
    #
    #
    # serve data
    #
    
    public function output_data()
    {
        $result = array();
        for($i = 0; $i < $this->count; $i++)
        {
            $result[] = file_get_contents($this->files[$i]);
        }
        return implode('',$result);
    }
    
    public function output_sizes()
    {
        $result = array();
        for($i = 0; $i < $this->count; $i++)
        {
            $result[] = filesize($this->files[$i]);
        }
        return array_sum($result);
    }
    
    public function output_mime()
    {
        return end($this->mimes[$this->type]);
    }
    
    # for testing purposes: check filtered file list
    public function output()
    {
        return implode(' ',$this->files);
    }
    
    
    
    
    #
    #
    # check data
    #
    
    private function check_ext($file)
    {
        return strtolower(pathinfo(parse_url($file,PHP_URL_PATH),PATHINFO_EXTENSION));
    }
    
    private function check_basename($file)
    {
        return pathinfo(parse_url($file,PHP_URL_PATH),PATHINFO_BASENAME);
    }
    
    private function check_uniqueness($files)
    {
        return array_merge(array(),array_filter(array_unique($files)));
    }
    
    private function check_mime($file)
    {
        if(method_exists('finfo','file'))
        {
            $finfo = new finfo(FILEINFO_MIME_TYPE);
            return $finfo->file($file);
        }
        else
        {
            return mime_content_type($file);
        }
        
    }
}
?>
somedude3488 228 Nearly a Posting Virtuoso

Looks good. The only things I would add is caching and css/js minification for faster loading times.

I implemented this into my framework a few years ago and it has worked quite well. Makes debugging a little harder but worth it.

cereal 1,524 Nearly a Senior Poster Featured Poster

Thank you for suggestion, I will work on that!

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.