[EXTENSION] ExtendedClientScript - Reduce your page loading times

To test the problem I replaced csstidy::is_important & csstidy::gvw_important in class.csstidy_optimise.php with isset()

That did the trick to test, however, im not sure why im getting this "csstidy::is_important() should not be called statically" error message. If anyone has any thoughts or could offer help would really appreciate it.

thanks

I have the same issue.

It’s because the function in the csstidy class file isn’t defined as being ‘static’. I’m not on the right machine currently to look up the exact lines, but just find where the start of the is_important function is and add static in front of the word public (eg. static public function is_important() ). Same seal with the other function.

Hi, currently having some issues with the latest Zii SVN (161) code, seemingly JQuery UI 1.8 doesn’t like the combine part.

Don’t have a solution yet, but couldn’t find something funny in the new Zii code yet.

Think it’s a good idea to put the code in a Google repo, for a better (and faster) handling. Will look it to that soon.

Ok, the current issue now with the new jQuery UI: They use @import. This is not working with csstidy. Seemingly minify is progressing there, so maybe swith to minify.

Currently I combine now all the @imports to one jquery.ui.all.css

Hi,

Where can I download the latest working version of this extension. Under the download section it says uploaded on March 12, 2009 but I can see lots of changes since than discussed here so if you could update me on the latest version I would be thankful.

Cheers,

bettor

Hi bettor,

I personally still use the version as in Yii extensions, works fine for me. Some others use samdarks version (see page 2 of this topic).

Please tell me if you have any issue with it, and I’ll look into it.

in my project i need also groups for differ js files. For example, for one language - one set, for other language - other set. That’s why i took last version from that thread and added groups:

i have to change registerScriptFile and registerCssFile to add groups. Usage,

registerCssFile($url, $media=’’, $group = ‘all’)

registerScriptFile($url, $position = 0, $group = ‘all’)

Yes, default group is always ‘all’.

P.S.: still need to solve problem with themes. Current version doesn’t support CSS grouping and theming :(




<?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>

 * @author Andrey gayvoronsky <plandem@gmail.com>

 *

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

 * @copyright Copyright &copy; 2008-2009

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

 * @version 0.7

 */

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,

	);


	/**

	 * Used for grouping

	 */

	public $groups = array(

		'js'	=> array(), // array(url => group)

		'css'	=> array(), // array(url => group)

	);


	/**

	 * 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;


		$optionsHash = ($type == 'js')

				? md5($this->basePath . $this->compressJs . $this->ttlDays . $this->prefix)

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


		$group_changes	= array();

		if ($this->autoRefresh) {

			$mtimes = array();

			foreach($this->groups[$type] as $group => $group_urls) {

				foreach($group_urls as $file) {

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

					if (file_exists($fileName)) {

						$mtimes[] = filemtime($fileName);

					}

				}


				$group_changes[$group] = md5(serialize($mtimes));

			}

		}


		$files		= array();

		$changes	= sizeof($group_changes);

		

		foreach($this->groups[$type] as $group => $group_urls) {

			$changedHash	= ($changes) ? $group_changes[$group]: '';

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

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

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


			$files[$group]	= array(

				'hash'	=> $combineHash,

				'name'	=> $fileName,

				'renew'	=> $renewFile,

			);

		}


		$cleared	= false;

		foreach($files as $group => $c_file) {

			if($c_file['renew']) {

				if(!$cleared) {

					$this->garbageCollect($type);

					$cleared	= true;

				}


				$combinedFile = '';

				foreach($this->groups[$type][$group] as $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 . '/' . $c_file['name'], $combinedFile);

			}

		}


		foreach($this->groups[$type] as $group => $group_urls) {

			foreach($group_urls as $url) {

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

			}

		}


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

	}


	/**

	 * Registers a javascript file.

	 * @param string URL of the javascript file

	 * @param integer the position of the JavaScript code. Valid values include the following:

	 * <ul>

	 * <li>CClientScript::POS_HEAD : the script is inserted in the head section right before the title element.</li>

	 * <li>CClientScript::POS_BEGIN : the script is inserted at the beginning of the body section.</li>

	 * <li>CClientScript::POS_END : the script is inserted at the end of the body section.</li>

	 * </ul>

	 * @param string group. Using for grouping (compressing/minimazing). Default group - 'all'

	 */

	public function registerScriptFile($url, $position = 0, $group = 'all') {

		parent::registerScriptFile($url, $position);


		if(!isset($this->groups['js'][$group]))

			$this->groups['js'][$group]	= array();


		$this->groups['js'][$group][]	= $url;

	}


	/**

	 * Registers a CSS file

	 * @param string URL of the CSS file

	 * @param string media that the CSS file should be applied to. If empty, it means all media types.

	 * @param string group. Using for grouping (compressing/minimazing). Default group - 'all'

	 */

	public function registerCssFile($url, $media='', $group = 'all') {

		parent::registerCssFile($url, $media);


		if(!isset($this->groups['css'][$group]))

			$this->groups['css'][$group]	= array();


		$this->groups['css'][$group][]	= $url;

	}

}




Looks good! Yes, I think CSS grouping could be interesting, especially when working with different media or themes.

For the JS combineAndCompress there could be another interesting option: http://labjs.com/. This will allow parallel loading of JS files, like ga.js from Google analytics.

Perhaps something like this could be something to be built in to Yii. Pls share your opinion!

Nice extension, but without support for relative CSS paths, it breaks many scripts. It would be great if you can add some regular expressions to automatically convert them.

Nice extension! Could someone advise a solution for the relative urls in css files e.g. jquery ui?

There is a new version supporting remote files (eg. excluding them from combining and compression) thanks to Kir. Relative paths is one of the things to desire, perhaps there will be a new version supporting this in the near future. I personally change all relative paths to absolute paths, something you should do anyway when using a CDN.

The PHP 5.3+ version, without the CssTidy issue will require another compressor since CssTidy is not developed anymore (or a rebuild of CssTidy). Perhaps cssmin will do?

Edit: the new version is based on the original one, not the one of Sam. Will combine soon.

I’m using cssmin / jsmin with a bit modified version of your extension. Works just fine.

Maybe it will be good putting it at GitHub. Probably will boost development process to some degree.

Version with CssMin is uploaded today. Some of the Sam parts are in too, but not the groups thing, didn’t work for me. It’s now an extension with CssMin and JsMin included in subdirs.

does anybody know why i get this in some pages and not others?

include(JSMin.php) [<a href=‘function.include’>function.include</a>]:

failed to open stream: No such file or directory

having $combineCss = true produces the error

Take a look at what the file is called…

Maybe it’s called JsMin.php instead?

File names are ok… :(

So if i empty assets dir after the error the page works, but others dont

it looks like the error is originated once i set the compressCss property to true …

Does anybody know if it has something to do with the way my css is written? perhaps errors? if so, is there a way for the minifier to detect it?

// UPDATE

i changed the way i was minifying css and now works, awesome extension.

There is a bug when you turn on the plan. JqueryUI JqueryTreeView and located in a subfolder of assets. And with the compression and union file is stored in the root of the problem and turns when referring to images, etc.