Methods
public __construct()
private _genHtml( $sType, $aFile) ... all param(s) required
private _getStats( $sOrig, $sCrunched, $sCommment) ... all param(s) required
private _log( $s) ... all param(s) required
public addCss( $sFile, $sMedia = 'all', $sTitle = '') ... 1 of 3 param(s) required
public addJs( $sFile, $defer = 'execution') ... 1 of 2 param(s) required
public addMedia( $sType, $aFiledata) ... all param(s) required
public compressCss( $s) ... all param(s) required
public compressJs( $s) ... all param(s) required
public getCacheBase()
public getCacheDir()
public getDebug()
public getMerging()
public getMinCacheTime()
public getRewriteCache()
public rewriteCss( $s, $sFile) ... all param(s) required
public setCacheBase( $str) ... all param(s) required
public setCacheDir( $str) ... all param(s) required
public setDebug( $bool) ... all param(s) required
public setDebugDefaults()
public setDefaults()
public setMerging( $bool) ... all param(s) required
public setMinCacheTime( $int) ... all param(s) required
public setRewriteCache( $bool) ... all param(s) required
public writeMedia( $sType, $sMyGroup = false) ... 1 of 2 param(s) required
Source
<?php
/**
*
* AXELS CLASS TO MERGE JS AND CSS DATA
* V0.10
*
* License: GPL 3.0 - http://www.gnu.org/licenses/gpl-3.0.html
* THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE
* LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
* OTHER PARTIES PROVIDE THE PROGRAM ?AS IS? WITHOUT WARRANTY OF ANY KIND,
* EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
* ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.
* SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY
* SERVICING, REPAIR OR CORRECTION.
*
* --------------------------------------------------------------------------------
* project home:
* http://www.axel-hahn.de/php_staticfiles.php
* --------------------------------------------------------------------------------
*
* How it works:
*
* This script collects all static files per type and saves merged code into a
* single file (grouped by defer type for javacscript and media for css).
*
* It uses caching for the merged files.
*
* In comparison to other combining scripts this one
* - detects relative urls in css files and rewrites them.
* - in develop environment you can diasable the merging and debug with
* original files
* - caching uses a dirty time before cache will be compared with included files
*
* --------------------------------------------------------------------------------
*
* --- typical usage:
* see readme.txt
*
* --- KNOWN LIMITS
* - static files must be added with absolute path (this class shows you an error)
* - don't forget to add gzip compression for .js and .css in your webserver or in .htaccess
*
* --- TODO/ IDEAS
* - html 5: static in js
* - merge all css of all media with @media [name]{} sections http://de.selfhtml.org/css/formate/einbinden.htm#media
* - cleanup in cache directory
* - setter functions have no prameter check
* - setter for compression needed?
*
* --- history:
* 2011-08-06 first version
* 2011-08-14 rewritten as php class
* 2011-08-19 first public version
* 2011-08-27 comments added
* 2012-02-06 little code rewrite: private functions start with "_"; sortorder of methods, ...
* 2012-06-30 css fixed for cachefile in debug mode; css: handle media queries
* 2012-07-05 remove css comments in compress function; css default media is "all" now (it was "screen" before)
* 2012-10-09 write method got a 2nd paramter for filtering
* 2018-07-23 update compressors for css and js
* --------------------------------------------------------------------------------
*/
class Staticfiles {
/**
* configuration array
* @var array
*/
private $_aStatic = array(
'js' => array(
'files' => array(), // will be filled with addMedia()
'tplHtml' => '<script type="text/javascript" SRC DEFER></script>',
'fRewrite' => '',
'fCompress' => 'compressJs',
),
'css' => array(
'files' => array(), // will be filled with addMedia()
'tplHtml' => '<link rel="stylesheet" type="text/css" HREF MEDIA TITLE />',
'fRewrite' => 'rewriteCss',
'fCompress' => 'compressCss',
)
);
/**
* flag: show debug output? If it is true then you see the merged files,
* replacements and statistic data in the html header section (as comment)
* @var boolean
*/
var $bDebug = false; // debug enables additional infos
/**
* flag: merge files into a cachefile? If it is false then the source
* files will be used. This is helpful in the development environment to
* debug code with original files.
* @var boolean
*/
var $bMerging = false; // use compressed files; false links to original files
/**
* flag: overwrite cachefile on each request?
* You can use it in your development environment. It overrides the setting
* of $this->iMinCacheTime and regenerates merged cachefiles on each request.
* debug code with original files.
* @var boolean
*/
var $bRewriteOnEachRequest = false;
/**
* minimum time to cache js and css cachefile on server - even if one of the
* source files have been changed.
* @var integer
*/
var $iMinCacheTime = 3600;
/**
* name for basepath behind webroot
* @var string
*/
var $sCacheBase = '/~cache/staticfiles/';
/**
* absolute path of cache directory; it will be set in the method
* setDefaults to [document root]/$this->sCacheBase/
* @var string
*/
var $sCacheDir = false;
// ----------------------------------------------------------------------
// INIT
// ----------------------------------------------------------------------
/**
* init function; it sets defaults.
* @return bool (dummy)
*/
public function __construct() {
return $this->setDefaults();
}
// ----------------------------------------------------------------------
// private functions
// ----------------------------------------------------------------------
/**
* generate html code to link the cachefile (or original files if cache is disabled)
* @param string $sType type; one of css|js
* @param array $aFile array with filedata (keys are attributes)
* @return string html code
*/
private function _genHtml($sType, $aFile) {
$s = $this->_aStatic[$sType]["tplHtml"];
// TODO: autodetect with pregmatch
$aReplace = array("SRC", "DEFER", "HREF", "MEDIA", "TITLE");
foreach ($aReplace as $sFrom) {
$sKey = strtolower($sFrom);
$sTo = "";
if ($aFile[$sKey])
$sTo = " $sKey=\"" . $aFile[$sKey] . "\" ";
$s = str_replace($sFrom, $sTo, $s);
}
$s = str_replace('defer="execution"', ' ', $s);
$s = str_replace(' ', ' ', $s);
$s = str_replace(' ', ' ', $s);
$s = str_replace(' >', '>', $s);
return $s;
}
/**
* show some statistics info about code compression an gzip
* @param type $sOrig
* @param type $sCrunched
* @param type $sCommment
* @return type
*/
private function _getStats($sOrig, $sCrunched, $sCommment) {
if (!$this->bDebug) {
return '';
}
$iOrigSize = strlen($sOrig);
$iNewSize = strlen($sCrunched);
$igzSize1 = strlen(gzencode($sOrig));
$igzSize2 = strlen(gzencode($sCrunched));
return $sCommment . "
statistics:
... orig size : $iOrigSize bytes
... compression function: $iNewSize bytes (" . (100 - round($iNewSize / $iOrigSize * 100)) . "% less)
... gzip
...... orig : $igzSize1 bytes (" . (100 - round($igzSize1 / $iOrigSize * 100)) . "% less)
...... compressed : $igzSize2 bytes (" . (100 - round($igzSize2 / $iNewSize * 100)) . "% less than crunched; " . (100 - round($igzSize2 / $iOrigSize * 100)) . "% less than original)
";
}
/**
* write logging information (only if debug is enabled)
* @param type $s
* @return type
*/
private function _log($s) {
if (!$this->bDebug) {
return true;
}
$s = ($s === "===" ? "================================================================================" : $s);
$s = ($s === "---" ? "................................................................................" : $s);
echo "<!-- DEBUG: " . $s . " -->\n";
}
// ----------------------------------------------------------------------
// add media files
// ----------------------------------------------------------------------
/**
* add a css file to this object
* @param string $sFile path and filename of css file like in href attribute
* @param string $sMedia media type
* @param title $sTitle title (will be ignored so far)
* @return bool (dummy)
*/
public function addCss($sFile, $sMedia = 'all', $sTitle = '') {
return $this->addMedia("css", array(
'file' => $sFile,
'href' => $sFile,
'media' => $sMedia,
'title' => $sTitle,
));
}
/**
* add a js file to the js array
* @param string $sFile file to load
* @param string $sDefer
* @return type
*/
public function addJs($sFile, $defer = "execution") {
return $this->addMedia("js", array(
'file' => $sFile,
'src' => $sFile,
'defer' => $defer,
)
);
}
/**
* add media file to array; it is called by addJs() and addCss()
* @param string $sType one of css|js
* @param array $aFiledata; Keys: file: filename; other: attributes
* @return type
*/
public function addMedia($sType, $aFiledata) {
$sGroup = false;
if ($sType == "css"){
$sGroup = $aFiledata["media"];
}
if ($sType == "js"){
$sGroup = $aFiledata["defer"];
}
if (!$sGroup){
return false;
}
return $this->_aStatic[$sType]["files"][$sGroup][] = $aFiledata;
}
/**
* compress css code: remove spaces, tabs, line breaks
* @param string $s css data
* @return string rewritten css data
*/
public function compressCss($s) {
// remove unneded spaces, tabs, new lines
// $s = str_replace(array("\r\n", "\r", "\n", "\t", ' ', ' ',), ' ', $s);
/* remove unnecessary spaces */
foreach (explode(" ", "{ } ; , : ( )") as $char) {
$s = str_replace(' ' . $char, $char, $s);
$s = str_replace(' ' . $char, $char, $s);
$s = str_replace($char . ' ', $char, $s);
$s = str_replace($char . ' ', $char, $s);
}
// remove comments
$s = preg_replace('#\/\*.*\*\/#U', '', $s);
return $s;
}
/**
* compress js code
* @param string $s js code
* @return string with new js code
*/
public function compressJs($s) {
// return $s;
// remove comments starting with //
// remark: the tricky things are:
// - urls are not a comment .. see https://example.com/...
$s = preg_replace("#[^:]\/\/\ .*#", '', $s);
// remove tab, new line, multi spaces ...
$s = str_replace(array("\t", ' ', ' ',), ' ', $s);
// remark:
// if removing "//" lines is not safe then I do not remove line breaks
// $s = str_replace(array("\r\n", "\r", "\n", "\t", ' ', ' ',), ' ', $s);
$s = preg_replace('#\/\*.*\*\/#U', '', $s);
/* remove unnecessary spaces */
foreach (explode(" ", "{ } ; , : ( )") as $char) {
$s = str_replace(' ' . $char, $char, $s);
$s = str_replace(' ' . $char, $char, $s);
$s = str_replace($char . ' ', $char, $s);
$s = str_replace($char . ' ', $char, $s);
}
return $s;
}
// ----------------------------------------------------------------------
// getter
// ----------------------------------------------------------------------
/**
* get current basepath of cache
* @return boolean
*/
public function getCacheBase() {
return $this->sCacheBase;
}
/**
* get full path of current cache directory
* @return boolean
*/
public function getCacheDir() {
return $this->sCacheDir;
}
/**
* get current debug flag
* @return boolean
*/
public function getDebug() {
return $this->bDebug;
}
/**
* get current merge flag
* @return boolean
*/
public function getMerging() {
return $this->bMerging;
}
/**
* get current caching time on server
* @return integer
*/
public function getMinCacheTime() {
return $this->iMinCacheTime;
}
/**
* get current flag to rewrite the cache
* @return boolean
*/
public function getRewriteCache() {
return $this->bRewriteOnEachRequest;
}
// ----------------------------------------------------------------------
//
// ----------------------------------------------------------------------
/**
* rewrite urls in css data; this function finds relative
* pathes and rewrites them to absolute pathes
* @param string $s css content
* @param string $sFile filename of included css (to detect the "current" path)
* @return string the reswritten css code
*/
public function rewriteCss($s, $sFile) {
$aFrom = array();
$aTo = array();
$aHits = array();
preg_match_all("/url\((.*)\)/U", $s, $aHits, PREG_OFFSET_CAPTURE);
foreach ($aHits[1] as $sUrl) {
if (!strstr($sUrl[0], "data:image")) {
$sTo = $sUrl[0];
$sWrap = $sTo[0];
if ($sWrap == "'" or $sWrap == '"') {
$sTo = str_replace($sWrap, "", $sTo);
} else
$sWrap = "";
// if there is no absolute path then try to rewrite
if ($sTo[0] != "/") {
$sTo = str_replace($sWrap, "", $sTo);
// do replacements
$sDir = dirname($sFile);
$sTo = str_replace("../", "", $sTo, $count);
for ($i = 0; $i < $count; $i++) {
$sDir = str_replace(basename($sDir), "", $sDir);
}
$sTo = $sDir . "/" . $sTo;
$sTo = str_replace("//", "/", $sTo);
$sTo = str_replace("./", "", $sTo);
$sTo = str_replace("//", "/", $sTo);
// add wrapping characters
$sTo = $sWrap . $sTo . $sWrap;
$aFrom[] = $sUrl[0];
$aTo[] = $sTo;
$this->_log("replacing " . $sUrl[0] . " to: " . $sTo . " ");
}
}
}
if (count($aFrom)){
$s = str_replace($aFrom, $aTo, $s);
}
return $s;
}
// ----------------------------------------------------------------------
// setter
// ----------------------------------------------------------------------
/**
* set another basepath (behind webroot)
* @param string new basepath
* @return boolean
*/
public function setCacheBase($str) {
$this->sCacheDir = $_SERVER["DOCUMENT_ROOT"] . $str;
return $this->sCacheBase = $str;
}
/**
* set another cache directory.
* @param string new basepath (below webroot)
* @return boolean
*/
public function setCacheDir($str) {
return $this->sCacheDir = $_SERVER["DOCUMENT_ROOT"] . $str;
}
/**
* set debug flag (for development environment)
* @param boolean set true to enable debug output
* @return boolean
*/
public function setDebug($bool) {
return $this->bDebug = (bool) $bool;
}
/**
* set debug default settings
* @return bool (always true)
*/
public function setDebugDefaults() {
$this->iMinCacheTime = 0;
$this->bDebug = true;
$this->bMerging = true; // set to false that you can debug with original files
$this->bRewriteOnEachRequest = true;
return true;
}
/**
* set all vars to initial values
* @return bool (always true)
*/
public function setDefaults() {
// $this->sCacheBase = '/~cache/'; // added to url
$this->iMinCacheTime = 3600; // force using the cachefile and don't update it
$this->bDebug = false; // debug enables additional infos
$this->bMerging = true; // use compressed files
$this->sCacheDir = $_SERVER["DOCUMENT_ROOT"] . $this->sCacheBase; // local file
$this->bRewriteOnEachRequest = false;
return true;
}
/**
* set merge flag (for development environment)
* @param boolean set false to disable the generation of merged cachefiles
* @return boolean
*/
public function setMerging($bool) {
return $this->bMerging = (bool) $bool;
}
/**
* set minimum cachetime on server for merged js and css files
* @param integer value in seconds
* @return boolean
*/
public function setMinCacheTime($int) {
return $this->iMinCacheTime = (int) $int;
}
/**
* set flag bRewriteOnEachRequest (for development environment)
* @param boolean set true to ignore caching time
* @return type
*/
public function setRewriteCache($bool) {
return $this->bRewriteOnEachRequest = (bool) $bool;
}
// ----------------------------------------------------------------------
//
// ----------------------------------------------------------------------
/**
* Merge original files of a group to a single cachefile. It updates or just touches the cachefile.
* Output is the html code to include the cached css/ js in the html header.
* @param string $sType one of css|js
* @param string $sMyGroup filter group (given 2nd parameter in addCss or addJs); i.e. "defer" and "execution" for javascripts or a media attribute for css
* @return bool (dummy)
*/
public function writeMedia($sType, $sMyGroup = false) {
$sHtml = ''; // generated html output to document
$aFiles = $this->_aStatic[$sType]["files"];
$this->_log("===");
$this->_log("type: $sType");
$this->_log("THANKS FOR USING MY PHP CLASS STATICFILES!");
$this->_log("(this message and debug stuff below is shown with enabled debug only)");
$this->_log("www.axel-hahn.de/projects/php/staticfiles");
// if caching is disabled then generate output with original files and return
if (!$this->bMerging) {
$this->_log("You disabled the merging feature - here you get the links to the original files.");
foreach ($aFiles as $sGroup => $aFiledata) {
foreach ($aFiledata as $aFile) {
$sHtml .= $this->_genHtml($sType, $aFile);
}
}
echo $sHtml;
return true;
}
// step 1: get all files of a group
$aFileList = array();
foreach ($aFiles as $sGroup => $aFiledata) {
foreach ($aFiledata as $aFile) {
$aFileList[$sGroup][] = $aFile['file'];
}
}
// check / regenerate cache file
foreach ($aFiles as $sGroup => $aFiledata) {
$bCompress = true;
$iNewest = 0;
$sData = '';
// get cachefile and check age
$sCacheFile = $sType . "_cache_" . md5($sGroup . implode($aFileList[$sGroup])) . "." . $sType;
$sCacheFullFile = $this->sCacheDir . $sCacheFile;
$this->_log("===");
$this->_log("$sType-group: \"$sGroup\"");
$this->_log("cachefile = " . $sCacheFullFile);
if (file_exists($sCacheFullFile)) {
$bCompress = false;
$aStatCache = stat($sCacheFullFile);
$iCacheAge = date("U") - $aStatCache['mtime'];
if ($iCacheAge > $this->iMinCacheTime) {
$bCompress = true;
}
$this->_log("age of cachefile = $iCacheAge s (cachefile will be used $this->iMinCacheTime s; " . ($this->iMinCacheTime - $iCacheAge) . " s left)");
} else {
$this->_log("cachefile does not exist yet");
}
// if there is no cachefile or cachefile is out of date
// then compare with newest filestamp
if ($bCompress) {
// merge all files into $sData
// and get timestamp of newest file $iNewest
foreach ($aFiledata as $aFile) {
$this->_log("---");
$sFile = $aFile["file"];
$sFullFile = $_SERVER["DOCUMENT_ROOT"] . $sFile;
if (!file_exists($sFullFile))
die("ERROR: writeMedia() failed. File not found: $sFullFile.");
$aTmp = stat($sFullFile);
if ($aTmp['mtime'] > $iNewest)
$iNewest = $aTmp['mtime'];
$s = file_get_contents($sFullFile);
$this->_log("checking $sFile - \tunix ts: " . $aTmp['mtime'] . ";\t" . strlen($s) . " byte (group: " . $sGroup . ")");
if ($this->_aStatic[$sType]["fRewrite"]) {
$this->_log("rewrite with \$this->" . $this->_aStatic[$sType]["fRewrite"]);
$sRewritten = $this->{$this->_aStatic[$sType]["fRewrite"]}($s, $sFile);
if (strlen($sRewritten) == 0) {
$this->_log("OOOPS rewrite function for file $sFile - " . $this->_aStatic[$sType]["fRewrite"] . " returns ZERO(!!!) bytes");
} else
$s = $sRewritten;
}
$sData .= $s;
}
$this->_log("unix ts of cachefile " . $aStatCache['mtime'] . " - " . date("Y-m-d H:i:s", $aStatCache['mtime']));
if (($aStatCache['mtime'] - $iMinCacheTime) < $iNewest || $this->bRewriteOnEachRequest) {
$this->_log("Need to (re)generate cache. A static file is newer than cache or cache did not exist.");
$sOrig = $sData;
if ($this->_aStatic[$sType]["fCompress"]) {
$this->_log("compress with \$this->" . $this->_aStatic[$sType]["fCompress"] . " ");
$sData = $this->{$this->_aStatic[$sType]["fCompress"]}($sData);
if (strlen($sData) == 0) {
$this->_log("OOOPS compress function for type $sType - " . $this->_aStatic[$sType]["fCompress"] . " returns ZERO(!!!) bytes");
$this->_log("orig: " . strlen($sOrig) . " byte - files: \n" . print_r($aFileList[$sGroup], true) . " ");
$sData = $sOrig;
}
} else {
$this->_log("no compression for type " . $sType . " ");
}
$sStats = $this->_getStats($sOrig, $sData, " ");
$this->_log($sStats);
if ($this->bDebug){
$sData = "/* AXEL HAHN - CACHED " . $sType . " MERGE FILE ... created " . date("Y-m-d H:i:s") . " " . $sStats . " (you see this only if debug is enabled)\n\n */ \n\n" . $sData;
}
$this->_log("writing cachefile");
if (!file_exists(dirname($sCacheFullFile)))
mkdir(dirname($sCacheFullFile));
file_put_contents($sCacheFullFile, $sData);
} else {
$this->_log("just touching cachefile to use it further $this->iMinCacheTime s");
touch($sCacheFullFile);
}
}
// generate html output: link to the cacheile
$aCachefileoptions = false;
if ($sType == "css") {
$aCachefileoptions = array(
'file' => $this->sCacheBase . $sCacheFile,
'href' => $this->sCacheBase . $sCacheFile,
'media' => $sGroup,
);
}
if ($sType == "js") {
if ($sGroup == $sMyGroup) {
$aCachefileoptions = array(
'file' => $this->sCacheBase . $sCacheFile,
'src' => $this->sCacheBase . $sCacheFile,
'defer' => $sMyGroup,
);
}
}
if ($aCachefileoptions) {
$sHtml .= $this->_genHtml($sType, $aCachefileoptions);
}
}
echo $sHtml;
return true;
}
}
@return bool (dummy)