1: <?php
2:
3: 4: 5: 6: 7: 8: 9: 10: 11:
12:
13:
14:
15: 16: 17: 18: 19: 20:
21: class Template extends Object implements ITemplate
22: {
23:
24: public $warnOnUndefined = TRUE;
25:
26:
27: public $onPrepareFilters = array();
28:
29:
30: private $source;
31:
32:
33: private $params = array();
34:
35:
36: private $filters = array();
37:
38:
39: private $helpers = array();
40:
41:
42: private $helperLoaders = array();
43:
44:
45: private $cacheStorage;
46:
47:
48:
49: 50: 51: 52: 53:
54: public function setSource($source)
55: {
56: $this->source = $source;
57: return $this;
58: }
59:
60:
61:
62: 63: 64: 65:
66: public function getSource()
67: {
68: return $this->source;
69: }
70:
71:
72:
73:
74:
75:
76:
77: 78: 79: 80:
81: public function render()
82: {
83: $cache = new Cache($storage = $this->getCacheStorage(), 'Nette.Template');
84: $cached = $compiled = $cache->load($this->source);
85:
86: if ($compiled === NULL) {
87: $compiled = $this->compile();
88: $cache->save($this->source, $compiled, array(Cache::CONSTS => 'Framework::REVISION'));
89: $cache->release();
90: $cached = $cache->load($this->source);
91: }
92:
93: if ($cached !== NULL && $storage instanceof PhpFileStorage) {
94: LimitedScope::load($cached['file'], $this->getParams());
95: } else {
96: LimitedScope::evaluate($compiled, $this->getParams());
97: }
98: }
99:
100:
101:
102: 103: 104: 105: 106:
107: public function save($file)
108: {
109: if (file_put_contents($file, $this->__toString(TRUE)) === FALSE) {
110: throw new IOException("Unable to save file '$file'.");
111: }
112: }
113:
114:
115:
116: 117: 118: 119: 120:
121: public function __toString()
122: {
123: $args = func_get_args();
124: ob_start();
125: try {
126: $this->render();
127: return ob_get_clean();
128:
129: } catch (Exception $e) {
130: ob_end_clean();
131: if ($args && $args[0]) {
132: throw $e;
133: } else {
134: Debugger::toStringException($e);
135: }
136: }
137: }
138:
139:
140:
141: 142: 143: 144:
145: public function compile()
146: {
147: if (!$this->filters) {
148: $this->onPrepareFilters($this);
149: }
150:
151: $code = $this->getSource();
152: foreach ($this->filters as $filter) {
153: $code = self::extractPhp($code, $blocks);
154: $code = $filter->invoke($code);
155: $code = strtr($code, $blocks);
156: }
157:
158: return self::optimizePhp($code);
159: }
160:
161:
162:
163:
164:
165:
166:
167: 168: 169: 170: 171:
172: public function registerFilter($callback)
173: {
174: $callback = callback($callback);
175: if (in_array($callback, $this->filters)) {
176: throw new InvalidStateException("Filter '$callback' was registered twice.");
177: }
178: $this->filters[] = $callback;
179: }
180:
181:
182:
183: 184: 185: 186:
187: final public function getFilters()
188: {
189: return $this->filters;
190: }
191:
192:
193:
194: 195: 196: 197: 198: 199:
200: public function registerHelper($name, $callback)
201: {
202: $this->helpers[strtolower($name)] = callback($callback);
203: }
204:
205:
206:
207: 208: 209: 210: 211:
212: public function registerHelperLoader($callback)
213: {
214: $this->helperLoaders[] = callback($callback);
215: }
216:
217:
218:
219: 220: 221: 222:
223: final public function getHelpers()
224: {
225: return $this->helpers;
226: }
227:
228:
229:
230: 231: 232: 233: 234: 235:
236: public function __call($name, $args)
237: {
238: $lname = strtolower($name);
239: if (!isset($this->helpers[$lname])) {
240: foreach ($this->helperLoaders as $loader) {
241: $helper = $loader->invoke($lname);
242: if ($helper) {
243: $this->registerHelper($lname, $helper);
244: return $this->helpers[$lname]->invokeArgs($args);
245: }
246: }
247: return parent::__call($name, $args);
248: }
249:
250: return $this->helpers[$lname]->invokeArgs($args);
251: }
252:
253:
254:
255: 256: 257: 258: 259:
260: public function setTranslator(ITranslator $translator = NULL)
261: {
262: $this->registerHelper('translate', $translator === NULL ? NULL : array($translator, 'translate'));
263: return $this;
264: }
265:
266:
267:
268:
269:
270:
271:
272: 273: 274: 275: 276: 277:
278: public function add($name, $value)
279: {
280: if (array_key_exists($name, $this->params)) {
281: throw new InvalidStateException("The variable '$name' already exists.");
282: }
283:
284: $this->params[$name] = $value;
285: }
286:
287:
288:
289: 290: 291: 292: 293:
294: public function setParams(array $params)
295: {
296: $this->params = $params + $this->params;
297: return $this;
298: }
299:
300:
301:
302: 303: 304: 305:
306: public function getParams()
307: {
308: $this->params['template'] = $this;
309: return $this->params;
310: }
311:
312:
313:
314: 315: 316: 317: 318: 319:
320: public function __set($name, $value)
321: {
322: $this->params[$name] = $value;
323: }
324:
325:
326:
327: 328: 329: 330: 331:
332: public function &__get($name)
333: {
334: if ($this->warnOnUndefined && !array_key_exists($name, $this->params)) {
335: trigger_error("The variable '$name' does not exist in template.", E_USER_NOTICE);
336: }
337:
338: return $this->params[$name];
339: }
340:
341:
342:
343: 344: 345: 346: 347:
348: public function __isset($name)
349: {
350: return isset($this->params[$name]);
351: }
352:
353:
354:
355: 356: 357: 358: 359:
360: public function __unset($name)
361: {
362: unset($this->params[$name]);
363: }
364:
365:
366:
367:
368:
369:
370:
371: 372: 373: 374: 375:
376: public function setCacheStorage(ICacheStorage $storage)
377: {
378: $this->cacheStorage = $storage;
379: }
380:
381:
382:
383: 384: 385:
386: public function getCacheStorage()
387: {
388: if ($this->cacheStorage === NULL) {
389: return new DevNullStorage;
390: }
391: return $this->cacheStorage;
392: }
393:
394:
395:
396:
397:
398:
399:
400: 401: 402: 403: 404: 405:
406: private static function extractPhp($source, & $blocks)
407: {
408: $res = '';
409: $blocks = array();
410: $tokens = token_get_all($source);
411: foreach ($tokens as $n => $token) {
412: if (is_array($token)) {
413: if ($token[0] === T_INLINE_HTML || $token[0] === T_CLOSE_TAG) {
414: $res .= $token[1];
415: continue;
416:
417: } elseif ($token[0] === T_OPEN_TAG && $token[1] === '<?' && isset($tokens[$n+1][1]) && $tokens[$n+1][1] === 'xml') {
418: $php = & $res;
419: $token[1] = '<<?php ?>?';
420:
421: } elseif ($token[0] === T_OPEN_TAG || $token[0] === T_OPEN_TAG_WITH_ECHO) {
422: $res .= $id = "<?php \x01@php:p" . count($blocks) . "@\x02";
423: $php = & $blocks[$id];
424: }
425: $php .= $token[1];
426:
427: } else {
428: $php .= $token;
429: }
430: }
431: return $res;
432: }
433:
434:
435:
436: 437: 438: 439: 440:
441: public static function optimizePhp($source, $lineLength = 80, $existenceOfThisParameterSolvesDamnBugInPHP535 = NULL)
442: {
443: $res = $php = '';
444: $lastChar = ';';
445: $tokens = new ArrayIterator(token_get_all($source));
446: foreach ($tokens as $key => $token) {
447: if (is_array($token)) {
448: if ($token[0] === T_INLINE_HTML) {
449: $lastChar = '';
450: $res .= $token[1];
451:
452: } elseif ($token[0] === T_CLOSE_TAG) {
453: $next = isset($tokens[$key + 1]) ? $tokens[$key + 1] : NULL;
454: if (substr($res, -1) !== '<' && preg_match('#^<\?php\s*$#', $php)) {
455: $php = '';
456:
457: } elseif (is_array($next) && $next[0] === T_OPEN_TAG) {
458: if (!strspn($lastChar, ';{}:/')) {
459: $php .= $lastChar = ';';
460: }
461: if (substr($next[1], -1) === "\n") {
462: $php .= "\n";
463: }
464: $tokens->next();
465:
466: } elseif ($next) {
467: $res .= preg_replace('#;?(\s)*$#', '$1', $php) . $token[1];
468: if (strlen($res) - strrpos($res, "\n") > $lineLength
469: && (!is_array($next) || strpos($next[1], "\n") === FALSE)
470: ) {
471: $res .= "\n";
472: }
473: $php = '';
474:
475: } else {
476: if (!strspn($lastChar, '};')) {
477: $php .= ';';
478: }
479: }
480:
481: } elseif ($token[0] === T_ELSE || $token[0] === T_ELSEIF) {
482: if ($tokens[$key + 1] === ':' && $lastChar === '}') {
483: $php .= ';';
484: }
485: $lastChar = '';
486: $php .= $token[1];
487:
488: } else {
489: if (!in_array($token[0], array(T_WHITESPACE, T_COMMENT, T_DOC_COMMENT, T_OPEN_TAG))) {
490: $lastChar = '';
491: }
492: $php .= $token[1];
493: }
494: } else {
495: $php .= $lastChar = $token;
496: }
497: }
498: return $res . $php;
499: }
500:
501: }
502: