1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Latte;
9:
10:
11: 12: 13:
14: class Engine
15: {
16: use Strict;
17:
18: const VERSION = '2.4.8';
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_ICAL = 'ical',
27: CONTENT_TEXT = 'text';
28:
29:
30: public $onCompile = [];
31:
32:
33: private $parser;
34:
35:
36: private $compiler;
37:
38:
39: private $loader;
40:
41:
42: private $filters;
43:
44:
45: private $providers = [];
46:
47:
48: private $contentType = self::CONTENT_HTML;
49:
50:
51: private $tempDirectory;
52:
53:
54: private $autoRefresh = true;
55:
56:
57: public function __construct()
58: {
59: $this->filters = new Runtime\FilterExecutor;
60: }
61:
62:
63: 64: 65: 66:
67: public function render($name, array $params = [], $block = null)
68: {
69: $this->createTemplate($name, $params + ['_renderblock' => $block])
70: ->render();
71: }
72:
73:
74: 75: 76: 77:
78: public function renderToString($name, array $params = [], $block = null)
79: {
80: $template = $this->createTemplate($name, $params + ['_renderblock' => $block]);
81: return $template->capture([$template, 'render']);
82: }
83:
84:
85: 86: 87: 88:
89: public function createTemplate($name, array $params = [])
90: {
91: $class = $this->getTemplateClass($name);
92: if (!class_exists($class, false)) {
93: $this->loadTemplate($name);
94: }
95: return new $class($this, $params, $this->filters, $this->providers, $name);
96: }
97:
98:
99: 100: 101: 102:
103: public function compile($name)
104: {
105: foreach ($this->onCompile ?: [] as $cb) {
106: call_user_func(Helpers::checkCallback($cb), $this);
107: }
108: $this->onCompile = [];
109:
110: $source = $this->getLoader()->getContent($name);
111:
112: try {
113: $tokens = $this->getParser()->setContentType($this->contentType)
114: ->parse($source);
115:
116: $code = $this->getCompiler()->setContentType($this->contentType)
117: ->compile($tokens, $this->getTemplateClass($name));
118:
119: } catch (\Exception $e) {
120: if (!$e instanceof CompileException) {
121: $e = new CompileException("Thrown exception '{$e->getMessage()}'", 0, $e);
122: }
123: $line = isset($tokens) ? $this->getCompiler()->getLine() : $this->getParser()->getLine();
124: throw $e->setSource($source, $line, $name);
125: }
126:
127: if (!preg_match('#\n|\?#', $name)) {
128: $code = "<?php\n// source: $name\n?>" . $code;
129: }
130: $code = PhpHelpers::reformatCode($code);
131: return $code;
132: }
133:
134:
135: 136: 137: 138: 139: 140:
141: public function warmupCache($name)
142: {
143: if (!$this->tempDirectory) {
144: throw new \LogicException('Path to temporary directory is not set.');
145: }
146:
147: $class = $this->getTemplateClass($name);
148: if (!class_exists($class, false)) {
149: $this->loadTemplate($name);
150: }
151: }
152:
153:
154: 155: 156:
157: private function loadTemplate($name)
158: {
159: if (!$this->tempDirectory) {
160: $code = $this->compile($name);
161: if (@eval('?>' . $code) === false) {
162: throw (new CompileException('Error in template: ' . error_get_last()['message']))
163: ->setSource($code, error_get_last()['line'], "$name (compiled)");
164: }
165: return;
166: }
167:
168: $file = $this->getCacheFile($name);
169:
170: if (!$this->isExpired($file, $name) && (@include $file) !== false) {
171: return;
172: }
173:
174: if (!is_dir($this->tempDirectory) && !@mkdir($this->tempDirectory) && !is_dir($this->tempDirectory)) {
175: throw new \RuntimeException("Unable to create directory '$this->tempDirectory'. " . error_get_last()['message']);
176: }
177:
178: $handle = @fopen("$file.lock", 'c+');
179: if (!$handle) {
180: throw new \RuntimeException("Unable to create file '$file.lock'. " . error_get_last()['message']);
181: } elseif (!@flock($handle, LOCK_EX)) {
182: throw new \RuntimeException("Unable to acquire exclusive lock on '$file.lock'. " . error_get_last()['message']);
183: }
184:
185: if (!is_file($file) || $this->isExpired($file, $name)) {
186: $code = $this->compile($name);
187: if (file_put_contents("$file.tmp", $code) !== strlen($code) || !rename("$file.tmp", $file)) {
188: @unlink("$file.tmp");
189: throw new \RuntimeException("Unable to create '$file'.");
190: } elseif (function_exists('opcache_invalidate')) {
191: @opcache_invalidate($file, true);
192: }
193: }
194:
195: if ((include $file) === false) {
196: throw new \RuntimeException("Unable to load '$file'.");
197: }
198:
199: flock($handle, LOCK_UN);
200: fclose($handle);
201: @unlink("$file.lock");
202: }
203:
204:
205: 206: 207: 208: 209:
210: private function isExpired($file, $name)
211: {
212: return $this->autoRefresh && $this->getLoader()->isExpired($name, (int) @filemtime($file));
213: }
214:
215:
216: 217: 218:
219: public function getCacheFile($name)
220: {
221: $hash = substr($this->getTemplateClass($name), 8);
222: $base = preg_match('#([/\\\\][\w@.-]{3,35}){1,3}\z#', $name, $m)
223: ? preg_replace('#[^\w@.-]+#', '-', substr($m[0], 1)) . '--'
224: : '';
225: return "$this->tempDirectory/$base$hash.php";
226: }
227:
228:
229: 230: 231:
232: public function getTemplateClass($name)
233: {
234: $key = $this->getLoader()->getUniqueId($name) . "\00" . self::VERSION;
235: return 'Template' . substr(md5($key), 0, 10);
236: }
237:
238:
239: 240: 241: 242: 243: 244:
245: public function addFilter($name, $callback)
246: {
247: $this->filters->add($name, $callback);
248: return $this;
249: }
250:
251:
252: 253: 254: 255:
256: public function getFilters()
257: {
258: return $this->filters->getAll();
259: }
260:
261:
262: 263: 264: 265: 266: 267:
268: public function invokeFilter($name, array $args)
269: {
270: return call_user_func_array($this->filters->$name, $args);
271: }
272:
273:
274: 275: 276: 277:
278: public function addMacro($name, IMacro $macro)
279: {
280: $this->getCompiler()->addMacro($name, $macro);
281: return $this;
282: }
283:
284:
285: 286: 287: 288:
289: public function addProvider($name, $value)
290: {
291: $this->providers[$name] = $value;
292: return $this;
293: }
294:
295:
296: 297: 298: 299:
300: public function getProviders()
301: {
302: return $this->providers;
303: }
304:
305:
306: 307: 308:
309: public function setContentType($type)
310: {
311: $this->contentType = $type;
312: return $this;
313: }
314:
315:
316: 317: 318: 319:
320: public function setTempDirectory($path)
321: {
322: $this->tempDirectory = $path;
323: return $this;
324: }
325:
326:
327: 328: 329: 330:
331: public function setAutoRefresh($on = true)
332: {
333: $this->autoRefresh = (bool) $on;
334: return $this;
335: }
336:
337:
338: 339: 340:
341: public function getParser()
342: {
343: if (!$this->parser) {
344: $this->parser = new Parser;
345: }
346: return $this->parser;
347: }
348:
349:
350: 351: 352:
353: public function getCompiler()
354: {
355: if (!$this->compiler) {
356: $this->compiler = new Compiler;
357: Macros\CoreMacros::install($this->compiler);
358: Macros\BlockMacros::install($this->compiler);
359: }
360: return $this->compiler;
361: }
362:
363:
364: 365: 366:
367: public function setLoader(ILoader $loader)
368: {
369: $this->loader = $loader;
370: return $this;
371: }
372:
373:
374: 375: 376:
377: public function getLoader()
378: {
379: if (!$this->loader) {
380: $this->loader = new Loaders\FileLoader;
381: }
382: return $this->loader;
383: }
384: }
385: