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.13';
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: $this->loadTemplate($name);
91: }
92:
93: $template = new $class($params, $this, $name);
94: $template->render();
95: }
96:
97:
98: 99: 100: 101:
102: public function renderToString($name, array $params = array())
103: {
104: ob_start(function () {});
105: try {
106: $this->render($name, $params);
107: } catch (\Throwable $e) {
108: ob_end_clean();
109: throw $e;
110: } catch (\Exception $e) {
111: ob_end_clean();
112: throw $e;
113: }
114: return ob_get_clean();
115: }
116:
117:
118: 119: 120: 121:
122: public function compile($name)
123: {
124: foreach ($this->onCompile ?: array() as $cb) {
125: call_user_func(Helpers::checkCallback($cb), $this);
126: }
127: $this->onCompile = array();
128:
129: $source = $this->getLoader()->getContent($name);
130:
131: try {
132: $tokens = $this->getParser()->setContentType($this->contentType)
133: ->parse($source);
134:
135: $code = $this->getCompiler()->setContentType($this->contentType)
136: ->compile($tokens, $this->getTemplateClass($name));
137:
138: } catch (\Exception $e) {
139: if (!$e instanceof CompileException) {
140: $e = new CompileException("Thrown exception '{$e->getMessage()}'", NULL, $e);
141: }
142: $line = isset($tokens) ? $this->getCompiler()->getLine() : $this->getParser()->getLine();
143: throw $e->setSource($source, $line, $name);
144: }
145:
146: if (!preg_match('#\n|\?#', $name)) {
147: $code = "<?php\n// source: $name\n?>" . $code;
148: }
149: $code = Helpers::optimizePhp($code);
150: return $code;
151: }
152:
153:
154: 155: 156: 157: 158: 159:
160: public function warmupCache($name)
161: {
162: if (!$this->tempDirectory) {
163: throw new \LogicException('Path to temporary directory is not set.');
164: }
165:
166: $class = $this->getTemplateClass($name);
167: if (!class_exists($class, FALSE)) {
168: $this->loadTemplate($name);
169: }
170: }
171:
172:
173: 174: 175:
176: private function loadTemplate($name)
177: {
178: if (!$this->tempDirectory) {
179: $code = $this->compile($name);
180: if (@eval('?>' . $code) === FALSE) {
181: $error = error_get_last();
182: $e = new CompileException('Error in template: ' . $error['message']);
183: throw $e->setSource($code, $error['line'], "$name (compiled)");
184: }
185: return;
186: }
187:
188: $file = $this->getCacheFile($name);
189:
190: if (!$this->isExpired($file, $name) && (@include $file) !== FALSE) {
191: return;
192: }
193:
194: if (!is_dir($this->tempDirectory)) {
195: @mkdir($this->tempDirectory);
196: }
197:
198: $handle = fopen("$file.lock", 'c+');
199: if (!$handle || !flock($handle, LOCK_EX)) {
200: throw new \RuntimeException("Unable to acquire exclusive lock '$file.lock'.");
201: }
202:
203: if (!is_file($file) || $this->isExpired($file, $name)) {
204: $code = $this->compile($name);
205: if (file_put_contents("$file.tmp", $code) !== strlen($code) || !rename("$file.tmp", $file)) {
206: @unlink("$file.tmp");
207: throw new \RuntimeException("Unable to create '$file'.");
208: }
209: }
210:
211: if ((include $file) === FALSE) {
212: throw new \RuntimeException("Unable to load '$file'.");
213: }
214:
215: flock($handle, LOCK_UN);
216: }
217:
218:
219: 220: 221: 222: 223:
224: private function isExpired($file, $name)
225: {
226: return $this->autoRefresh && $this->getLoader()->isExpired($name, (int) @filemtime($file));
227: }
228:
229:
230: 231: 232:
233: public function getCacheFile($name)
234: {
235: $file = $this->getTemplateClass($name);
236: if (preg_match('#\b\w.{10,50}$#', $name, $m)) {
237: $file = trim(preg_replace('#\W+#', '-', $m[0]), '-') . '-' . $file;
238: }
239: return $this->tempDirectory . '/' . $file . '.php';
240: }
241:
242:
243: 244: 245:
246: public function getTemplateClass($name)
247: {
248: return 'Template' . md5("$this->tempDirectory\00$name");
249: }
250:
251:
252: 253: 254: 255: 256: 257:
258: public function addFilter($name, $callback)
259: {
260: if ($name == NULL) {
261: array_unshift($this->filters[NULL], $callback);
262: } else {
263: $this->filters[strtolower($name)] = $callback;
264: }
265: return $this;
266: }
267:
268:
269: 270: 271: 272:
273: public function getFilters()
274: {
275: return $this->filters;
276: }
277:
278:
279: 280: 281: 282: 283: 284:
285: public function invokeFilter($name, array $args)
286: {
287: $lname = strtolower($name);
288: if (!isset($this->filters[$lname])) {
289: $args2 = $args;
290: array_unshift($args2, $lname);
291: foreach ($this->filters[NULL] as $filter) {
292: $res = call_user_func_array(Helpers::checkCallback($filter), $args2);
293: if ($res !== NULL) {
294: return $res;
295: } elseif (isset($this->filters[$lname])) {
296: return call_user_func_array(Helpers::checkCallback($this->filters[$lname]), $args);
297: }
298: }
299: $hint = ($t = Helpers::getSuggestion(array_keys($this->filters), $name)) ? ", did you mean '$t'?" : '.';
300: throw new \LogicException("Filter '$name' is not defined$hint");
301: }
302: return call_user_func_array(Helpers::checkCallback($this->filters[$lname]), $args);
303: }
304:
305:
306: 307: 308: 309:
310: public function addMacro($name, IMacro $macro)
311: {
312: $this->getCompiler()->addMacro($name, $macro);
313: return $this;
314: }
315:
316:
317: 318: 319:
320: public function setContentType($type)
321: {
322: $this->contentType = $type;
323: return $this;
324: }
325:
326:
327: 328: 329: 330:
331: public function setTempDirectory($path)
332: {
333: $this->tempDirectory = $path;
334: return $this;
335: }
336:
337:
338: 339: 340: 341:
342: public function setAutoRefresh($on = TRUE)
343: {
344: $this->autoRefresh = (bool) $on;
345: return $this;
346: }
347:
348:
349: 350: 351:
352: public function getParser()
353: {
354: if (!$this->parser) {
355: $this->parser = new Parser;
356: }
357: return $this->parser;
358: }
359:
360:
361: 362: 363:
364: public function getCompiler()
365: {
366: if (!$this->compiler) {
367: $this->compiler = new Compiler;
368: Macros\CoreMacros::install($this->compiler);
369: Macros\BlockMacros::install($this->compiler);
370: }
371: return $this->compiler;
372: }
373:
374:
375: 376: 377:
378: public function setLoader(ILoader $loader)
379: {
380: $this->loader = $loader;
381: return $this;
382: }
383:
384:
385: 386: 387:
388: public function getLoader()
389: {
390: if (!$this->loader) {
391: $this->loader = new Loaders\FileLoader;
392: }
393: return $this->loader;
394: }
395:
396: }
397: