[EXTENSION] ExtendedClientScript - Reduce your page loading times

Quote

I think I should copy old one to another place (will be yii-blogdemo-enhanced-debug) in order to debug ExtendedClientScript.

Yea, I can reproduce that error at the following site.

http://pugpug.agilit…enhanced-debug/

Quote

PHP Error

Description

filemtime() [<a href='function.filemtime'>function.filemtime</a>]: stat failed for //demos/yii-blogdemo-enhanced-debug/assets/ec4f9b44/jquery.js

Source File

Read first here: http://www.yiiframew…script/reviews/

I have the same issue Boris did.

For me in line 221 does not make sense use DIRECTORY_SEPARATOR because this a URL separator and not a file separator.

You said: "It is also used in several other places in this extension".

This is just coincidence. In my Windows XP DIRECTORY_SEPARATOR is '&#039; and in your tests maybe is LINUX. And linux maybe is '/' separator, just like URL separator which is '/'.

I hope the author could read this… :(

Updated version with separated $combineJs and $combineCss. DIRECTORY_SEPARATOR issue fixed. Also added some checks for file existence instead of using @.



<?php


/**


 * Compress and cache used JS and CSS files.


 * Needs jsmin in helpers and csstidy in extensions.


 *


 * Ties into the 1.0.4 (or > SVN 813) Yii CClientScript functions.


 *


 * @author Maxximus <maxximus007@gmail.com>


 * @author Alexander Makarov <sam@rmcreative.ru>


 *


 * @link http://www.yiiframework.com/


 * @copyright Copyright &copy; 2008-2009


 * @license http://www.yiiframework.com/license/


 * @version 0.5


 */


class ExtendedClientScript extends CClientScript {


    /**


     * Compress all Javascript files with JSMin. JSMin must be installed as an extension in dir jsmin.


     * code.google.com/p/jsmin-php/


     */


    public $compressJs = false;





    /**


     * Compress all CSS files with CssTidy. CssTidy must be installed as an extension in dir csstidy.


     * Specific browserhacks will be removed, so don't add them in to be compressed CSS files


     * csstidy.sourceforge.net


     */


    public $compressCss = false;





    /**


     * Combine all JS files into one.


     *


     * @var boolean


     */


    public $combineJs = false;





    /**


     * Combine all CSS files into one. Be careful with relative paths in CSS.


     *


     * @var boolean


     */


    public $combineCss = false;





    /**


     * Exclude certain files from inclusion. array('/path/to/excluded/file') Useful for fixed base


     * and incidental additional JS.


     */


    public $excludeFiles = array();





    /**


     * Path where the combined/compressed file will be stored. Will use coreScriptUrl if not defined


     */


    public $filePath;





    /**


     * If true, all files to be included will be checked if they are modified.


     * To enhance speed (eg production) set to false.


     */


    public $autoRefresh = true;





    /**


     * Relative Url where the combined/compressed file can be found


     */


    public $fileUrl;





    /**


     * Path where files can be found


     */


    public $basePath;





    /**


     * Used for garbage collection. If not accessed for that period: remove.


     */


    public $ttlDays = 1;


    


    /**


     * prefix for the combined/compressed files


     */


    public $prefix = 'c_';


    


    /**


     * CssTidy template. See CssTidy for more information


     */


    public $cssTidyTemplate = "highest_compression";





    /**


     * CssTidy parameters. See CssTidy for more information


     */


    public $cssTidyConfig = array(


	'css_level' => 'CSS2.1',


	'discard_invalid_properties' => FALSE,


	'lowercase_s' => FALSE,


	'sort_properties' => FALSE,


	'sort_selectors' => FALSE,


	'preserve_css' => FALSE,


	'timestamp' => FALSE,


	'remove_bslash' => TRUE,


	'compress_colors' => TRUE,


	'compress_font-weight' => TRUE,


	'remove_last_,' => TRUE,


	'optimise_shorthands' => 1,


	'case_properties' => 1,


	'merge_selectors' => 2,


    );





    private $_changesHash = '';


    private $_renewFile;





    /**


     * Will combine/compress JS and CSS if wanted/needed, and will continue with original


     * renderHead afterwards


     *


     * @param string $output


     */


    public function renderHead(&$output) {


	if ($this->combineJs) {


	    if (isset($this->scriptFiles[parent::POS_HEAD]) && count($this->scriptFiles[parent::POS_HEAD]) !==  0) {


		$jsFiles = $this->scriptFiles[parent::POS_HEAD];


		if (!empty($this->excludeFiles)) {


		    foreach ($jsFiles as &$fileName)


			(in_array($fileName, $this->excludeFiles)) AND $fileName = false;


		    $jsFiles = array_filter($jsFiles);


		}


		$this->combineAndCompress('js',$jsFiles,parent::POS_HEAD);


	    }


	}





	if($this->combineCss) {


	    if (count($this->cssFiles) !==  0) {


		foreach ($this->cssFiles as $url => $media)


		    $cssFiles[$media][$url] = $url;


		foreach ($cssFiles as $media => $url)


		    $this->combineAndCompress('css',$url, $media);


	    }


	}


	parent::renderHead($output);


    }





    /**


     * Will combine/compress JS if wanted/needed, and will continue with original


     * renderBodyEnd afterwards


     *


     * @param string $output


     */


    public function renderBodyBegin(&$output) {


	if ($this->combineJs) {


	    if (isset($this->scriptFiles[parent::POS_BEGIN]) && count($this->scriptFiles[parent::POS_BEGIN]) !==  0) {


		$jsFiles = $this->scriptFiles[parent::POS_BEGIN];


		if (!empty($this->excludeFiles)) {


		    foreach ($jsFiles as &$fileName)


			(in_array($fileName, $this->excludeFiles)) AND $fileName = false;


		    $jsFiles = array_filter($jsFiles);


		}


		$this->combineAndCompress('js',$jsFiles,parent::POS_BEGIN);


	    }


	}


	parent::renderBodyBegin($output);


    }





    /**


     * Will combine/compress JS if wanted/needed, and will continue with original


     * renderBodyEnd afterwards


     *


     * @param string $output


     */


    public function renderBodyEnd(&$output) {


	if ($this->combineJs) {


	    if (isset($this->scriptFiles[parent::POS_END]) && count($this->scriptFiles[parent::POS_END]) !==  0) {


		$jsFiles = $this->scriptFiles[parent::POS_END];


		if (!empty($this->excludeFiles)) {


		    foreach ($jsFiles as &$fileName)


			(in_array($fileName, $this->excludeFiles)) AND $fileName = false;


		    $jsFiles = array_filter($jsFiles);


		}


		$this->combineAndCompress('js',$jsFiles,parent::POS_END);


	    }


	}


	parent::renderBodyEnd($output);


    }





    /**


     *	Performs the actual combining and compressing


     *


     * @param string $type


     * @param array $urls


     * @param string $pos


     */


    private function combineAndCompress($type, $urls, $pos) {


	$this->fileUrl or $this->fileUrl = $this->getCoreScriptUrl();


	$this->filePath or $this->filePath = realpath($_SERVER['DOCUMENT_ROOT'].$this->fileUrl);


	$this->basePath or $this->basePath = $_SERVER['DOCUMENT_ROOT'];





	if ($this->autoRefresh) {


	    $mtimes = array();


	    foreach ($urls as $file) {


		$fileName = $this->basePath.'/'.trim($file,'/');


		if(file_exists($fileName)) {


		    $mtimes[] = filemtime($fileName);


		}


	    }


	    $this->_changesHash = md5(serialize($mtimes));


	}





	$combineHash = md5(implode('',$urls));





	$optionsHash = ($type == 'js') ? md5($this->basePath.$this->compressJs.$this->ttlDays.$this->prefix):


	    md5($this->basePath.$this->compressCss.$this->ttlDays.$this->prefix.serialize($this->cssTidyConfig));





	$fileName = $this->prefix.md5($combineHash.$optionsHash.$this->_changesHash).".$type";





	$this->_renewFile = (file_exists($this->filePath.'/'.$fileName)) ? false : true;





	if ($this->_renewFile) {


	    $this->garbageCollect($type);





	    $combinedFile = '';





	    foreach ($urls as $key => $file)


		$combinedFile .= file_get_contents($this->basePath.'/'.$file);





	    if ($type == 'js' && $this->compressJs)


		$combinedFile = $this->minifyJs($combinedFile);





	    if ($type == 'css' && $this->compressCss)


		$combinedFile = $this->minifyCss($combinedFile);





	    file_put_contents($this->filePath.'/'.$fileName, $combinedFile);


	}





	foreach ($urls as $url)


	    $this->scriptMap[basename($url)] = $this->fileUrl.'/'.$fileName;





	$this->remapScripts();


    }





    private function garbageCollect($type) {


	$files = CFileHelper::findFiles($this->filePath, array('fileTypes' => array($type), 'level'=> 0));





	foreach($files as $file) {


	    if (strpos($file, $this->prefix) !== false && $this->fileTTL($file)){


		unlink($file);


	    }


	}


    }





    /**


     * See if file is ready for deletion


     *


     * @param string $file


     */


    private function fileTTL($file) {


	if(!file_exists($file)) return false;


	$ttl = $this->ttlDays * 60 * 60 * 24;


	return ((fileatime($file) + $ttl) < time()) ? true : false;


    }





    /**


     * Minify javascript with JSMin


     *


     * @param string $js


     */


    private function minifyJs($js) {


	Yii::import('application.extensions.jsmin.*');


	require_once('JSMin.php');


	return JSMin::minify($js);


    }





    /**


     * Yii-ified version of CSS.php of the Minify package with fixed options


     *


     * @param string $css


     */


    private function minifyCss($css) {


	Yii::import('application.extensions.csstidy.*');


	require_once('class.csstidy.php');





	$cssTidy = new csstidy();


	$cssTidy->load_template($this->cssTidyTemplate);





	foreach($this->cssTidyConfig as $k => $v)


	    $cssTidy->set_cfg($k, $v);





	$cssTidy->parse($css);


	return $cssTidy->print->plain();


    }


}


Thank you samdark.

Still exists the error. You can find on the agility hoster free hosting service. ??? On the other hand, this extension is working well on the byethost free hosting service.

Yii itself is working on the agilityhoster well, because there is no exp​ression using $_SERVER[‘DOCUMENT_ROOT’] in the framework. On the other hand, this extension uses this variable and there is the case that it does not work on some hosts, thus we should remove this.

Replaced DOCUMENT_ROOT with Yii's getPathOfAlias(). Please try if it works now.



<?php


/**


 * Compress and cache used JS and CSS files.


 * Needs jsmin in helpers and csstidy in extensions.


 *


 * Ties into the 1.0.4 (or > SVN 813) Yii CClientScript functions.


 *


 * @author Maxximus <maxximus007@gmail.com>


 * @author Alexander Makarov <sam@rmcreative.ru>


 *


 * @link http://www.yiiframework.com/


 * @copyright Copyright &copy; 2008-2009


 * @license http://www.yiiframework.com/license/


 * @version 0.6


 */


class ExtendedClientScript extends CClientScript {


    /**


     * Compress all Javascript files with JSMin. JSMin must be installed as an extension in dir jsmin.


     * code.google.com/p/jsmin-php/


     */


    public $compressJs = false;





    /**


     * Compress all CSS files with CssTidy. CssTidy must be installed as an extension in dir csstidy.


     * Specific browserhacks will be removed, so don't add them in to be compressed CSS files


     * csstidy.sourceforge.net


     */


    public $compressCss = false;





    /**


     * Combine all JS files into one.


     *


     * @var boolean


     */


    public $combineJs = false;





    /**


     * Combine all CSS files into one. Be careful with relative paths in CSS.


     *


     * @var boolean


     */


    public $combineCss = false;





    /**


     * Exclude certain files from inclusion. array('/path/to/excluded/file') Useful for fixed base


     * and incidental additional JS.


     */


    public $excludeFiles = array();





    /**


     * Path where the combined/compressed file will be stored. Will use coreScriptUrl if not defined


     */


    public $filePath;





    /**


     * If true, all files to be included will be checked if they are modified.


     * To enhance speed (eg production) set to false.


     */


    public $autoRefresh = true;





    /**


     * Relative Url where the combined/compressed file can be found


     */


    public $fileUrl;





    /**


     * Path where files can be found


     */


    public $basePath;





    /**


     * Used for garbage collection. If not accessed for that period: remove.


     */


    public $ttlDays = 1;


    


    /**


     * prefix for the combined/compressed files


     */


    public $prefix = 'c_';


    


    /**


     * CssTidy template. See CssTidy for more information


     */


    public $cssTidyTemplate = "highest_compression";





    /**


     * CssTidy parameters. See CssTidy for more information


     */


    public $cssTidyConfig = array(


	'css_level' => 'CSS2.1',


	'discard_invalid_properties' => FALSE,


	'lowercase_s' => FALSE,


	'sort_properties' => FALSE,


	'sort_selectors' => FALSE,


	'preserve_css' => FALSE,


	'timestamp' => FALSE,


	'remove_bslash' => TRUE,


	'compress_colors' => TRUE,


	'compress_font-weight' => TRUE,


	'remove_last_,' => TRUE,


	'optimise_shorthands' => 1,


	'case_properties' => 1,


	'merge_selectors' => 2,


    );





    private $_changesHash = '';


    private $_renewFile;





    /**


     * Will combine/compress JS and CSS if wanted/needed, and will continue with original


     * renderHead afterwards


     *


     * @param string $output


     */


    public function renderHead(&$output) {


	if ($this->combineJs) {


	    if (isset($this->scriptFiles[parent::POS_HEAD]) && count($this->scriptFiles[parent::POS_HEAD]) !==  0) {


		$jsFiles = $this->scriptFiles[parent::POS_HEAD];


		if (!empty($this->excludeFiles)) {


		    foreach ($jsFiles as &$fileName)


			(in_array($fileName, $this->excludeFiles)) AND $fileName = false;


		    $jsFiles = array_filter($jsFiles);


		}


		$this->combineAndCompress('js',$jsFiles,parent::POS_HEAD);


	    }


	}





	if($this->combineCss) {


	    if (count($this->cssFiles) !==  0) {


		foreach ($this->cssFiles as $url => $media)


		    $cssFiles[$media][$url] = $url;


		foreach ($cssFiles as $media => $url)


		    $this->combineAndCompress('css',$url, $media);


	    }


	}


	parent::renderHead($output);


    }





    /**


     * Will combine/compress JS if wanted/needed, and will continue with original


     * renderBodyEnd afterwards


     *


     * @param string $output


     */


    public function renderBodyBegin(&$output) {


	if ($this->combineJs) {


	    if (isset($this->scriptFiles[parent::POS_BEGIN]) && count($this->scriptFiles[parent::POS_BEGIN]) !==  0) {


		$jsFiles = $this->scriptFiles[parent::POS_BEGIN];


		if (!empty($this->excludeFiles)) {


		    foreach ($jsFiles as &$fileName)


			(in_array($fileName, $this->excludeFiles)) AND $fileName = false;


		    $jsFiles = array_filter($jsFiles);


		}


		$this->combineAndCompress('js',$jsFiles,parent::POS_BEGIN);


	    }


	}


	parent::renderBodyBegin($output);


    }





    /**


     * Will combine/compress JS if wanted/needed, and will continue with original


     * renderBodyEnd afterwards


     *


     * @param string $output


     */


    public function renderBodyEnd(&$output) {


	if ($this->combineJs) {


	    if (isset($this->scriptFiles[parent::POS_END]) && count($this->scriptFiles[parent::POS_END]) !==  0) {


		$jsFiles = $this->scriptFiles[parent::POS_END];


		if (!empty($this->excludeFiles)) {


		    foreach ($jsFiles as &$fileName)


			(in_array($fileName, $this->excludeFiles)) AND $fileName = false;


		    $jsFiles = array_filter($jsFiles);


		}


		$this->combineAndCompress('js',$jsFiles,parent::POS_END);


	    }


	}


	parent::renderBodyEnd($output);


    }





    /**


     *	Performs the actual combining and compressing


     *


     * @param string $type


     * @param array $urls


     * @param string $pos


     */


    private function combineAndCompress($type, $urls, $pos) {


	$this->fileUrl or $this->fileUrl = $this->getCoreScriptUrl();


	$this->basePath or $this->basePath = Yii::getPathOfAlias('webroot');


	$this->filePath or $this->filePath = Yii::getPathOfAlias('webroot').$this->fileUrl;	





	if ($this->autoRefresh) {


	    $mtimes = array();


	    foreach ($urls as $file) {


		$fileName = $this->basePath.'/'.trim($file,'/');


		if(file_exists($fileName)) {


		    $mtimes[] = filemtime($fileName);


		}


	    }


	    $this->_changesHash = md5(serialize($mtimes));


	}





	$combineHash = md5(implode('',$urls));





	$optionsHash = ($type == 'js') ? md5($this->basePath.$this->compressJs.$this->ttlDays.$this->prefix):


	    md5($this->basePath.$this->compressCss.$this->ttlDays.$this->prefix.serialize($this->cssTidyConfig));





	$fileName = $this->prefix.md5($combineHash.$optionsHash.$this->_changesHash).".$type";





	$this->_renewFile = (file_exists($this->filePath.'/'.$fileName)) ? false : true;





	if ($this->_renewFile) {


	    $this->garbageCollect($type);





	    $combinedFile = '';





	    foreach ($urls as $key => $file)


		$combinedFile .= file_get_contents($this->basePath.'/'.$file);





	    if ($type == 'js' && $this->compressJs)


		$combinedFile = $this->minifyJs($combinedFile);





	    if ($type == 'css' && $this->compressCss)


		$combinedFile = $this->minifyCss($combinedFile);





	    file_put_contents($this->filePath.'/'.$fileName, $combinedFile);


	}





	foreach ($urls as $url)


	    $this->scriptMap[basename($url)] = $this->fileUrl.'/'.$fileName;





	$this->remapScripts();


    }





    private function garbageCollect($type) {


	$files = CFileHelper::findFiles($this->filePath, array('fileTypes' => array($type), 'level'=> 0));





	foreach($files as $file) {


	    if (strpos($file, $this->prefix) !== false && $this->fileTTL($file)){


		unlink($file);


	    }


	}


    }





    /**


     * See if file is ready for deletion


     *


     * @param string $file


     */


    private function fileTTL($file) {


	if(!file_exists($file)) return false;


	$ttl = $this->ttlDays * 60 * 60 * 24;


	return ((fileatime($file) + $ttl) < time()) ? true : false;


    }





    /**


     * Minify javascript with JSMin


     *


     * @param string $js


     */


    private function minifyJs($js) {


	Yii::import('application.extensions.jsmin.*');


	require_once('JSMin.php');


	return JSMin::minify($js);


    }





    /**


     * Yii-ified version of CSS.php of the Minify package with fixed options


     *


     * @param string $css


     */


    private function minifyCss($css) {


	Yii::import('application.extensions.csstidy.*');


	require_once('class.csstidy.php');





	$cssTidy = new csstidy();


	$cssTidy->load_template($this->cssTidyTemplate);





	foreach($this->cssTidyConfig as $k => $v)


	    $cssTidy->set_cfg($k, $v);





	$cssTidy->parse($css);


	return $cssTidy->print->plain();


    }


}


Okay, nice one! Did not test it yet, but will update soon.

It still does not work. If it only happens on this free host, it may be mis-configuration of this host ???

EDIT: NO. It does not work every host I have.

Insert

echo Yii::getPathOfAlias(‘webroot’);
somewhere and post results here. It can be helpful.

DOCUMENT_ROOT may rather be Yii::getPathOfAlias('system')."/…/" though it does not work on the agility hoster, while it works on the other hosts.

Yii::getPathOfAlias('webroot');
should give webroot.

In terms of webroot, I have posted the result as below.

Quote

It still does not work. If it only happens on this free host, it may be mis-configuration of this host ???

EDIT: NO. It does NOT work EVERY host I have.

OK. It does not work… but what is the content of

echo Yii::getPathOfAlias('webroot');

?

The code



echo Yii::getPathOfAlias('webroot');


produces

Quote

/home/www/pugpug.agilityhoster.com/demos/yii-blogdemo-enhanced

FYI, you can see the info as below. DOCUMENT_ROOT may be wrong.

http://pugpug.agilit…/demos/info.php

As I myself modified to produce the equivarent exp​ression as DOCUMENT_ROOT using following code, it produced internal error that I cannot analyze.



    $this->basePath or $this->basePath = substr($_SERVER['SCRIPT_FILENAME'], 0, strpos($_SERVER['SCRIPT_FILENAME'], $_SERVER['PHP_SELF']));


    $this->filePath or $this->filePath = realpath($this->basePath.$this->fileUrl);


Quote

/home/www/pugpug.agilityhoster.com/demos/yii-blogdemo-enhanced

If your webroot is /demos/yii-blogdemo-enhanced it looks OK.

Quote

Replaced DOCUMENT_ROOT with Yii's getPathOfAlias(). Please try if it works now.

HI samdark, thanks for this issue fix. But at my workstation, DOCUMENT_ROOT is working, while the Yii's getPathOfAlias() is not.

So I changed back to DOCUMENT_ROOT, and it works well.



$this->fileUrl or $this->fileUrl = $this->getCoreScriptUrl();


$this->basePath or $this->basePath = Yii::getPathOfAlias('webroot');


//$this->filePath or $this->filePath = Yii::getPathOfAlias('webroot').$this->fileUrl);


$this->filePath or $this->filePath = realpath($_SERVER['DOCUMENT_ROOT'].$this->fileUrl);


Thanks again!

PS: Run this on Linux and Windows.

I’m trying to use Yii’s CStarRating widget with extendedclientscript.

It works with CSS and Javascript compression set to true, but when I set combineFiles to true, only little boxes outlined in red show up.

Any ideas?

It turns out the widget CSS bundled with CStarRating couldn’t find it’s images in the same directory (I’m guessing because of something done during extendedclientscript’s combine and/or JSMin compression).

I had to override the CSS file in CStarRating.php to a duplicate in /css and it worked.

The little red boxes are a result of a 1px border in that same CSS file. I removed that and now it all looks normal.

and THEN I learned I could override the CSS file in the call to the widget itself…lol, I’m doing good work over here ::)