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