[EXTENSION] ExtendedClientScript - Reduce your page loading times

Thanks! I didn't use themes yet myself, but are these urls not already replaced by the theme manager?

If not, how does Yii handle this normally, since the extension grabs the urls at the latest possible moment.

Hi Maxximus,

I'm trying to use your extension to see how does it work with the jui extension, but I'm getting this error:

but:

  • [tt]jquery.js[/tt] is a core script.

  • My app is in my public_html (user's web directory), not in /var/www (why is this being appended?)

I'm using the latest yii from the svn repository. My [tt]main.php[/tt] config is the example given in the extension.

Thanks in advanced.

Hi MetaYii,

First of all, you can (will) run into some issues with JUI, especially when used with ThemeRoller themes, because of the relative paths used there.

For your current issue: It seems something goes wrong with the determination of the following: realpath($_SERVER['DOCUMENT_ROOT']). For one reason or another your $_SERVER['DOCUMENT_ROOT'] is still the default /var/www/.

Besides that, I doubt that the use of relative filepaths (~metayii/) will work as expected. Could you please try to set basePath to the correct absolute path (/home/metayii/Desarrollo/YII/yii-svn/demos/helloworld2/) in main.php, and see if things work as expected?

Hi Maximmus,

I have tried out this extension as well.

I think it might be advantageous to be able to choose to compress CSS or JS files separately, so you don't run into issues with relative paths in CSS but you still want to compile and compress your published JS scripts into one file.

Hi, sorry for my late reply. You can already set almost everything in your main.php config, for instance:



	    'clientScript'=>array(


			'class'=>'application.components.ExtendedClientScript',


			'combineFiles'=>false,


			'compressCss'=>false,


			'compressJs'=>true,


		),


Only thing is that CSS and JS will be combined with combineFiles. In another version I will separate that.

Maxximus: as far as I can see, the files to compress/combine are get by their URIs, right?

No, the files are got by their absolute filepath to compress and combine. After that the Yii function remapScripts() points them all to the created file, and Yii will only output the correct HTML line once.

In general, I only touch some variables used by Yii::clientScript just before output time. But because of location change (writing the generated file to the temporary Yii cache directory) relative URL's won't work anymore.

I could adjust the relative URL's in the generated file pretty easy by prepending the relative URL's with the original URI, but other options are appreciated.

Maxximus, first of all, Great extension. I like it.

Secondly, according to my googling result, you do not need to use the DIRECTORY_SEPERATOR constant. I find it causes problem in my local Windows machine. Check the following link,

http://old.alanhogan…r-not-necessary

Cheers,

Well, I actually used DIRECTORY_SEPARATOR to enhance compatibility ;). Yii itself uses it a lot too, so I’m surprised it is only giving trouble here.

Will remove it in a next version.

Quote

Secondly, according to my googling result, you do not need to use the DIRECTORY_SEPERATOR constant. I find it causes problem in my local Windows machine. Check the following link,

http://old.alanhogan…r-not-necessary

I checked my program and found that I have used DIRECTORY_SEPERATOR in my URL. Thank you.

http://www.yiiframew…opic,835.0.html

Hi Maxximus,

I have transferred my application to the free hosting site as below, and found the error saying that $this->basePath is null ???

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

As this host is free and you can check yourself, but can you think of how to debug this according to this error message?

Sorry but the error message was changed.

Original error message was something like file '//demos/yii-blogdemo-enhanced/…/' not found. I thought that the variable of $_SERVER['DOCUMENT_ROOT'] seems to be null.

I just checked it on the host, and found that the variable of $_SERVER['DOCUMENT_ROOT'] being '/', then the tool seems not to work.

Now you can see the application is working, because I disabled this extension of ExtendedClientScript.

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

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

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();


    }


}