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