LightweightTemplate.php 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <?php
  2. namespace KarmaFW\Templates;
  3. use \KarmaFW\App;
  4. class LightweightTemplate {
  5. // https://codeshack.io/lightweight-template-engine-php/
  6. static $blocks = array();
  7. static $cache_path = TPL_CACHE_DIR; // APP_DIR . '/var/cache/templates';
  8. static $tpl_path = TPL_DIR;
  9. static $cache_enabled = (ENV == 'prod') || true;
  10. static $tpl_last_updated = null;
  11. protected $data = [];
  12. public function __construct($tpl_path=null, $variables=[], $layout=null)
  13. {
  14. $this->data = $variables;
  15. }
  16. public function assign($k, $v=null)
  17. {
  18. if (is_array($k)) {
  19. $keys = $k;
  20. foreach ($keys as $k => $v) {
  21. $this->assign($k, $v);
  22. }
  23. } else {
  24. $this->data[$k] = $v;
  25. }
  26. }
  27. public function getVariables()
  28. {
  29. return $this->data;
  30. }
  31. public function getVar($var_name, $default_value=null)
  32. {
  33. return isset($this->data[$var_name]) ? $this->data[$var_name] : $default_value;
  34. }
  35. public function fetch($tpl=null, $extra_vars=[], $layout=null, $options=[])
  36. {
  37. ob_start();
  38. $this->display($tpl, $extra_vars, $layout, $options);
  39. $content = ob_get_contents();
  40. ob_end_clean();
  41. return $content;
  42. }
  43. public function display($tpl=null, $extra_vars=[], $layout=null, $options=[])
  44. {
  45. $tpl_data = $this->data + $extra_vars;
  46. self::view($tpl, $tpl_data);
  47. return true;
  48. }
  49. public static function view($file, $tpl_data = array()) {
  50. $cached_file = self::cache($file);
  51. extract($tpl_data, EXTR_SKIP);
  52. $debugbar = App::getData('debugbar');
  53. if ($debugbar) {
  54. if (isset($debugbar['templates_vars'])) {
  55. $debugbar['templates_vars']->setData($tpl_data);
  56. }
  57. }
  58. unset($debugbar);
  59. require $cached_file;
  60. }
  61. protected static function cache($file) {
  62. if (!file_exists(self::$cache_path)) {
  63. if (! @mkdir(self::$cache_path, 0744)) {
  64. throw new \Exception("Cannot create templates cache dir " . self::$cache_path, 1);
  65. }
  66. }
  67. $cached_file = self::$cache_path . '/' . str_replace(array('/', '.html'), array('_', ''), $file . '.php');
  68. $cached_file_exists = is_file($cached_file);
  69. if ($cached_file_exists) {
  70. $cached_file_updated = filemtime($cached_file);
  71. $file_path = strpos($file, '/') === 0 ? $file : (self::$tpl_path . '/' . $file);
  72. self::$tpl_last_updated = filemtime($file_path);
  73. } else {
  74. $cached_file_updated = null;
  75. }
  76. if (ENV == 'dev') {
  77. // on force le parcours de tous les fichiers inclus pour avoir la vraie valeur de self::$tpl_last_updated
  78. $code = self::includeFiles($file);
  79. }
  80. if (!self::$cache_enabled || ! $cached_file_exists || $cached_file_updated < self::$tpl_last_updated) {
  81. if (! isset($code)) {
  82. $code = self::includeFiles($file);
  83. }
  84. $code = self::compileCode($code);
  85. file_put_contents($cached_file, '<?php class_exists(\'' . __CLASS__ . '\') or exit; ?>' . PHP_EOL . $code);
  86. } else {
  87. //header('X-Template: cached'); // TODO: $response->addHeader(...)
  88. $debugbar = App::getData('debugbar');
  89. if ($debugbar) {
  90. if (isset($debugbar['templates'])) {
  91. $debugbar_message_idx = $debugbar['templates']->addMessage([
  92. 'tpl' => $cached_file,
  93. 'content_length' => filesize($cached_file),
  94. 'content_length_str' => formatSize(filesize($cached_file)),
  95. 'cached' => true,
  96. ]);
  97. }
  98. }
  99. }
  100. return $cached_file;
  101. }
  102. public static function clearCache() {
  103. foreach(glob(self::$cache_path . '/*') as $file) {
  104. unlink($file);
  105. }
  106. }
  107. protected static function compileCode($code) {
  108. $code = self::compileBlock($code);
  109. $code = self::compileYield($code);
  110. $code = self::compileModules($code);
  111. $code = self::compileEscapedEchos($code);
  112. $code = self::compileEchos($code);
  113. //$code = self::compilePHP($code);
  114. return $code;
  115. }
  116. protected static function includeFiles($file, $level=0, $caller_file=null, $parent_file=null) {
  117. $file_path = strpos($file, '/') === 0 ? $file : (self::$tpl_path . '/' . $file);
  118. if (! is_file($file_path)) {
  119. throw new \Exception("Template file not found " . $file, 500);
  120. }
  121. $code = file_get_contents($file_path);
  122. $code_init = $code;
  123. $layout = null;
  124. $ts_update_file = filectime($file_path);
  125. if (empty(self::$tpl_last_updated) || self::$tpl_last_updated < $ts_update_file) {
  126. self::$tpl_last_updated = $ts_update_file;
  127. }
  128. static $tpl_idx = null;
  129. if (is_null($tpl_idx) || (empty($caller_file) && empty($parent_file))) {
  130. $tpl_idx = 0;
  131. }
  132. $tpl_idx++;
  133. $debugbar = App::getData('debugbar');
  134. if ($debugbar) {
  135. if (isset($debugbar['templates'])) {
  136. $debugbar_message_idx = $debugbar['templates']->addMessage([
  137. 'tpl' => $file,
  138. ]);
  139. }
  140. }
  141. $ts_start = microtime(true);
  142. // Layout (1)
  143. preg_match_all('/{layout ?\'?(.*?)\'? ?}/i', $code, $layout_matches, PREG_SET_ORDER);
  144. if ($layout_matches) {
  145. $value = $layout_matches[0];
  146. $layout = $value[1];
  147. } else {
  148. $layout = null;
  149. }
  150. if (defined('ENV') && ENV == 'dev') {
  151. $tpl_infos = '';
  152. if ($caller_file) {
  153. $tpl_infos .= ' layout for ' . $caller_file . '';
  154. }
  155. if ($parent_file) {
  156. $tpl_infos .= ' child of ' . $parent_file . '';
  157. }
  158. if ($layout) {
  159. $tpl_infos .= ' with layout ' . $layout . '';
  160. }
  161. $begin = '<!-- [' . $level . '] BEGIN TEMPLATE #' . $tpl_idx . ' : ' . $file . ' (size: ' . formatSize(strlen($code)) . ' - ' . $tpl_infos . ') -->';
  162. $end = '<!-- [' . $level . '] END TEMPLATE #' . $tpl_idx . ' : ' . $file . ' (size: ' . formatSize(strlen($code)) . ' - ' . $tpl_infos . ') -->';
  163. $code = PHP_EOL . $begin . PHP_EOL . $code . PHP_EOL . $end . PHP_EOL;
  164. }
  165. // Layout (2)
  166. if ($layout_matches) {
  167. $value = $layout_matches[0];
  168. $layout = $value[1];
  169. $layout_code = self::includeFiles($layout, $level-1, $file);
  170. $code = str_replace($value[0], '', $code);
  171. $layout_code = str_replace('<' . '?=$child_content?' . '>', '{@content}', $layout_code);
  172. $layout_code = str_replace('{$child_content}', '{@content}', $layout_code);
  173. $layout_code = str_replace('{@content}', $code, $layout_code);
  174. $code = $layout_code;
  175. }
  176. // includes
  177. preg_match_all('/{include ?\'?(.*?)\'? ?}/i', $code, $matches, PREG_SET_ORDER);
  178. foreach ($matches as $value) {
  179. $included_code = self::includeFiles($value[1], $level+1, null, $file);
  180. $code = str_replace($value[0], $included_code, $code);
  181. }
  182. $ts_end = microtime(true);
  183. $duration = $ts_end - $ts_start;
  184. if (isset($debugbar_message_idx) && ! is_null($debugbar_message_idx)) {
  185. $debugbar['templates']->updateMessage($debugbar_message_idx, [
  186. 'tpl' => $file,
  187. 'layout' => $layout,
  188. 'source_length' => strlen($code_init),
  189. 'source_length_str' => formatSize(strlen($code_init)),
  190. 'content_length' => strlen($code),
  191. 'content_length_str' => formatSize(strlen($code)),
  192. 'duration' => round($duration, 6),
  193. 'duration_str' => formatDuration($duration),
  194. 'level' => $level,
  195. ]);
  196. }
  197. return $code;
  198. }
  199. protected static function compilePHP($code) {
  200. /* return preg_replace('~\{%\s*(.+?)\s*\%}~is', '<?php $1 ?>', $code); */
  201. return $code;
  202. }
  203. protected static function compileModules($code) {
  204. // url => {url clients_list}
  205. $code = preg_replace('/{routeUrl /', '{url ', $code); // for compatibility with old templates
  206. preg_match_all('~{url (.*?)}~is', $code, $matches, PREG_SET_ORDER);
  207. foreach ($matches as $value) {
  208. //pre($matches); exit;
  209. $code = str_replace($value[0], getRouteUrl($value[1]), $code);
  210. }
  211. // foreach => {foreach $list as $item}<div>...</div>{/foreach}
  212. preg_match_all('~{foreach (.*?) ?}(.*?){/foreach}~is', $code, $matches, PREG_SET_ORDER);
  213. foreach ($matches as $value) {
  214. $replaced = PHP_EOL . '<' . '?php foreach (' . $value[1] . ') : ?' . '>' . PHP_EOL . $value[2] . PHP_EOL . '<' . '?php endforeach; ?' . '>';
  215. $code = str_replace($value[0], $replaced, $code);
  216. }
  217. // if => {if $item}<div>...</div>{/if}
  218. preg_match_all('~{if (.*?) ?}(.*?){/if}~is', $code, $matches, PREG_SET_ORDER);
  219. foreach ($matches as $value) {
  220. $replaced = '<' . '?php elseif ( $1 ) : ?' . '>';
  221. $value[2] = preg_replace('/{elseif (.*?) ?}/', $replaced, $value[2]);
  222. $replaced = PHP_EOL . '<' . '?php if (' . $value[1] . ') : ?' . '>' . PHP_EOL . $value[2] . PHP_EOL . '<' . '?php endif; ?' . '>';
  223. $code = str_replace($value[0], $replaced, $code);
  224. }
  225. return $code;
  226. }
  227. protected static function compileEchos($code, $strict=true) {
  228. // compile PHP variables (method 1) => {$my_var}
  229. if ($strict) {
  230. $code = preg_replace('~\{\$(.+?)}~is', '<?php echo \$$1 ?>', $code);
  231. } else {
  232. $code = preg_replace('~\{\$(.+?)}~is', '<?php echo isset(\$$1) ? (\$$1) : ""; ?>', $code);
  233. }
  234. // compile PHP variables (method 2) => {{ $my_var }}
  235. return preg_replace('~\{{\s*(.+?)\s*\}}~is', '<?php echo $1 ?>', $code);
  236. }
  237. protected static function compileEscapedEchos($code) {
  238. // compile PHP escaped variables => {{{ $my_var }}}
  239. return preg_replace('~\{{{\s*(.+?)\s*\}}}~is', '<?php echo htmlentities($1, ENT_QUOTES, \'UTF-8\') ?>', $code);
  240. }
  241. protected static function compileBlock($code) {
  242. preg_match_all('~{block ?(.*?) ?}(.*?){/block}~is', $code, $matches, PREG_SET_ORDER);
  243. foreach ($matches as $value) {
  244. if (!array_key_exists($value[1], self::$blocks)) self::$blocks[$value[1]] = '';
  245. if (strpos($value[2], '@parent') === false) {
  246. self::$blocks[$value[1]] = $value[2];
  247. } else {
  248. self::$blocks[$value[1]] = str_replace('@parent', self::$blocks[$value[1]], $value[2]);
  249. }
  250. $code = str_replace($value[0], '', $code);
  251. }
  252. return $code;
  253. }
  254. protected static function compileYield($code) {
  255. // compile yields => {yield my_block}
  256. foreach(self::$blocks as $block => $value) {
  257. $code = preg_replace('/{yield ' . $block . ' ?}/', $value, $code);
  258. }
  259. $code = preg_replace('/{yield ?(.*?) ?}/i', '', $code);
  260. return $code;
  261. }
  262. }