Overview

Packages

  • Template

Classes

  • Horde_Template
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Horde Template system. Adapted from bTemplate by Brian Lozier
  4:  * <brian@massassi.net>.
  5:  *
  6:  * Horde_Template provides a basic template engine with tags, loops,
  7:  * and if conditions. However, it is also a simple interface with
  8:  * several essential functions: set(), fetch(), and
  9:  * parse(). Subclasses or decorators can implement (or delegate) these
 10:  * three methods, plus the options api, and easily implement other
 11:  * template engines (PHP code, XSLT, etc.) without requiring usage
 12:  * changes.
 13:  *
 14:  * Compilation code adapted from code written by Bruno Pedro <bpedro@ptm.pt>.
 15:  *
 16:  * Copyright 2002-2012 Horde LLC (http://www.horde.org/)
 17:  *
 18:  * See the enclosed file COPYING for license information (LGPL). If you
 19:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
 20:  *
 21:  * @author  Chuck Hagenbuch <chuck@horde.org>
 22:  * @author  Michael Slusarz <slusarz@horde.org>
 23:  * @package Template
 24:  */
 25: class Horde_Template
 26: {
 27:     /** The identifier to use for memory-only templates. */
 28:     const TEMPLATE_STRING = '**string';
 29: 
 30:     /**
 31:      * The Horde_Cache object to use.
 32:      *
 33:      * @var Horde_Cache
 34:      */
 35:     protected $_cache;
 36: 
 37:     /**
 38:      * Logger.
 39:      *
 40:      * @var Horde_Log_Logger
 41:      */
 42:     protected $_logger;
 43: 
 44:     /**
 45:      * Option values.
 46:      *
 47:      * @var array
 48:      */
 49:     protected $_options = array();
 50: 
 51:     /**
 52:      * Directory that templates should be read from.
 53:      *
 54:      * @var string
 55:      */
 56:     protected $_basepath = '';
 57: 
 58:     /**
 59:      * Tag (scalar) values.
 60:      *
 61:      * @var array
 62:      */
 63:     protected $_scalars = array();
 64: 
 65:     /**
 66:      * Loop tag values.
 67:      *
 68:      * @var array
 69:      */
 70:     protected $_arrays = array();
 71: 
 72:     /**
 73:      * Path to template source.
 74:      *
 75:      * @var string
 76:      */
 77:     protected $_templateFile = null;
 78: 
 79:     /**
 80:      * Template source.
 81:      *
 82:      * @var string
 83:      */
 84:     protected $_template = null;
 85: 
 86:     /**
 87:      * Foreach variable mappings.
 88:      *
 89:      * @var array
 90:      */
 91:     protected $_foreachMap = array();
 92: 
 93:     /**
 94:      * Foreach variable incrementor.
 95:      *
 96:      * @var integer
 97:      */
 98:     protected $_foreachVar = 0;
 99: 
100:     /**
101:      * preg_match() cache.
102:      *
103:      * @var array
104:      */
105:     protected $_pregcache = array();
106: 
107:     /**
108:      * Constructor.
109:      *
110:      * @param array $params  The following configuration options:
111:      * <pre>
112:      * 'basepath' - (string) The directory where templates are read from.
113:      * 'cacheob' - (Horde_Cache) A caching object used to cache the output.
114:      * 'logger' - (Horde_Log_Logger) A logger object.
115:      * </pre>
116:      */
117:     public function __construct($params = array())
118:     {
119:         if (isset($params['basepath'])) {
120:             $this->_basepath = $params['basepath'];
121:         }
122: 
123:         if (isset($params['cacheob'])) {
124:             $this->_cache = $params['cacheob'];
125:         }
126: 
127:         if (isset($params['logger'])) {
128:             $this->_logger = $params['logger'];
129:         }
130:     }
131: 
132:     /**
133:      * Sets an option.
134:      * Currently available options are:
135:      * <pre>
136:      * 'debug' - Output debugging information to screen
137:      * 'forcecompile' - Force a compilation on every page load
138:      * 'gettext' - Activate gettext detection
139:      * <pre>
140:      *
141:      * @param string $option  The option name.
142:      * @param mixed $val      The option's value.
143:      */
144:     public function setOption($option, $val)
145:     {
146:         $this->_options[$option] = $val;
147:     }
148: 
149:     /**
150:      * Set the template contents to a string.
151:      *
152:      * @param string $template  The template text.
153:      */
154:     public function setTemplate($template)
155:     {
156:         $this->_template = $template;
157:         $this->_parse();
158:         $this->_templateFile = self::TEMPLATE_STRING;
159:     }
160: 
161:     /**
162:      * Returns an option's value.
163:      *
164:      * @param string $option  The option name.
165:      *
166:      * @return mixed  The option's value.
167:      */
168:     public function getOption($option)
169:     {
170:         return isset($this->_options[$option])
171:             ? $this->_options[$option]
172:             : null;
173:     }
174: 
175:     /**
176:      * Sets a tag, loop, or if variable.
177:      *
178:      * @param string|array $tag   Either the tag name or a hash with tag names
179:      *                            as keys and tag values as values.
180:      * @param mixed        $var   The value to replace the tag with.
181:      */
182:     public function set($tag, $var)
183:     {
184:         if (is_array($tag)) {
185:             foreach ($tag as $tTag => $tVar) {
186:                 $this->set($tTag, $tVar);
187:             }
188:         } elseif (is_array($var)) {
189:             $this->_arrays[$tag] = $var;
190:         } else {
191:             $this->_scalars[$tag] = (string) $var;
192:         }
193:     }
194: 
195:     /**
196:      * Returns the value of a tag or loop.
197:      *
198:      * @param string $tag  The tag name.
199:      *
200:      * @return mixed  The tag value or null if the tag hasn't been set yet.
201:      */
202:     public function get($tag)
203:     {
204:         if (isset($this->_arrays[$tag])) {
205:             return $this->_arrays[$tag];
206:         }
207:         if (isset($this->_scalars[$tag])) {
208:             return $this->_scalars[$tag];
209:         }
210:         return null;
211:     }
212: 
213:     /**
214:      * Fetches a template from the specified file and return the parsed
215:      * contents.
216:      *
217:      * @param string $filename  The file to fetch the template from.
218:      *
219:      * @return string  The parsed template.
220:      */
221:     public function fetch($filename = null)
222:     {
223:         $file = $this->_basepath . $filename;
224:         $force = $this->getOption('forcecompile');
225: 
226:         if (!is_null($filename) && ($file != $this->_templateFile)) {
227:             $this->_template = $this->_templateFile = null;
228:         }
229: 
230:         /* First, check for a cached compiled version. */
231:         $parts = array(
232:             'horde_template',
233:             filemtime($file),
234:             $file
235:         );
236:         if ($this->getOption('gettext')) {
237:             $parts[] = setlocale(LC_ALL, 0);
238:         }
239:         $cacheid = implode('|', $parts);
240: 
241:         if (!$force && is_null($this->_template) && $this->_cache) {
242:             $this->_template = $this->_cache->get($cacheid, 0);
243:             if ($this->_template === false) {
244:                 $this->_template = null;
245:             }
246:         }
247: 
248:         /* Parse and compile the template. */
249:         if ($force || is_null($this->_template)) {
250:             $this->_template = str_replace("\n", " \n", file_get_contents($file));
251:             $this->_parse();
252:             if ($this->_cache) {
253:                 $this->_cache->set($cacheid, $this->_template);
254:                 if ($this->_logger) {
255:                     $this->_logger->log(sprintf('Saved compiled template file for "%s".', $file), 'DEBUG');
256:                 }
257:             }
258:         }
259: 
260:         $this->_templateFile = $file;
261: 
262:         /* Template debugging. */
263:         if ($this->getOption('debug')) {
264:             echo '<pre>' . htmlspecialchars($this->_template) . '</pre>';
265:         }
266: 
267:         return $this->parse();
268:     }
269: 
270:     /**
271:      * Parses all variables/tags in the template.
272:      *
273:      * @param string $contents  The unparsed template.
274:      *
275:      * @return string  The parsed template.
276:      */
277:     public function parse($contents = null)
278:     {
279:         if (!is_null($contents)) {
280:             $this->setTemplate(str_replace("\n", " \n", $contents));
281:         }
282: 
283:         /* Evaluate the compiled template and return the output. */
284:         ob_start();
285:         eval('?>' . $this->_template);
286:         return is_null($contents)
287:             ? ob_get_clean()
288:             : str_replace(" \n", "\n", ob_get_clean());
289:     }
290: 
291:     /**
292:      * Parses all variables/tags in the template.
293:      */
294:     protected function _parse()
295:     {
296:         // Escape XML instructions.
297:         $this->_template = preg_replace('/\?>|<\?/', '<?php echo \'$0\' ?>', $this->_template);
298: 
299:         // Parse gettext tags, if the option is enabled.
300:         if ($this->getOption('gettext')) {
301:             $this->_parseGettext();
302:         }
303: 
304:         // Process ifs.
305:         $this->_parseIf();
306: 
307:         // Process loops and arrays.
308:         $this->_parseLoop();
309: 
310:         // Process base scalar tags.  Needs to be after _parseLoop() as we
311:         // rely on _foreachMap().
312:         $this->_parseTags();
313: 
314:         // Finally, process any associative array scalar tags.
315:         $this->_parseAssociativeTags();
316:     }
317: 
318:     /**
319:      * Parses gettext tags.
320:      *
321:      * @todo Convert to use Horde_Translation.
322:      */
323:     protected function _parseGettext()
324:     {
325:         if (preg_match_all("/<gettext>(.+?)<\/gettext>/s", $this->_template, $matches, PREG_SET_ORDER)) {
326:             $replace = array();
327:             foreach ($matches as $val) {
328:                 // eval gettext independently so we can embed tempate tags
329:                 $code = 'echo _(\'' . str_replace("'", "\\'", $val[1]) . '\');';
330:                 ob_start();
331:                 eval($code);
332:                 $replace[$val[0]] = ob_get_clean();
333:             }
334: 
335:             $this->_doReplace($replace);
336:         }
337:     }
338: 
339:     /**
340:      * Parses 'if' statements.
341:      *
342:      * @param string $key  The key prefix to parse.
343:      */
344:     protected function _parseIf($key = null)
345:     {
346:         $replace = array();
347: 
348:         foreach ($this->_doSearch('if', $key) as $val) {
349:             $replace[$val[0]] = '<?php if (!empty(' . $this->_generatePHPVar('scalars', $val[1]) . ') || !empty(' . $this->_generatePHPVar('arrays', $val[1]) . ')): ?>';
350:             $replace[$val[2]] = '<?php endif; ?>';
351: 
352:             // Check for else statement.
353:             foreach ($this->_doSearch('else', $key) as $val2) {
354:                 $replace[$val2[0]] = '<?php else: ?>';
355:                 $replace[$val2[2]] = '';
356:             }
357:         }
358: 
359:         $this->_doReplace($replace);
360:     }
361: 
362:     /**
363:      * Parses the given array for any loops or other uses of the array.
364:      *
365:      * @param string $key  The key prefix to parse.
366:      */
367:     protected function _parseLoop($key = null)
368:     {
369:         $replace = array();
370: 
371:         foreach ($this->_doSearch('loop', $key) as $val) {
372:             $divider = null;
373: 
374:             // See if we have a divider.
375:             if (preg_match("/<divider:" . $val[1] . ">(.*)<\/divider:" . $val[1] . ">/sU", $this->_template, $m)) {
376:                 $divider = $m[1];
377:                 $replace[$m[0]] = '';
378:             }
379: 
380:             if (!isset($this->_foreachMap[$val[1]])) {
381:                 $this->_foreachMap[$val[1]] = ++$this->_foreachVar;
382:             }
383:             $varId = $this->_foreachMap[$val[1]];
384:             $var = $this->_generatePHPVar('arrays', $val[1]);
385: 
386:             $replace[$val[0]] = '<?php ' .
387:                 (($divider) ? '$i' . $varId . ' = count(' . $var . '); ' : '') .
388:                 'foreach (' . $this->_generatePHPVar('arrays', $val[1]) . ' as $k' . $varId . ' => $v' . $varId . '): ?>';
389:             $replace[$val[2]] = '<?php ' .
390:                 (($divider) ? 'if (--$i' . $varId . ' != 0) { echo \'' . $divider . '\'; }; ' : '') .
391:                 'endforeach; ?>';
392: 
393:             // Parse ifs.
394:             $this->_parseIf($val[1]);
395: 
396:             // Parse interior loops.
397:             $this->_parseLoop($val[1]);
398: 
399:             // Replace scalars.
400:             $this->_parseTags($val[1]);
401:         }
402: 
403:         $this->_doReplace($replace);
404:     }
405: 
406:     /**
407:      * Replaces 'tag' tags with their PHP equivalents.
408:      *
409:      * @param string $key  The key prefix to parse.
410:      */
411:     protected function _parseTags($key = null)
412:     {
413:         $replace = array();
414: 
415:         foreach ($this->_doSearch('tag', $key, true) as $val) {
416:             $replace_text = '<?php ';
417:             if (isset($this->_foreachMap[$val[1]])) {
418:                 $var = $this->_foreachMap[$val[1]];
419:                 $replace_text .= 'if (isset($v' . $var . ')) { echo is_array($v' . $var . ') ? $k' . $var . ' : $v' . $var . '; } else';
420:             }
421:             $var = $this->_generatePHPVar('scalars', $val[1]);
422:             $replace[$val[0]] = $replace_text . 'if (isset(' . $var . ')) { echo ' . $var . '; } ?>';
423:         }
424: 
425:         $this->_doReplace($replace);
426:     }
427: 
428:     /**
429:      * Parse associative tags (i.e. <tag:foo.bar />).
430:      */
431:     protected function _parseAssociativeTags()
432:     {
433:         $replace = array();
434: 
435:         foreach ($this->_pregcache['tag'] as $key => $val) {
436:             $parts = explode('.', $val[1]);
437:             $var = '$this->_arrays[\'' . $parts[0] . '\'][\'' . $parts[1] . '\']';
438:             $replace[$val[0]] = '<?php if (isset(' . $var . ')) { echo ' . $var . '; } ?>';
439:             unset($this->_pregcache['tag'][$key]);
440:         }
441: 
442:         $this->_doReplace($replace);
443:     }
444: 
445:     /**
446:      * Output the correct PHP variable string for use in template space.
447:      */
448:     protected function _generatePHPVar($tag, $key)
449:     {
450:         $out = '';
451: 
452:         $a = explode('.', $key);
453:         $a_count = count($a);
454: 
455:         if ($a_count == 1) {
456:             switch ($tag) {
457:             case 'arrays':
458:                 $out = '$this->_arrays';
459:                 break;
460: 
461:             case 'scalars':
462:                 $out = '$this->_scalars';
463:                 break;
464:             }
465:         } else {
466:             $out = '$v' . $this->_foreachMap[implode('.', array_slice($a, 0, -1))];
467:         }
468: 
469:         return $out . '[\'' . end($a) . '\']';
470:     }
471: 
472:     /**
473:      * TODO
474:      */
475:     protected function _doSearch($tag, $key, $noclose = false)
476:     {
477:         $out = array();
478:         $level = (is_null($key)) ? 0 : substr_count($key, '.') + 1;
479: 
480:         if (!isset($this->_pregcache[$key])) {
481:             $regex = ($noclose) ?
482:                 "/<" . $tag . ":(.+?)\s\/>/" :
483:                 "/<" . $tag . ":([^>]+)>/";
484:             preg_match_all($regex, $this->_template, $this->_pregcache[$tag], PREG_SET_ORDER);
485:         }
486: 
487:         foreach ($this->_pregcache[$tag] as $pkey => $val) {
488:             $val_level = substr_count($val[1], '.');
489:             $add = false;
490:             if (is_null($key)) {
491:                 $add = !$val_level;
492:             } else {
493:                 $add = (($val_level == $level) &&
494:                         (strpos($val[1], $key . '.') === 0));
495:             }
496:             if ($add) {
497:                 if (!$noclose) {
498:                     $val[2] = '</' . $tag . ':' . $val[1] . '>';
499:                 }
500:                 $out[] = $val;
501:                 unset($this->_pregcache[$tag][$pkey]);
502:             }
503:         }
504: 
505:         return $out;
506:     }
507: 
508:     /**
509:      * TODO
510:      */
511:     protected function _doReplace($replace)
512:     {
513:         if (empty($replace)) {
514:             return;
515:         }
516: 
517:         $search = array();
518: 
519:         foreach (array_keys($replace) as $val) {
520:             $search[] = '/' . preg_quote($val, '/') . '/';
521:         }
522: 
523:         $this->_template = preg_replace($search, array_values($replace), $this->_template);
524:     }
525: 
526: }
527: 
API documentation generated by ApiGen