SimpleAssetmanager for Yii

In a recent Yii version my main-concern about safe_mode not working and duplication of files was nearly fixed
So the below code/text is quite out of date. But since safe_mode isn't 100% working I developed another extension: here

I learned from this that I shouldn't move a very simple problem to something extremely complex through adding more and more features to it. (I learned it already quite often (for example the eav project))
The problem was, looking at this after a half year:
  • I didn't understand
  • It looks too complex
  • .. maybe caused through mixing all features together (it would be better to make them more distinguishable)
What was good and what I'm still missing: more readable pathnames.

I developed this Assetmanager cause the current Implementation of Yii doesn't work
in PHP safe_mode and hides the folders too much from the developer
And makes it hard to develop on css/javascript cause publishing only occurs once
Hashes will change if project moves inside the filesystem - for example uploading would invalidate all hashes

So the main features are:
  • different Strategies to publish assets
    • copy - just like the already existing behavior
    • link - will create a symlink to the directories
    • keep - won't publish the directory but point directly to that folder - important: you have to adjust your .htaccess to allow access
  • human readable foldernames - they will look like this: protected_modules_testa13af23 the italic part in the name is the already known hash
  • hashes are relative.. so not beginning from / or C:/ but from the project or framework folder
  • the api is almost the same to the existing assetmanager - i removed only some functions and parameters which were never used (empiric reasearch with some extensions;) )
Cause I'm still developing on this extension I won't add a downloadfile here.. but you can see the life running code below.. if you have problems downloading the code just wget this html-file and strip out the surrounding html ;)
	
<?php
/* SimpleAssetmanager will give extended functionality to the yii-assetmanager
License: MIT | Copyright 2010 by balrok | Author: balrok.1787569@googlemail.com, http://balrok.com
*/

class SimpleAssetManager extends CApplicationComponent
{
	// directory name inside the project's folder where assets getting published too.. I see no reason to change this..
	public $default_basepath ='assets';

	// when true: will allway republish the files. Works with strategy link and copy
	public $forceCopy = false;

	// you either can 'copy' all the files in the roots asset or 'link' (symlink) them there
	// 'keep'/'safe_mode' will use path_map to map directory pathes to urls (Adjust your .htaccess for this!)
	public $strategy = 'copy';

	// the path_map is used for generating hashes (when you don't want to get hashes
	// by absolute pathes but by relative ones
	// also it's used by the 'keep'-strategy which will map pathes to urls
	// array has the form '/absolute/path/'=>'http://123.0.0.1/path/
	// if you don't need the keep-strategy you should at least add '/absolute/path'=>''
	// the entry for projects folder is already added together with url
	// and the entry for framework folder is mapped to '' when no url is specified for it
	public $path_map = array();

	// human readable will add the directory structure in front of the hash to identify faster the source
	public $human_readable = false;
	// max length of a directory name - needed when using human readable pathnames which can get very long
	public $dir_max_length = 128;

	public function init()
	{
		$scriptDir = dirname(Yii::app()->getRequest()->getScriptFile()).DIRECTORY_SEPARATOR;
		if (!isset($this->path_map[$scriptDir]))
			$this->path_map[$scriptDir] = Yii::app()->getRequest()->getBaseUrl()."/";
		// at least map the yii-framework path to nothing to support human_readable option and create relative-pathes
		if (!isset($this->path_map[YII_PATH]))
			$this->path_map[YII_PATH] = '';
	}

	public function getBasePath()
	{
		static $basePath = null;
		if($basePath===null)
		{
			$request=Yii::app()->getRequest();
			$basePath = $this->setBasePath(dirname($request->getScriptFile()).DIRECTORY_SEPARATOR.$this->default_basepath);
		}
		return $basePath;
	}

	public function setBasePath($value)
	{
		if(($basePath=realpath($value))!==false && is_dir($basePath) && is_writable($basePath))
			return $basePath;
		else
			throw new CException(Yii::t('yii','CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.',
				array('{path}'=>$value)));
	}

	public function getBaseUrl()
	{
		static $baseUrl = null;
		if($baseUrl===null)
		{
			$request=Yii::app()->getRequest();
			$baseUrl = $this->setBaseUrl($request->getBaseUrl().'/'.$this->default_basepath);
		}
		return $baseUrl;
	}

	public function setBaseUrl($value)
	{
		return rtrim($value,'/');
	}

	private function _strategyKeep($realpath)
	{
		foreach ($this->path_map as $k => $v)
			if (($dst = str_replace($k, $v, $realpath)) != $realpath)
				return str_replace(DIRECTORY_SEPARATOR, '/', $dst);
		throw new CException("couldn't convert path to url - specify a an URL inside path_map for path: ".$realpath);
	}
	
	// old api was publish($path,$hashByName=false,$level=-1,$forceCopy=false)
	public function publish($path)
	{
		static $_published = array();
		if(isset($_published[$path]))
			return $_published[$path];
		if(($src=realpath($path))===false)
			throw new CException(Yii::t('yii','The asset "{asset}" to be published does not exist.',
				array('{asset}'=>$path)));
		if ($this->strategy == 'keep' || $this->strategy == 'safe_mode')
		{
			$_published[$path] = $this->_strategyKeep($src);
		}
		else
		{
			if(is_file($src))
				$location = array($this->hash(dirname($src)), basename($src));
			else if(is_dir($src))
				$location = array($this->hash($src));

			$_published[$path] = $this->getBaseUrl()."/".implode('/', $location);
			$dst = $this->getBasePath().DIRECTORY_SEPARATOR.implode(DIRECTORY_SEPARATOR, $location);

			// if dstdir doesn't exist publish it
			if(!file_exists($dst) || $this->forceCopy)
				$this->_moveFiles($src, $dst);
		}
		return $_published[$path];
	}

	private function _moveFiles($src, $dst)
	{
		switch ($this->strategy)
		{
			case 'copy':
			default:
				if (is_file($src))
				{
					@mkdir(dirname($dst));
					@chmod(dirname($dst),0777);
					copy($src, $dst);
				}
				else
					CFileHelper::copyDirectory($src,$dst,array('exclude'=>array('.svn'),'level'=>-1));
				break;
			case 'link':
				if (is_file($src))
				{
					if (!file_exists(dirname($dst)))
					{
						@mkdir(dirname($dst));
						@chmod(dirname($dst),0777);
					}
					@unlink($dst);
					@symlink($src, $dst);
				}
				else
				{
					// TODO rmdir recursive
					@rmdir($dst);
					@symlink($src, $dst);
				}
				break;
		}
	}

	protected function hash($path)
	{
		/* the idea is to have only relative pathes and not absolute one - so when the underlying
		* structure changes the hashes won't change
		* all absolute pathes will get an unique id ($i) instead
		* so if you have two framework folders with the same relatives-pathes they still get different hashes
		*/
		$i = 0;
		foreach ($this->path_map as $k=>$v)
			if (++$i && ($dst = str_replace($k, $i, $path)) != $path)
				break;

		// now the path should be relative and we have an unique hash for it
		// now start with adding some human-readable path information to it
		$hash = sprintf('%x',crc32($dst.Yii::getVersion()));

		// dirname($dst) cause i don't want to have assets inside
		$humanReadable = ($this->human_readable)? $this->getShortPath(dirname($dst), $this->dir_max_length-strlen($hash)): '';
		return $humanReadable.$hash;
	}

	public function getShortPath($path, $maxLength)
	{
		// this function should create a path with a defined maximum length which should be human readable
		$path = ltrim($path, DIRECTORY_SEPARATOR); // remove the trailing /
		// remove all special chars and capitalize the next character
		$specialChar = array(',','.','_',' ');
		$lastSpecial = false;
		$newPath = '';
		for ($i = 0; $i< strlen($path); $i++)
		{
			if (in_array($path[$i], $specialChar))
			{
				$lastSpecial = true;
				continue;
			}
			if ($lastSpecial)
				$char = strtoupper($path[$i]);
			else
				$char = $path[$i];
			$newPath.=$path[$i];
		}

		// replace all / with _
		$path = str_replace('/','_',$newPath);

		if (($length = strlen($path)) < $maxLength)
			return $path;

		// the foldername deepest inside the hierarchy mostly has the highest information
		// i'm too lazy to think about a nice algorithm for it.. since my os doesn't have so big directory-name limitations i just substr it

		return substr($path, $maxLength);
	}
}
	
To setup this component you should add the following to your config:
	
'components'=>array(
	'assetManager'=>array(
		'class'=>'application.extensions.SimpleAssetManager', // important

		// following stuff is optional but worth looking at:
		'forceCopy'=>false, // for development it can be usefull to always copy the files

		// valid strategies:
		//	link: symlinking
		//	copy: copying
		//	keep: directly point to the asset-dir (you have to permit access to them inside htaccess
		'strategy'=>'copy',

		// path_map will map the pathes to urls (which is important for keep strategy)
		// they will map an absolute path to a directory to an url
		// you don't need to map your project-path this is done automatically..
		// if you use additional frameworks add them here too - the yii-framework must be added manually.. there is no way to obtain the
		// url automatically :/
		// YII_PATH is a constant which points to your yii-framework folder
		//'path_map'=>array(YII_PATH => 'http://127.0.0.1/yii/yii-dev/framework'), // is the same as the following
		'path_map'=>array(YII_PATH => '/yii/yii-dev/framework'),

		/* next idea with path_map is to map them to foreign hosts when you use the strategy 'keep':
			'path_map'=>array(YII_PATH=>'http://fast.example.com/staticData_fromFramework',
					// that complicated function is your projects folder
					dirname(Yii::app()->getRequest()->getScriptFile()).DIRECTORY_SEPARATOR=>'http://fast.example.com/staticData_fromMyProject'),
			note that your file will then still be inside the default_basepath - for debugging simply check the generated urls
		*/
		'default_basepath'=>'assets', // in general no need to touch this

		'human_readable'=>true, // use human readable directory names (the hashes will be simply appended)
		'dir_max_length'=>256, // cause the humanReadable dirnames can get very long..
	),
	..
	
I hope you still can find your way through the config.. the stuff isn't as complex as my comments are long.. I'm just bad at writing short descriptions;)
most stuff of it doesn't need to be set - look at the simpleassetmanager.php for the default values

Last but not least a small usage example.. it won't be very interesting since the basic api is the same.. i only stripped unneeded parameters from the publish function:
	
$this->baseUrl = Yii::app()->getAssetManager()->publish($dir);

$cs = Yii::app()->getClientScript();
$cs->registerScriptFile($this->baseUrl.'/jquery.jstree.js');