Monthly Archive for December, 2007

Smarty Templates

We began working on the next version of our software recently, and one of the things we wanted to include was a templating system. We decided to use Smarty, and since I have some experience with it I took the lead on that. Shortly after everything got rolling we decided Smarty wasn’t going to work for us and switched to something homemade.

Guess I’ll start at the beginning. We build eCommerce software. Shopping carts and custom designs. Our software has a sort of global object which contains references to all the big objects we use, like the merchant object, the db object, the product object, and the smarty object. We use this function to set up smarty…

function smartySetup(&$mcObj) {
 
	require_once '/usr/local/share/smarty/Smarty.class.php';
 
	$smarty = new smarty;
 
	$smarty->compile_dir  = $mcObj->MERCHANT->private_path . 'smarty/templates_c';
	$smarty->function_dir = '/usr/local/domains/php/' . MC_FOLDER . '/smarty/functions';
 
	$smarty->default_template_handler_func = 'smartyFindTemplate';
 
 
	$smarty->funcs_loaded = array();
 
	// put the smarty object in our mcObj
	// so that everybody can use it
	$mcObj->SMARTY =& $smarty;
	$smarty->OBJ =& $mcObj;
 
	// load some "all purpose" variables that don't come from any class
 
	smartyLoadGoodies($smarty);
 
} // end smartySetup

The sets up some things. Smarty is going to put it’s compiled/cached templates in the merchant’s directory on the web server. We have a template handler function that I’ll get into later. A little later on we call another function…

function smartySetupObjects(&$mcObj, $debug = false) {
 
	$done = array();
 
	foreach ($mcObj as $key => $obj) {
		$objName = strtoupper(get_class($obj));
 
		if ($debug) { echo $objName . ' = ' . $key . ''; }
		if (!in_array($objName, $done)) {
			if (($objName != 'SMARTY')
					&& ($objName != 'DB') 
					&& ($objName != 'MEMCACHE')
					&& ($objName != '')) {
 
 
				$mcObj->SMARTY->assign_by_ref($objName, $mcObj->{$objName});
				$done[] = $objName;
			}
		}
 
	} // end foreach object
 
} // end smartySetupObjects

That loops through everything in our global object and makes references to some things, so that they can be used really easily in the templates. You’d be able to access the merchant object with $MERCHANT but you can’t use the db object.

Smarty let’s you develop your own functions. I did a bunch of these so that our designers could just call a function for simple, repetitive tasks. I had them organized so that any functions for the product object would be in a file called smarty_product.php. All the functions in it would be called smarty_product_function_name(). Then, in the constructor of any given object, you’d call smartyLoadFuncs() and all of the custom functions we’d written for that object would get loaded. Here’s the code for that function…

function smartyLoadFuncs(&$mcObj, $funcs) {
 
	if (!is_array($funcs)) { $funcs = array($funcs); }
 
	foreach ($funcs as $func) {
 
		$funcfile = $mcObj->SMARTY->function_dir . '/smarty_' . $func . '.php';
 
		if (!file_exists($funcfile)) {
			continue; 
 
		}
 
		// let's not load these functions more than once
		if (in_array($func, $mcObj->SMARTY->funcs_loaded)) {
			continue;
		} else {
			$mcObj->SMARTY->funcs_loaded[] = $func;
		}
 
		include $funcfile;
 
 
		// find out what the functions are called
		$funcsToLoad = array();
		$f = split(chr(10), file_get_contents($funcfile));
		foreach ($f as $line) {
			$pos = strpos($line, 'function smarty_' . $func . '_');
			if ($pos !== false) {
				$getfrom = $pos + strlen('function ');
				$getstop = strpos($line, '(', $pos);
 
				$funcphp = trim(substr($line, $getfrom, $getstop - $getfrom));
				$funcsm  = ereg_replace('smarty_', '', $funcphp);
				$funcsToLoad[] = array('php' => $funcphp, 'smarty' => $funcsm);
			}
		}
 
 
		// register them with smarty
		foreach ($funcsToLoad as $funcToLoad) {
			showme($funcToLoad, 'Loading smarty functions for ' . $func);
 
			$mcObj->SMARTY->register_function($funcToLoad['smarty'], $funcToLoad['php']);
		}
 
 
	} // end foreach functionset to load
 
} // end smartyLoadFuncs

I’m not really a big fan of how Smarty handles the passing of parameters to functions. They seem really ambiguous and arbitrary. And I thought it was hard to handle them in our custom functions, so I wrote a function to do it. The idea was that you would pass it an array of default values and it would return an array of actual values, which you could then list() and use like a real parameter. I’m not terribly happy with this solution but like I said, the whole thing didn’t last long enough for me to improve upon.

function smartyParseParams($params, $defaults) {
 
	$p = array();
 
 
	foreach ($defaults as $var => $val) {
		if (array_key_exists($var, $params)) {
			$p[] = $params[$var];
		} else {
			$p[] = $val;
		}
	}
 
 
	return $p;
 
} // end smartyParseParams

Alright, so here’s our custom template handler function. This looks for the template in the merchant’s directories first. If it doesn’t find it there, it uses the one from the codebase. This means that any template can be overridden or customized as necessary.

function smartyFindTemplate($resource_type, $resource_name, &$template_source, &$template_timestamp, &$smarty) {
 
	$found = false;
 
	// okay, look in the merchant's directory first
 
	$merchant = $smarty->OBJ->MERCHANT;
	if (is_object($merchant) && isset($merchant)) {
		$template_path = $merchant->public_path . 'smarty/templates/' . $resource_name;
		if (file_exists($template_path)) {
			$template_source = file_get_contents($template_path);
			$found = true;
		}
 
 
	}
 
 
	// look in mountain commerce for it (maybe)
	if (!$found) {
		$template_path = '/usr/local/domains/php/' . MC_FOLDER . '/smarty/templates/' . $resource_name;
		if (file_exists($template_path)) {
			$template_source = file_get_contents($template_path);
			$found = true;
		}
 
	}
 
	if (!$found) {
#		mcNotice('Could not find template: ' . $template_path);
	}
 
	// i'm going to return true all the time
	// so that even if it can't find the template, no error is thrown to the user
	return true;
 
} // end smartyFindTemplate

You can check out the small sampling of custom smarty functions I was able to develop here.