Update version.php to 3.3.0
[isso.git] / Template.php
1 <?php
2 /*=====================================================================*\
3 || ###################################################################
4 || # Blue Static ISSO Framework
5 || # Copyright (c)2005-2009 Blue Static
6 || #
7 || # This program is free software; you can redistribute it and/or modify
8 || # it under the terms of the GNU General Public License as published by
9 || # the Free Software Foundation; version 2 of the License.
10 || #
11 || # This program is distributed in the hope that it will be useful, but
12 || # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
13 || # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
14 || # more details.
15 || #
16 || # You should have received a copy of the GNU General Public License along
17 || # with this program; if not, write to the Free Software Foundation, Inc.,
18 || # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
19 || ###################################################################
20 \*=====================================================================*/
21
22 /**
23 * Database-Driven Template System (template.php)
24 *
25 * @package ISSO
26 */
27
28 require_once(ISSO . '/Functions.php');
29
30 /**
31 * File-Based Template System
32 *
33 * This framework merely replaces the template loading functions with
34 * file-system based ones. It has an optional caching system in which
35 * template data will remain stored in the database as long as the filesystem
36 * file is modified. To do this, pass a table name to setDatabaseCache() and make sure
37 * there's a DB module that has access to a table with this schema:
38 *
39 * CREATE TABLE template (filename VARCHAR (250) NOT NULL, template TEXT NOT NULL, timestamp INT NOT NULL);
40 *
41 * @author Blue Static
42 * @copyright Copyright (c)2005 - 2009, Blue Static
43 * @package ISSO
44 *
45 */
46 class BSTemplate
47 {
48 /**
49 * The name of a function that is called before template parsing of phrases and conditionals occurs
50 * @var string
51 */
52 public static $preParseHook = ':undefined:';
53
54 /**
55 * The database table name for the template cache
56 * @var string
57 */
58 public static $dbCacheTable = null;
59
60 /**
61 * The name of the function phrases are fetched with
62 * @var string
63 */
64 public static $langcall = 'gettext';
65
66 /**
67 * The template path pattern; for instance, this could be: ./templates/%s.tpl
68 * @var string
69 */
70 public static $templatePath = '%s';
71
72 /**
73 * Array of pre-compiled templates that are stored for optimization
74 * @var array
75 */
76 protected static $cache = array();
77
78 /**
79 * The name of the function phrases are sprintf() parsed with
80 * @var string
81 */
82 public static $langconst = 'sprintf';
83
84 /**
85 * Template variables to populate
86 * @var array
87 */
88 public $vars = array();
89
90 /**
91 * Global variables
92 * @var array
93 */
94 public static $globalVars = array();
95
96 /**
97 * The path of the template file
98 * @var string
99 */
100 protected $path;
101
102 /**
103 * The name of the template
104 * @var string
105 */
106 protected $name;
107
108 /**
109 * Template contents
110 * @var string
111 */
112 protected $template;
113
114 /**
115 * Takes an array of template names, loads them, and then stores a
116 * parsed version for optimum speed.
117 *
118 * @param array List of template names to be cached
119 */
120 public static function cache($namearray)
121 {
122 if (!self::$dbCacheTable)
123 {
124 return; // there's no point in pre-caching file templates
125 }
126
127 $namearray = array_map(array('self', '_path'), $namearray);
128 $cache = BSApp::$db->query("SELECT * FROM " . self::$dbCacheTable . " WHERE filename IN ('" . implode("', '", $namearray) . "')");
129 while ($tpl = $cache->fetchArray())
130 {
131 self::$cache[$tpl['filename']] = $tpl;
132 }
133 }
134
135 /**
136 * Fluent interface-compatible constructor
137 */
138 public static function fetch()
139 {
140 $obj = new ReflectionClass(__CLASS__);
141 $args = func_get_args();
142 return $obj->newInstanceArgs($args);
143 }
144
145 /**
146 * Constructor
147 *
148 * @param string File name
149 */
150 public function __construct($name)
151 {
152 $this->name = $name;
153 $this->path = self::_path($name);
154
155 // checks to see if the template has been cached
156 if (isset(self::$cache[$this->path]))
157 {
158 if (!self::$dbCacheTable || filemtime($this->path) <= self::$cache[$this->path]['timestamp'])
159 {
160 $this->template = self::$cache[$this->path]['template'];
161 return;
162 }
163 }
164
165 // it hasn't been cached
166 if (!is_file($this->path) || !is_readable($this->path))
167 {
168 throw new Exception("Could not load the template {$this->path}");
169 }
170 $this->template = $this->_parseTemplate(file_get_contents($this->path));
171 self::$cache[$this->path]['template'] = $this->template;
172
173 // store the template in the database
174 if (self::$dbCacheTable)
175 {
176 BSApp::$db->query("REPLACE INTO " . self::$dbCacheTable . " SET template = '" . BSApp::$db->escapeString($this->template) . "', timestamp = " . TIMENOW . ", filename = '" . $this->path . "'");
177 self::$cache[$this->path]['time'] = TIMENOW;
178 }
179 }
180
181 /**
182 * Evaluates and returns the template. This is equivalent to calling:
183 * $tpl->evaluate()->getTemplate()
184 *
185 * @return string
186 */
187 public function __toString()
188 {
189 return $this->evaluate()->getTemplate();
190 }
191
192 /**
193 * Returns the template data
194 *
195 * @return string Final template data
196 */
197 public function getTemplate()
198 {
199 return $this->template;
200 }
201
202 /**
203 * This function globalizes/extracts the assigned variables and then
204 * returns the output buffer
205 *
206 * @param string Unevaluated template
207 *
208 * @return fluent interface
209 */
210 public function evaluate()
211 {
212 extract($this->vars);
213 extract(self::$globalVars);
214
215 ob_start();
216 $this->template = str_replace(array('$this->', 'self::'), 'null', $this->template); // don't want internal access coming from a template
217 $this->template = '?>' . $this->template;
218 $test = eval($this->template);
219 $output = ob_get_clean();
220 if ($output === false)
221 {
222 throw new Exception('A parse error was encountered while evaluating the template');
223 }
224
225 $this->template = $output;
226
227 return $this;
228 }
229
230 /**
231 * Output a template fully compiled to the browser
232 */
233 public function flush()
234 {
235 ob_start();
236
237 if (empty($this->template))
238 {
239 throw new Exception('There is no output to print');
240 }
241
242 echo $this->template;
243 }
244
245 /**
246 * Returns the debug block
247 *
248 * @return string
249 */
250 public static function get_debug_block()
251 {
252 if (!BSApp::get_debug())
253 {
254 return;
255 }
256
257 $debugBlock = "\n<div align=\"center\">Executed in " . round(BSFunctions::fetch_microtime_diff('0 ' . $_SERVER['REQUEST_TIME']), 10) . ' seconds</div>';
258 $debugBlock .= "\n<br /><div align=\"center\">" . BSApp::get_debug_list() . "</div>";
259
260 if (BSApp::$db)
261 {
262 $queries = BSApp::$db->getHistory();
263
264 $debugBlock .= "<br />\n" . '<table cellpadding="4" cellspacing="1" border="0" align="center" width="30%" style="background-color: rgb(60, 60, 60); color: white">' . "\n\t" . '<tr><td><strong>Query Debug</strong></td></tr>';
265
266 foreach ($queries as $query)
267 {
268 $debugBlock .= "\n\t<tr style=\"background-color: rgb(230, 230, 230); color: black\">";
269 $debugBlock .= "\n\t\t<td>";
270 $debugBlock .= "\n\t\t\t$query[query]\n\n\t\t\t<div style=\"font-size: 9px;\">($query[time])</div>\n<!--\n$query[trace]\n-->\n\t\t</td>\n\t</tr>";
271 }
272
273 $debugBlock .= "\n</table>\n\n\n";
274 }
275
276 return $debugBlock;
277 }
278
279 /**
280 * A wrapper for all the parsing functions and compiling functins
281 *
282 * @param string Unparsed template data
283 *
284 * @return string Parsed template data
285 */
286 protected function _parseTemplate($template)
287 {
288 if (function_exists(self::$preParseHook))
289 {
290 $template = call_user_func(self::$preParseHook, $template, $this);
291 }
292
293 $template = $this->_parseTokens($template);
294 return $template;
295 }
296
297 /**
298 * Parses tokens <% %>
299 *
300 * @param string Template data
301 *
302 * @return string Parsed template data
303 */
304 protected function _parseTokens($template)
305 {
306 $stack = array();
307 $tokens = array();
308
309 for ($i = 0; $i < strlen($template); $i++)
310 {
311 // opening tag
312 if ($template[$i] == '<' && $template[$i + 1] == '%')
313 {
314 array_push($stack, $i);
315 }
316 // closing tag
317 else if ($template[$i] == '%' && $template[$i + 1] == '>')
318 {
319 // there's no stack, so it's a bad template
320 if (sizeof($stack) == 0)
321 {
322 throw new Exception('Malformed template data: unexpected closing substitution tag');
323 }
324 // we're good and nested
325 else if (sizeof($stack) == 1)
326 {
327 $open = array_pop($stack);
328 $echo = ($template[$open + 2] == '-' ? 'echo ' : '');
329 $replace = '<?php ' . $echo . BSFunctions::substring($template, $open + ($echo ? 3 : 2), $i) . ' ?>';
330 $template = substr_replace($template, $replace, $open, ($i + 2) - $open);
331 }
332 // just pop it off
333 else
334 {
335 array_pop($stack);
336 } // end else
337 } // end if
338 } // end for
339
340 return $template;
341 }
342
343 /**
344 * Returns the full path of a template given a name
345 *
346 * @param string Template name
347 *
348 * @return string Template path
349 */
350 protected static function _path($name)
351 {
352 return sprintf(self::$templatePath, $name);
353 }
354 }
355
356 ?>