+ Moved variables resolution in the compiler Parser. (Temp. solution until the new...
[haanga.git] / lib / Haanga.php
blob8ae8423d2c5ac3729747e89f4317869f96b5f182
1 <?php
2 /*
3 +---------------------------------------------------------------------------------+
4 | Copyright (c) 2010 César Rodas and Menéame Comunicacions S.L. |
5 +---------------------------------------------------------------------------------+
6 | Redistribution and use in source and binary forms, with or without |
7 | modification, are permitted provided that the following conditions are met: |
8 | 1. Redistributions of source code must retain the above copyright |
9 | notice, this list of conditions and the following disclaimer. |
10 | |
11 | 2. Redistributions in binary form must reproduce the above copyright |
12 | notice, this list of conditions and the following disclaimer in the |
13 | documentation and/or other materials provided with the distribution. |
14 | |
15 | 3. All advertising materials mentioning features or use of this software |
16 | must display the following acknowledgement: |
17 | This product includes software developed by César D. Rodas. |
18 | |
19 | 4. Neither the name of the César D. Rodas nor the |
20 | names of its contributors may be used to endorse or promote products |
21 | derived from this software without specific prior written permission. |
22 | |
23 | THIS SOFTWARE IS PROVIDED BY CÉSAR D. RODAS ''AS IS'' AND ANY |
24 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
25 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
26 | DISCLAIMED. IN NO EVENT SHALL CÉSAR D. RODAS BE LIABLE FOR ANY |
27 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
28 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
29 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
30 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
32 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE |
33 +---------------------------------------------------------------------------------+
34 | Authors: César Rodas <crodas@php.net> |
35 +---------------------------------------------------------------------------------+
38 if (!defined('HAANGA_VERSION')) {
39 /* anyone can override this value to force recompilation */
40 define('HAANGA_VERSION', '1.0.4');
44 /**
45 * Haanga Runtime class
47 * Simple class to call templates efficiently. This class aims
48 * to reduce the compilation of a template as less a possible. Also
49 * it will not load in memory the compiler, except when there is not
50 * cache (compiled template) or it is out-dated.
53 class Haanga
55 protected static $cache_dir;
56 protected static $templates_dir='.';
57 protected static $debug;
58 protected static $bootstrap = NULL;
59 protected static $check_ttl;
60 protected static $check_get;
61 protected static $check_set;
62 protected static $use_autoload = TRUE;
63 protected static $hash_filename = TRUE;
64 protected static $compiler = array();
66 public static $has_compiled;
68 private function __construct()
70 /* The class can't be instanced */
73 final public static function AutoLoad($class)
75 static $loaded = array();
76 static $path;
78 if (!isset($loaded[$class]) && substr($class, 0, 6) === 'Haanga' && !class_exists($class, false)) {
79 if ($path === NULL) {
80 $path = dirname(__FILE__);
82 $file = $path.DIRECTORY_SEPARATOR.str_replace('_', DIRECTORY_SEPARATOR, $class).'.php';
83 if (is_file($file)) {
84 require $file;
86 $loaded[$class] = TRUE;
87 return;
90 return FALSE;
93 public static function getTemplateDir()
95 return self::$templates_dir;
98 // configure(Array $opts) {{{
99 /**
100 * Configuration to load Haanga
102 * Options:
104 * - (string) cache_dir
105 * - (string) tempalte_dir
106 * - (callback) on_compile
107 * - (boolean) debug
108 * - (int) check_ttl
109 * - (callback) check_get
110 * - (callback) check_set
111 * - (boolean) autoload
112 * - (boolean) use_hash_filename
114 * @return void
116 final public static function configure(Array $opts)
118 foreach ($opts as $option => $value) {
119 switch (strtolower($option)) {
120 case 'cache_dir':
121 self::$cache_dir = $value;
122 break;
123 case 'template_dir':
124 self::$templates_dir = $value;
125 break;
126 case 'bootstrap':
127 if (is_callable($value)) {
128 self::$bootstrap = $value;
130 break;
131 case 'debug':
132 self::enableDebug((bool)$value);
133 break;
134 case 'check_ttl':
135 self::$check_ttl = (int)$value;
136 break;
137 case 'check_get':
138 if (is_callable($value)) {
139 self::$check_get = $value;
141 break;
142 case 'check_set':
143 if (is_callable($value)) {
144 self::$check_set = $value;
146 break;
147 case 'autoload':
148 self::$use_autoload = (bool)$value;
149 break;
150 case 'use_hash_filename':
151 self::$hash_filename = (bool)$value;
152 break;
153 case 'compiler':
154 if (is_array($value)) {
155 self::$compiler = $value;
157 break;
158 default:
159 continue;
163 // }}}
165 // checkCacheDir(string $dir) {{{
167 * Check the directory where the compiled templates
168 * are stored.
170 * @param string $dir
172 * @return void
174 public static function checkCacheDir()
176 $dir = self::$cache_dir;
177 if (!is_dir($dir)) {
178 $old = umask(0);
179 if (!mkdir($dir, 0777, TRUE)) {
180 throw new Haanga_Exception("{$dir} is not a valid directory");
182 umask($old);
184 if (!is_writable($dir)) {
185 throw new Haanga_Exception("{$dir} can't be written");
188 // }}}
190 // enableDebug($bool) {{{
191 public static function enableDebug($bool)
193 self::$debug = $bool;
195 // }}}
197 // getCompiler($checkdir=TRUE) {{{
199 * This function is a singleton for the Haanga_Compiler_Runtime class.
200 * The instance is already set up properly and resetted.
203 * @param bool $checkdir TRUE
205 * @return Haanga_Compiler_Runtime
207 protected static function getCompiler($checkdir=TRUE)
209 static $compiler;
210 static $has_checkdir = FALSE;
212 if (!$compiler) {
214 /* Load needed files (to avoid autoload as much as possible) */
215 $dir = dirname(__FILE__);
216 require_once "{$dir}/Haanga/AST.php";
217 require_once "{$dir}/Haanga/Compiler.php";
218 require_once "{$dir}/Haanga/Compiler/Runtime.php";
219 require_once "{$dir}/Haanga/Compiler/Parser.php";
220 require_once "{$dir}/Haanga/Compiler/Tokenizer.php";
221 require_once "{$dir}/Haanga/Generator/PHP.php";
222 require_once "{$dir}/Haanga/Extension.php";
223 require_once "{$dir}/Haanga/Extension/Filter.php";
224 require_once "{$dir}/Haanga/Extension/Tag.php";
226 /* load compiler (done just once) */
227 if (self::$use_autoload) {
228 spl_autoload_register(array(__CLASS__, 'AutoLoad'));
231 $compiler = new Haanga_Compiler_Runtime;
233 if (self::$bootstrap) {
234 /* call bootstrap hook, just the first time */
235 call_user_func(self::$bootstrap);
238 if (count(self::$compiler) != 0) {
239 foreach (self::$compiler as $opt => $value) {
240 Haanga_Compiler::setOption($opt, $value);
246 if ($checkdir && !$has_checkdir) {
247 self::checkCacheDir();
248 $has_checkdir = TRUE;
251 $compiler->reset();
252 return $compiler;
254 // }}}
256 // callback compile(string $tpl, $context=array()) {{{
258 * Compile one template and return a PHP function
260 * @param string $tpl Template body
261 * @param array $context Context variables useful to generate efficient code (for array, objects and array)
263 * @return callback($vars=array(), $return=TRUE, $block=array())
265 public static function compile($tpl, $context=array())
267 $compiler = self::getCompiler(FALSE);
269 foreach ($context as $var => $value) {
270 $compiler->set_context($var, $value);
273 $code = $compiler->compile($tpl);
275 return create_function('$vars=array(), $return=TRUE, $blocks=array()', $code);
277 // }}}
279 // safe_load(string $file, array $vars, bool $return, array $blocks) {{{
280 public static function Safe_Load($file, $vars = array(), $return=FALSE, $blocks=array())
282 try {
284 $tpl = self::$templates_dir.'/'.$file;
285 if (file_exists($tpl)) {
286 /* call load if the tpl file exists */
287 return self::Load($file, $vars, $return, $blocks);
289 } Catch (Exception $e) {
291 /* some error but we don't care at all */
292 return "";
294 // }}}
296 // load(string $file, array $vars, bool $return, array $blocks) {{{
298 * Load
300 * Load template. If the template is already compiled, just the compiled
301 * PHP file will be included an used. If the template is new, or it
302 * had changed, the Haanga compiler is loaded in memory, and the template
303 * is compiled.
306 * @param string $file
307 * @param array $vars
308 * @param bool $return
309 * @param array $blocks
311 * @return string|NULL
313 public static function Load($file, $vars = array(), $return=FALSE, $blocks=array())
315 if (empty(self::$cache_dir)) {
316 throw new Haanga_Exception("Cache dir or template dir is missing");
319 self::$has_compiled = FALSE;
321 $tpl = self::$templates_dir.'/'.$file;
322 $fnc = sha1($tpl);
323 $callback = "haanga_".$fnc;
325 if (is_callable($callback)) {
326 return $callback($vars, $return, $blocks);
329 $php = self::$hash_filename ? $fnc : $file;
330 $php = self::$cache_dir.'/'.$php.'.php';
332 $check = TRUE;
334 if (self::$check_ttl && self::$check_get && self::$check_set) {
335 /* */
336 if (call_user_func(self::$check_get, $callback)) {
337 /* disable checking for the next $check_ttl seconds */
338 $check = FALSE;
339 } else {
340 $result = call_user_func(self::$check_set, $callback, TRUE, self::$check_ttl);
344 if (!is_file($php) || ($check && filemtime($tpl) > filemtime($php))) {
346 if (!is_file($tpl)) {
347 /* There is no template nor compiled file */
348 throw new Exception("View {$file} doesn't exists");
351 if (!is_dir(dirname($php))) {
352 $old = umask(0);
353 mkdir(dirname($php), 0777, TRUE);
354 umask($old);
357 $fp = fopen($php, "a+");
358 /* try to block PHP file */
359 if (!flock($fp, LOCK_EX | LOCK_NB)) {
360 /* couldn't block, another process is already compiling */
361 fclose($fp);
362 if (is_file($php)) {
364 ** if there is an old version of the cache
365 ** load it
367 require $php;
368 if (is_callable($callback)) {
369 return $callback($vars, $return, $blocks);
373 ** no luck, probably the template is new
374 ** the compilation will be done, but we won't
375 ** save it (we'll use eval instead)
377 unset($fp);
380 /* recompile */
381 $compiler = self::getCompiler();
383 if (self::$debug) {
384 $compiler->setDebug($php.".dump");
387 try {
388 $code = $compiler->compile_file($tpl, FALSE, $vars);
389 } catch (Exception $e) {
390 if (isset($fp)) {
392 ** set the $php file as old (to force future
393 ** recompilation)
395 touch($php, 300, 300);
396 chmod($php, 0777);
398 /* re-throw exception */
399 throw $e;
402 if (isset($fp)) {
403 ftruncate($fp, 0); // truncate file
404 fwrite($fp, "<?php".$code);
405 flock($fp, LOCK_UN); // release the lock
406 fclose($fp);
407 } else {
408 /* local eval */
409 eval($code);
412 self::$has_compiled = TRUE;
415 if (!is_callable($callback)) {
416 /* Load the cached PHP file */
417 require $php;
418 if (!is_callable($callback)) {
420 really weird case ($php is empty, another process is compiling
421 the $tpl for the first time), so create a lambda function
422 for the template
424 $lambda= self::compile(file_get_contents($tpl), $vars);
425 return $lambda($vars, $return, $blocks);
429 if (!isset($HAANGA_VERSION) || $HAANGA_VERSION != HAANGA_VERSION) {
430 touch($php, 300, 300);
431 chmod($php, 0777);
434 return $callback($vars, $return, $blocks);
436 // }}}
441 * Local variables:
442 * tab-width: 4
443 * c-basic-offset: 4
444 * End:
445 * vim600: sw=4 ts=4 fdm=marker
446 * vim<600: sw=4 ts=4