* @package Template
*/
class Horde_Template
{
/** The identifier to use for memory-only templates. */
const TEMPLATE_STRING = '**string';
/**
* The Horde_Cache object to use.
*
* @var Horde_Cache
*/
protected $_cache;
/**
* Logger.
*
* @var Horde_Log_Logger
*/
protected $_logger;
/**
* Option values.
*
* @var array
*/
protected $_options = array();
/**
* Directory that templates should be read from.
*
* @var string
*/
protected $_basepath = '';
/**
* Tag (scalar) values.
*
* @var array
*/
protected $_scalars = array();
/**
* Loop tag values.
*
* @var array
*/
protected $_arrays = array();
/**
* Path to template source.
*
* @var string
*/
protected $_templateFile = null;
/**
* Template source.
*
* @var string
*/
protected $_template = null;
/**
* Foreach variable mappings.
*
* @var array
*/
protected $_foreachMap = array();
/**
* Foreach variable incrementor.
*
* @var integer
*/
protected $_foreachVar = 0;
/**
* preg_match() cache.
*
* @var array
*/
protected $_pregcache = array();
/**
* Constructor.
*
* @param array $params The following configuration options:
*
* 'basepath' - (string) The directory where templates are read from.
* 'cacheob' - (Horde_Cache) A caching object used to cache the output.
* 'logger' - (Horde_Log_Logger) A logger object.
*
*/
public function __construct($params = array())
{
if (isset($params['basepath'])) {
$this->_basepath = $params['basepath'];
}
if (isset($params['cacheob'])) {
$this->_cache = $params['cacheob'];
}
if (isset($params['logger'])) {
$this->_logger = $params['logger'];
}
}
/**
* Sets an option.
* Currently available options are:
*
* 'debug' - Output debugging information to screen
* 'forcecompile' - Force a compilation on every page load
* 'gettext' - Activate gettext detection
*
*
* @param string $option The option name.
* @param mixed $val The option's value.
*/
public function setOption($option, $val)
{
$this->_options[$option] = $val;
}
/**
* Set the template contents to a string.
*
* @param string $template The template text.
*/
public function setTemplate($template)
{
$this->_template = $template;
$this->_parse();
$this->_templateFile = self::TEMPLATE_STRING;
}
/**
* Returns an option's value.
*
* @param string $option The option name.
*
* @return mixed The option's value.
*/
public function getOption($option)
{
return isset($this->_options[$option])
? $this->_options[$option]
: null;
}
/**
* Sets a tag, loop, or if variable.
*
* @param string|array $tag Either the tag name or a hash with tag names
* as keys and tag values as values.
* @param mixed $var The value to replace the tag with.
*/
public function set($tag, $var)
{
if (is_array($tag)) {
foreach ($tag as $tTag => $tVar) {
$this->set($tTag, $tVar);
}
} elseif (is_array($var)) {
$this->_arrays[$tag] = $var;
} else {
$this->_scalars[$tag] = (string) $var;
}
}
/**
* Returns the value of a tag or loop.
*
* @param string $tag The tag name.
*
* @return mixed The tag value or null if the tag hasn't been set yet.
*/
public function get($tag)
{
if (isset($this->_arrays[$tag])) {
return $this->_arrays[$tag];
}
if (isset($this->_scalars[$tag])) {
return $this->_scalars[$tag];
}
return null;
}
/**
* Fetches a template from the specified file and return the parsed
* contents.
*
* @param string $filename The file to fetch the template from.
*
* @return string The parsed template.
*/
public function fetch($filename = null)
{
$file = $this->_basepath . $filename;
$force = $this->getOption('forcecompile');
if (!is_null($filename) && ($file != $this->_templateFile)) {
$this->_template = $this->_templateFile = null;
}
/* First, check for a cached compiled version. */
$parts = array(
'horde_template',
filemtime($file),
$file
);
if ($this->getOption('gettext')) {
$parts[] = setlocale(LC_ALL, 0);
}
$cacheid = implode('|', $parts);
if (!$force && is_null($this->_template) && $this->_cache) {
$this->_template = $this->_cache->get($cacheid, 0);
if ($this->_template === false) {
$this->_template = null;
}
}
/* Parse and compile the template. */
if ($force || is_null($this->_template)) {
$this->_template = str_replace("\n", " \n", file_get_contents($file));
$this->_parse();
if ($this->_cache) {
$this->_cache->set($cacheid, $this->_template);
if ($this->_logger) {
$this->_logger->log(sprintf('Saved compiled template file for "%s".', $file), 'DEBUG');
}
}
}
$this->_templateFile = $file;
/* Template debugging. */
if ($this->getOption('debug')) {
echo '' . htmlspecialchars($this->_template) . '
';
}
return $this->parse();
}
/**
* Parses all variables/tags in the template.
*
* @param string $contents The unparsed template.
*
* @return string The parsed template.
*/
public function parse($contents = null)
{
if (!is_null($contents)) {
$this->setTemplate(str_replace("\n", " \n", $contents));
}
/* Evaluate the compiled template and return the output. */
ob_start();
eval('?>' . $this->_template);
return is_null($contents)
? ob_get_clean()
: str_replace(" \n", "\n", ob_get_clean());
}
/**
* Parses all variables/tags in the template.
*/
protected function _parse()
{
// Escape XML instructions.
$this->_template = preg_replace('/\?>|<\?/', '', $this->_template);
// Parse gettext tags, if the option is enabled.
if ($this->getOption('gettext')) {
$this->_parseGettext();
}
// Process ifs.
$this->_parseIf();
// Process loops and arrays.
$this->_parseLoop();
// Process base scalar tags. Needs to be after _parseLoop() as we
// rely on _foreachMap().
$this->_parseTags();
// Finally, process any associative array scalar tags.
$this->_parseAssociativeTags();
}
/**
* Parses gettext tags.
*
* @todo Convert to use Horde_Translation.
*/
protected function _parseGettext()
{
if (preg_match_all("/(.+?)<\/gettext>/s", $this->_template, $matches, PREG_SET_ORDER)) {
$replace = array();
foreach ($matches as $val) {
// eval gettext independently so we can embed tempate tags
$code = 'echo _(\'' . str_replace("'", "\\'", $val[1]) . '\');';
ob_start();
eval($code);
$replace[$val[0]] = ob_get_clean();
}
$this->_doReplace($replace);
}
}
/**
* Parses 'if' statements.
*
* @param string $key The key prefix to parse.
*/
protected function _parseIf($key = null)
{
$replace = array();
foreach ($this->_doSearch('if', $key) as $val) {
$replace[$val[0]] = '_generatePHPVar('scalars', $val[1]) . ') || !empty(' . $this->_generatePHPVar('arrays', $val[1]) . ')): ?>';
$replace[$val[2]] = '';
// Check for else statement.
foreach ($this->_doSearch('else', $key) as $val2) {
$replace[$val2[0]] = '';
$replace[$val2[2]] = '';
}
}
$this->_doReplace($replace);
}
/**
* Parses the given array for any loops or other uses of the array.
*
* @param string $key The key prefix to parse.
*/
protected function _parseLoop($key = null)
{
$replace = array();
foreach ($this->_doSearch('loop', $key) as $val) {
$divider = null;
// See if we have a divider.
if (preg_match("/(.*)<\/divider:" . $val[1] . ">/sU", $this->_template, $m)) {
$divider = $m[1];
$replace[$m[0]] = '';
}
if (!isset($this->_foreachMap[$val[1]])) {
$this->_foreachMap[$val[1]] = ++$this->_foreachVar;
}
$varId = $this->_foreachMap[$val[1]];
$var = $this->_generatePHPVar('arrays', $val[1]);
$replace[$val[0]] = '_generatePHPVar('arrays', $val[1]) . ' as $k' . $varId . ' => $v' . $varId . '): ?>';
$replace[$val[2]] = '';
// Parse ifs.
$this->_parseIf($val[1]);
// Parse interior loops.
$this->_parseLoop($val[1]);
// Replace scalars.
$this->_parseTags($val[1]);
}
$this->_doReplace($replace);
}
/**
* Replaces 'tag' tags with their PHP equivalents.
*
* @param string $key The key prefix to parse.
*/
protected function _parseTags($key = null)
{
$replace = array();
foreach ($this->_doSearch('tag', $key, true) as $val) {
$replace_text = '_foreachMap[$val[1]])) {
$var = $this->_foreachMap[$val[1]];
$replace_text .= 'if (isset($v' . $var . ')) { echo is_array($v' . $var . ') ? $k' . $var . ' : $v' . $var . '; } else';
}
$var = $this->_generatePHPVar('scalars', $val[1]);
$replace[$val[0]] = $replace_text . 'if (isset(' . $var . ')) { echo ' . $var . '; } ?>';
}
$this->_doReplace($replace);
}
/**
* Parse associative tags (i.e. ).
*/
protected function _parseAssociativeTags()
{
$replace = array();
foreach ($this->_pregcache['tag'] as $key => $val) {
$parts = explode('.', $val[1]);
$var = '$this->_arrays[\'' . $parts[0] . '\'][\'' . $parts[1] . '\']';
$replace[$val[0]] = '';
unset($this->_pregcache['tag'][$key]);
}
$this->_doReplace($replace);
}
/**
* Output the correct PHP variable string for use in template space.
*/
protected function _generatePHPVar($tag, $key)
{
$out = '';
$a = explode('.', $key);
$a_count = count($a);
if ($a_count == 1) {
switch ($tag) {
case 'arrays':
$out = '$this->_arrays';
break;
case 'scalars':
$out = '$this->_scalars';
break;
}
} else {
$out = '$v' . $this->_foreachMap[implode('.', array_slice($a, 0, -1))];
}
return $out . '[\'' . end($a) . '\']';
}
/**
* TODO
*/
protected function _doSearch($tag, $key, $noclose = false)
{
$out = array();
$level = (is_null($key)) ? 0 : substr_count($key, '.') + 1;
if (!isset($this->_pregcache[$key])) {
$regex = ($noclose) ?
"/<" . $tag . ":(.+?)\s\/>/" :
"/<" . $tag . ":([^>]+)>/";
preg_match_all($regex, $this->_template, $this->_pregcache[$tag], PREG_SET_ORDER);
}
foreach ($this->_pregcache[$tag] as $pkey => $val) {
$val_level = substr_count($val[1], '.');
$add = false;
if (is_null($key)) {
$add = !$val_level;
} else {
$add = (($val_level == $level) &&
(strpos($val[1], $key . '.') === 0));
}
if ($add) {
if (!$noclose) {
$val[2] = '' . $tag . ':' . $val[1] . '>';
}
$out[] = $val;
unset($this->_pregcache[$tag][$pkey]);
}
}
return $out;
}
/**
* TODO
*/
protected function _doReplace($replace)
{
if (empty($replace)) {
return;
}
$search = array();
foreach (array_keys($replace) as $val) {
$search[] = '/' . preg_quote($val, '/') . '/';
}
$this->_template = preg_replace($search, array_values($replace), $this->_template);
}
}