1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Latte;
9:
10:
11: 12: 13:
14: class Engine extends Object
15: {
16: const VERSION = '2.3.8';
17:
18:
19: const CONTENT_HTML = 'html',
20: CONTENT_XHTML = 'xhtml',
21: CONTENT_XML = 'xml',
22: CONTENT_JS = 'js',
23: CONTENT_CSS = 'css',
24: CONTENT_URL = 'url',
25: CONTENT_ICAL = 'ical',
26: CONTENT_TEXT = 'text';
27:
28:
29: public $onCompile = array();
30:
31:
32: private $parser;
33:
34:
35: private $compiler;
36:
37:
38: private $loader;
39:
40:
41: private $contentType = self::CONTENT_HTML;
42:
43:
44: private $tempDirectory;
45:
46:
47: private $autoRefresh = TRUE;
48:
49:
50: private $filters = array(
51: NULL => array(),
52: 'bytes' => 'Latte\Runtime\Filters::bytes',
53: 'capitalize' => 'Latte\Runtime\Filters::capitalize',
54: 'datastream' => 'Latte\Runtime\Filters::dataStream',
55: 'date' => 'Latte\Runtime\Filters::date',
56: 'escapecss' => 'Latte\Runtime\Filters::escapeCss',
57: 'escapehtml' => 'Latte\Runtime\Filters::escapeHtml',
58: 'escapehtmlcomment' => 'Latte\Runtime\Filters::escapeHtmlComment',
59: 'escapeical' => 'Latte\Runtime\Filters::escapeICal',
60: 'escapejs' => 'Latte\Runtime\Filters::escapeJs',
61: 'escapeurl' => 'rawurlencode',
62: 'escapexml' => 'Latte\Runtime\Filters::escapeXML',
63: 'firstupper' => 'Latte\Runtime\Filters::firstUpper',
64: 'implode' => 'implode',
65: 'indent' => 'Latte\Runtime\Filters::indent',
66: 'lower' => 'Latte\Runtime\Filters::lower',
67: 'nl2br' => 'Latte\Runtime\Filters::nl2br',
68: 'number' => 'number_format',
69: 'repeat' => 'str_repeat',
70: 'replace' => 'Latte\Runtime\Filters::replace',
71: 'replacere' => 'Latte\Runtime\Filters::replaceRe',
72: 'safeurl' => 'Latte\Runtime\Filters::safeUrl',
73: 'strip' => 'Latte\Runtime\Filters::strip',
74: 'striptags' => 'strip_tags',
75: 'substr' => 'Latte\Runtime\Filters::substring',
76: 'trim' => 'Latte\Runtime\Filters::trim',
77: 'truncate' => 'Latte\Runtime\Filters::truncate',
78: 'upper' => 'Latte\Runtime\Filters::upper',
79: );
80:
81:
82: 83: 84: 85:
86: public function render($name, array $params = array())
87: {
88: $class = $this->getTemplateClass($name);
89: if (!class_exists($class, FALSE)) {
90: if ($this->tempDirectory) {
91: $this->loadTemplateFromCache($name);
92: } else {
93: $this->loadTemplate($name);
94: }
95: }
96:
97: $template = new $class($params, $this, $name);
98: $template->render();
99: }
100:
101:
102: 103: 104: 105:
106: public function renderToString($name, array $params = array())
107: {
108: ob_start();
109: try {
110: $this->render($name, $params);
111: } catch (\Throwable $e) {
112: ob_end_clean();
113: throw $e;
114: } catch (\Exception $e) {
115: ob_end_clean();
116: throw $e;
117: }
118: return ob_get_clean();
119: }
120:
121:
122: 123: 124: 125:
126: public function compile($name)
127: {
128: foreach ($this->onCompile ?: array() as $cb) {
129: call_user_func(Helpers::checkCallback($cb), $this);
130: }
131: $this->onCompile = array();
132:
133: $source = $this->getLoader()->getContent($name);
134:
135: try {
136: $tokens = $this->getParser()->setContentType($this->contentType)
137: ->parse($source);
138:
139: $code = $this->getCompiler()->setContentType($this->contentType)
140: ->compile($tokens, $this->getTemplateClass($name));
141:
142: } catch (\Exception $e) {
143: if (!$e instanceof CompileException) {
144: $e = new CompileException("Thrown exception '{$e->getMessage()}'", NULL, $e);
145: }
146: $line = isset($tokens) ? $this->getCompiler()->getLine() : $this->getParser()->getLine();
147: throw $e->setSource($source, $line, $name);
148: }
149:
150: if (!preg_match('#\n|\?#', $name)) {
151: $code = "<?php\n// source: $name\n?>" . $code;
152: }
153: $code = Helpers::optimizePhp($code);
154: return $code;
155: }
156:
157:
158: 159: 160: 161: 162: 163:
164: public function warmupCache($name)
165: {
166: if (!$this->tempDirectory) {
167: throw new \LogicException('Path to temporary directory is not set.');
168: }
169:
170: $class = $this->getTemplateClass($name);
171: if (!class_exists($class, FALSE)) {
172: $this->loadTemplateFromCache($name);
173: }
174: }
175:
176:
177: 178: 179:
180: private function loadTemplateFromCache($name)
181: {
182: $file = $this->getCacheFile($name);
183:
184: if (!$this->isExpired($file, $name) && (@include $file) !== FALSE) {
185: return;
186: }
187:
188: if (!is_dir($this->tempDirectory)) {
189: @mkdir($this->tempDirectory);
190: }
191:
192: $handle = fopen("$file.lock", 'c+');
193: if (!$handle || !flock($handle, LOCK_EX)) {
194: throw new \RuntimeException("Unable to acquire exclusive lock '$file.lock'.");
195: }
196:
197: if (!is_file($file) || $this->isExpired($file, $name)) {
198: $code = $this->loadTemplate($name);
199: if (file_put_contents("$file.tmp", $code) !== strlen($code) || !rename("$file.tmp", $file)) {
200: @unlink("$file.tmp");
201: throw new \RuntimeException("Unable to create '$file'.");
202: }
203:
204: } elseif ((include $file) === FALSE) {
205: throw new \RuntimeException("Unable to load '$file'.");
206: }
207:
208: flock($handle, LOCK_UN);
209: }
210:
211:
212: 213: 214:
215: private function loadTemplate($name)
216: {
217: $code = $this->compile($name);
218: try {
219: if (@eval('?>' . $code) === FALSE) {
220: $error = error_get_last();
221: $e = new CompileException('Error in template: ' . $error['message']);
222: throw $e->setSource($code, $error['line'], $name . ' (compiled)');
223: }
224: } catch (\ParseError $e) {
225: $e = new CompileException('Error in template: ' . $e->getMessage(), 0, $e);
226: throw $e->setSource($code, $e->getLine(), $name . ' (compiled)');
227: }
228: return $code;
229: }
230:
231:
232: 233: 234: 235: 236:
237: private function isExpired($file, $name)
238: {
239: return $this->autoRefresh && $this->getLoader()->isExpired($name, (int) @filemtime($file));
240: }
241:
242:
243: 244: 245:
246: public function getCacheFile($name)
247: {
248: $file = $this->getTemplateClass($name);
249: if (preg_match('#\b\w.{10,50}$#', $name, $m)) {
250: $file = trim(preg_replace('#\W+#', '-', $m[0]), '-') . '-' . $file;
251: }
252: return $this->tempDirectory . '/' . $file . '.php';
253: }
254:
255:
256: 257: 258:
259: public function getTemplateClass($name)
260: {
261: return 'Template' . md5("$this->tempDirectory\00$name");
262: }
263:
264:
265: 266: 267: 268: 269: 270:
271: public function addFilter($name, $callback)
272: {
273: if ($name == NULL) {
274: array_unshift($this->filters[NULL], $callback);
275: } else {
276: $this->filters[strtolower($name)] = $callback;
277: }
278: return $this;
279: }
280:
281:
282: 283: 284: 285:
286: public function getFilters()
287: {
288: return $this->filters;
289: }
290:
291:
292: 293: 294: 295: 296: 297:
298: public function invokeFilter($name, array $args)
299: {
300: $lname = strtolower($name);
301: if (!isset($this->filters[$lname])) {
302: $args2 = $args;
303: array_unshift($args2, $lname);
304: foreach ($this->filters[NULL] as $filter) {
305: $res = call_user_func_array(Helpers::checkCallback($filter), $args2);
306: if ($res !== NULL) {
307: return $res;
308: } elseif (isset($this->filters[$lname])) {
309: return call_user_func_array(Helpers::checkCallback($this->filters[$lname]), $args);
310: }
311: }
312: $hint = ($t = Helpers::getSuggestion(array_keys($this->filters), $name)) ? ", did you mean '$t'?" : '.';
313: throw new \LogicException("Filter '$name' is not defined$hint");
314: }
315: return call_user_func_array(Helpers::checkCallback($this->filters[$lname]), $args);
316: }
317:
318:
319: 320: 321: 322:
323: public function addMacro($name, IMacro $macro)
324: {
325: $this->getCompiler()->addMacro($name, $macro);
326: return $this;
327: }
328:
329:
330: 331: 332:
333: public function setContentType($type)
334: {
335: $this->contentType = $type;
336: return $this;
337: }
338:
339:
340: 341: 342: 343:
344: public function setTempDirectory($path)
345: {
346: $this->tempDirectory = $path;
347: return $this;
348: }
349:
350:
351: 352: 353: 354:
355: public function setAutoRefresh($on = TRUE)
356: {
357: $this->autoRefresh = (bool) $on;
358: return $this;
359: }
360:
361:
362: 363: 364:
365: public function getParser()
366: {
367: if (!$this->parser) {
368: $this->parser = new Parser;
369: }
370: return $this->parser;
371: }
372:
373:
374: 375: 376:
377: public function getCompiler()
378: {
379: if (!$this->compiler) {
380: $this->compiler = new Compiler;
381: Macros\CoreMacros::install($this->compiler);
382: Macros\BlockMacros::install($this->compiler);
383: }
384: return $this->compiler;
385: }
386:
387:
388: 389: 390:
391: public function setLoader(ILoader $loader)
392: {
393: $this->loader = $loader;
394: return $this;
395: }
396:
397:
398: 399: 400:
401: public function getLoader()
402: {
403: if (!$this->loader) {
404: $this->loader = new Loaders\FileLoader;
405: }
406: return $this->loader;
407: }
408:
409: }
410: