Engine.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. <?php
  2. /*
  3. * This file is part of Mustache.php.
  4. *
  5. * (c) 2012 Justin Hileman
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. /**
  11. * A Mustache implementation in PHP.
  12. *
  13. * {@link http://defunkt.github.com/mustache}
  14. *
  15. * Mustache is a framework-agnostic logic-less templating language. It enforces separation of view
  16. * logic from template files. In fact, it is not even possible to embed logic in the template.
  17. *
  18. * This is very, very rad.
  19. *
  20. * @author Justin Hileman {@link http://justinhileman.com}
  21. */
  22. class Mustache_Engine
  23. {
  24. const VERSION = '2.4.1';
  25. const SPEC_VERSION = '1.1.2';
  26. const PRAGMA_FILTERS = 'FILTERS';
  27. // Template cache
  28. private $templates = array();
  29. // Environment
  30. private $templateClassPrefix = '__Mustache_';
  31. private $cache = null;
  32. private $cacheFileMode = null;
  33. private $loader;
  34. private $partialsLoader;
  35. private $helpers;
  36. private $escape;
  37. private $entityFlags = ENT_COMPAT;
  38. private $charset = 'UTF-8';
  39. private $logger;
  40. private $strictCallables = false;
  41. /**
  42. * Mustache class constructor.
  43. *
  44. * Passing an $options array allows overriding certain Mustache options during instantiation:
  45. *
  46. * $options = array(
  47. * // The class prefix for compiled templates. Defaults to '__Mustache_'.
  48. * 'template_class_prefix' => '__MyTemplates_',
  49. *
  50. * // A cache directory for compiled templates. Mustache will not cache templates unless this is set
  51. * 'cache' => dirname(__FILE__).'/tmp/cache/mustache',
  52. *
  53. * // Override default permissions for cache files. Defaults to using the system-defined umask. It is
  54. * // *strongly* recommended that you configure your umask properly rather than overriding permissions here.
  55. * 'cache_file_mode' => 0666,
  56. *
  57. * // A Mustache template loader instance. Uses a StringLoader if not specified.
  58. * 'loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views'),
  59. *
  60. * // A Mustache loader instance for partials.
  61. * 'partials_loader' => new Mustache_Loader_FilesystemLoader(dirname(__FILE__).'/views/partials'),
  62. *
  63. * // An array of Mustache partials. Useful for quick-and-dirty string template loading, but not as
  64. * // efficient or lazy as a Filesystem (or database) loader.
  65. * 'partials' => array('foo' => file_get_contents(dirname(__FILE__).'/views/partials/foo.mustache')),
  66. *
  67. * // An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order
  68. * // sections), or any other valid Mustache context value. They will be prepended to the context stack,
  69. * // so they will be available in any template loaded by this Mustache instance.
  70. * 'helpers' => array('i18n' => function($text) {
  71. * // do something translatey here...
  72. * }),
  73. *
  74. * // An 'escape' callback, responsible for escaping double-mustache variables.
  75. * 'escape' => function($value) {
  76. * return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
  77. * },
  78. *
  79. * // Type argument for `htmlspecialchars`. Defaults to ENT_COMPAT. You may prefer ENT_QUOTES.
  80. * 'entity_flags' => ENT_QUOTES,
  81. *
  82. * // Character set for `htmlspecialchars`. Defaults to 'UTF-8'. Use 'UTF-8'.
  83. * 'charset' => 'ISO-8859-1',
  84. *
  85. * // A Mustache Logger instance. No logging will occur unless this is set. Using a PSR-3 compatible
  86. * // logging library -- such as Monolog -- is highly recommended. A simple stream logger implementation is
  87. * // available as well:
  88. * 'logger' => new Mustache_Logger_StreamLogger('php://stderr'),
  89. *
  90. * // Only treat Closure instances and invokable classes as callable. If true, values like
  91. * // `array('ClassName', 'methodName')` and `array($classInstance, 'methodName')`, which are traditionally
  92. * // "callable" in PHP, are not called to resolve variables for interpolation or section contexts. This
  93. * // helps protect against arbitrary code execution when user input is passed directly into the template.
  94. * // This currently defaults to false, but will default to true in v3.0.
  95. * 'strict_callables' => true,
  96. * );
  97. *
  98. * @throws Mustache_Exception_InvalidArgumentException If `escape` option is not callable.
  99. *
  100. * @param array $options (default: array())
  101. */
  102. public function __construct(array $options = array())
  103. {
  104. if (isset($options['template_class_prefix'])) {
  105. $this->templateClassPrefix = $options['template_class_prefix'];
  106. }
  107. if (isset($options['cache'])) {
  108. $this->cache = $options['cache'];
  109. }
  110. if (isset($options['cache_file_mode'])) {
  111. $this->cacheFileMode = $options['cache_file_mode'];
  112. }
  113. if (isset($options['loader'])) {
  114. $this->setLoader($options['loader']);
  115. }
  116. if (isset($options['partials_loader'])) {
  117. $this->setPartialsLoader($options['partials_loader']);
  118. }
  119. if (isset($options['partials'])) {
  120. $this->setPartials($options['partials']);
  121. }
  122. if (isset($options['helpers'])) {
  123. $this->setHelpers($options['helpers']);
  124. }
  125. if (isset($options['escape'])) {
  126. if (!is_callable($options['escape'])) {
  127. throw new Mustache_Exception_InvalidArgumentException('Mustache Constructor "escape" option must be callable');
  128. }
  129. $this->escape = $options['escape'];
  130. }
  131. if (isset($options['entity_flags'])) {
  132. $this->entityFlags = $options['entity_flags'];
  133. }
  134. if (isset($options['charset'])) {
  135. $this->charset = $options['charset'];
  136. }
  137. if (isset($options['logger'])) {
  138. $this->setLogger($options['logger']);
  139. }
  140. if (isset($options['strict_callables'])) {
  141. $this->strictCallables = $options['strict_callables'];
  142. }
  143. }
  144. /**
  145. * Shortcut 'render' invocation.
  146. *
  147. * Equivalent to calling `$mustache->loadTemplate($template)->render($context);`
  148. *
  149. * @see Mustache_Engine::loadTemplate
  150. * @see Mustache_Template::render
  151. *
  152. * @param string $template
  153. * @param mixed $context (default: array())
  154. *
  155. * @return string Rendered template
  156. */
  157. public function render($template, $context = array())
  158. {
  159. return $this->loadTemplate($template)->render($context);
  160. }
  161. /**
  162. * Get the current Mustache escape callback.
  163. *
  164. * @return mixed Callable or null
  165. */
  166. public function getEscape()
  167. {
  168. return $this->escape;
  169. }
  170. /**
  171. * Get the current Mustache entitity type to escape.
  172. *
  173. * @return int
  174. */
  175. public function getEntityFlags()
  176. {
  177. return $this->entityFlags;
  178. }
  179. /**
  180. * Get the current Mustache character set.
  181. *
  182. * @return string
  183. */
  184. public function getCharset()
  185. {
  186. return $this->charset;
  187. }
  188. /**
  189. * Set the Mustache template Loader instance.
  190. *
  191. * @param Mustache_Loader $loader
  192. */
  193. public function setLoader(Mustache_Loader $loader)
  194. {
  195. $this->loader = $loader;
  196. }
  197. /**
  198. * Get the current Mustache template Loader instance.
  199. *
  200. * If no Loader instance has been explicitly specified, this method will instantiate and return
  201. * a StringLoader instance.
  202. *
  203. * @return Mustache_Loader
  204. */
  205. public function getLoader()
  206. {
  207. if (!isset($this->loader)) {
  208. $this->loader = new Mustache_Loader_StringLoader;
  209. }
  210. return $this->loader;
  211. }
  212. /**
  213. * Set the Mustache partials Loader instance.
  214. *
  215. * @param Mustache_Loader $partialsLoader
  216. */
  217. public function setPartialsLoader(Mustache_Loader $partialsLoader)
  218. {
  219. $this->partialsLoader = $partialsLoader;
  220. }
  221. /**
  222. * Get the current Mustache partials Loader instance.
  223. *
  224. * If no Loader instance has been explicitly specified, this method will instantiate and return
  225. * an ArrayLoader instance.
  226. *
  227. * @return Mustache_Loader
  228. */
  229. public function getPartialsLoader()
  230. {
  231. if (!isset($this->partialsLoader)) {
  232. $this->partialsLoader = new Mustache_Loader_ArrayLoader;
  233. }
  234. return $this->partialsLoader;
  235. }
  236. /**
  237. * Set partials for the current partials Loader instance.
  238. *
  239. * @throws Mustache_Exception_RuntimeException If the current Loader instance is immutable
  240. *
  241. * @param array $partials (default: array())
  242. */
  243. public function setPartials(array $partials = array())
  244. {
  245. if (!isset($this->partialsLoader)) {
  246. $this->partialsLoader = new Mustache_Loader_ArrayLoader;
  247. }
  248. if (!$this->partialsLoader instanceof Mustache_Loader_MutableLoader) {
  249. throw new Mustache_Exception_RuntimeException('Unable to set partials on an immutable Mustache Loader instance');
  250. }
  251. $this->partialsLoader->setTemplates($partials);
  252. }
  253. /**
  254. * Set an array of Mustache helpers.
  255. *
  256. * An array of 'helpers'. Helpers can be global variables or objects, closures (e.g. for higher order sections), or
  257. * any other valid Mustache context value. They will be prepended to the context stack, so they will be available in
  258. * any template loaded by this Mustache instance.
  259. *
  260. * @throws Mustache_Exception_InvalidArgumentException if $helpers is not an array or Traversable
  261. *
  262. * @param array|Traversable $helpers
  263. */
  264. public function setHelpers($helpers)
  265. {
  266. if (!is_array($helpers) && !$helpers instanceof Traversable) {
  267. throw new Mustache_Exception_InvalidArgumentException('setHelpers expects an array of helpers');
  268. }
  269. $this->getHelpers()->clear();
  270. foreach ($helpers as $name => $helper) {
  271. $this->addHelper($name, $helper);
  272. }
  273. }
  274. /**
  275. * Get the current set of Mustache helpers.
  276. *
  277. * @see Mustache_Engine::setHelpers
  278. *
  279. * @return Mustache_HelperCollection
  280. */
  281. public function getHelpers()
  282. {
  283. if (!isset($this->helpers)) {
  284. $this->helpers = new Mustache_HelperCollection;
  285. }
  286. return $this->helpers;
  287. }
  288. /**
  289. * Add a new Mustache helper.
  290. *
  291. * @see Mustache_Engine::setHelpers
  292. *
  293. * @param string $name
  294. * @param mixed $helper
  295. */
  296. public function addHelper($name, $helper)
  297. {
  298. $this->getHelpers()->add($name, $helper);
  299. }
  300. /**
  301. * Get a Mustache helper by name.
  302. *
  303. * @see Mustache_Engine::setHelpers
  304. *
  305. * @param string $name
  306. *
  307. * @return mixed Helper
  308. */
  309. public function getHelper($name)
  310. {
  311. return $this->getHelpers()->get($name);
  312. }
  313. /**
  314. * Check whether this Mustache instance has a helper.
  315. *
  316. * @see Mustache_Engine::setHelpers
  317. *
  318. * @param string $name
  319. *
  320. * @return boolean True if the helper is present
  321. */
  322. public function hasHelper($name)
  323. {
  324. return $this->getHelpers()->has($name);
  325. }
  326. /**
  327. * Remove a helper by name.
  328. *
  329. * @see Mustache_Engine::setHelpers
  330. *
  331. * @param string $name
  332. */
  333. public function removeHelper($name)
  334. {
  335. $this->getHelpers()->remove($name);
  336. }
  337. /**
  338. * Set the Mustache Logger instance.
  339. *
  340. * @throws Mustache_Exception_InvalidArgumentException If logger is not an instance of Mustache_Logger or Psr\Log\LoggerInterface.
  341. *
  342. * @param Mustache_Logger|Psr\Log\LoggerInterface $logger
  343. */
  344. public function setLogger($logger = null)
  345. {
  346. if ($logger !== null && !($logger instanceof Mustache_Logger || is_a($logger, 'Psr\\Log\\LoggerInterface'))) {
  347. throw new Mustache_Exception_InvalidArgumentException('Expected an instance of Mustache_Logger or Psr\\Log\\LoggerInterface.');
  348. }
  349. $this->logger = $logger;
  350. }
  351. /**
  352. * Get the current Mustache Logger instance.
  353. *
  354. * @return Mustache_Logger|Psr\Log\LoggerInterface
  355. */
  356. public function getLogger()
  357. {
  358. return $this->logger;
  359. }
  360. /**
  361. * Set the Mustache Tokenizer instance.
  362. *
  363. * @param Mustache_Tokenizer $tokenizer
  364. */
  365. public function setTokenizer(Mustache_Tokenizer $tokenizer)
  366. {
  367. $this->tokenizer = $tokenizer;
  368. }
  369. /**
  370. * Get the current Mustache Tokenizer instance.
  371. *
  372. * If no Tokenizer instance has been explicitly specified, this method will instantiate and return a new one.
  373. *
  374. * @return Mustache_Tokenizer
  375. */
  376. public function getTokenizer()
  377. {
  378. if (!isset($this->tokenizer)) {
  379. $this->tokenizer = new Mustache_Tokenizer;
  380. }
  381. return $this->tokenizer;
  382. }
  383. /**
  384. * Set the Mustache Parser instance.
  385. *
  386. * @param Mustache_Parser $parser
  387. */
  388. public function setParser(Mustache_Parser $parser)
  389. {
  390. $this->parser = $parser;
  391. }
  392. /**
  393. * Get the current Mustache Parser instance.
  394. *
  395. * If no Parser instance has been explicitly specified, this method will instantiate and return a new one.
  396. *
  397. * @return Mustache_Parser
  398. */
  399. public function getParser()
  400. {
  401. if (!isset($this->parser)) {
  402. $this->parser = new Mustache_Parser;
  403. }
  404. return $this->parser;
  405. }
  406. /**
  407. * Set the Mustache Compiler instance.
  408. *
  409. * @param Mustache_Compiler $compiler
  410. */
  411. public function setCompiler(Mustache_Compiler $compiler)
  412. {
  413. $this->compiler = $compiler;
  414. }
  415. /**
  416. * Get the current Mustache Compiler instance.
  417. *
  418. * If no Compiler instance has been explicitly specified, this method will instantiate and return a new one.
  419. *
  420. * @return Mustache_Compiler
  421. */
  422. public function getCompiler()
  423. {
  424. if (!isset($this->compiler)) {
  425. $this->compiler = new Mustache_Compiler;
  426. }
  427. return $this->compiler;
  428. }
  429. /**
  430. * Helper method to generate a Mustache template class.
  431. *
  432. * @param string $source
  433. *
  434. * @return string Mustache Template class name
  435. */
  436. public function getTemplateClassName($source)
  437. {
  438. return $this->templateClassPrefix . md5(sprintf(
  439. 'version:%s,escape:%s,entity_flags:%i,charset:%s,strict_callables:%s,source:%s',
  440. self::VERSION,
  441. isset($this->escape) ? 'custom' : 'default',
  442. $this->entityFlags,
  443. $this->charset,
  444. $this->strictCallables ? 'true' : 'false',
  445. $source
  446. ));
  447. }
  448. /**
  449. * Load a Mustache Template by name.
  450. *
  451. * @param string $name
  452. *
  453. * @return Mustache_Template
  454. */
  455. public function loadTemplate($name)
  456. {
  457. return $this->loadSource($this->getLoader()->load($name));
  458. }
  459. /**
  460. * Load a Mustache partial Template by name.
  461. *
  462. * This is a helper method used internally by Template instances for loading partial templates. You can most likely
  463. * ignore it completely.
  464. *
  465. * @param string $name
  466. *
  467. * @return Mustache_Template
  468. */
  469. public function loadPartial($name)
  470. {
  471. try {
  472. if (isset($this->partialsLoader)) {
  473. $loader = $this->partialsLoader;
  474. } elseif (isset($this->loader) && !$this->loader instanceof Mustache_Loader_StringLoader) {
  475. $loader = $this->loader;
  476. } else {
  477. throw new Mustache_Exception_UnknownTemplateException($name);
  478. }
  479. return $this->loadSource($loader->load($name));
  480. } catch (Mustache_Exception_UnknownTemplateException $e) {
  481. // If the named partial cannot be found, log then return null.
  482. $this->log(
  483. Mustache_Logger::WARNING,
  484. 'Partial not found: "{name}"',
  485. array('name' => $e->getTemplateName())
  486. );
  487. }
  488. }
  489. /**
  490. * Load a Mustache lambda Template by source.
  491. *
  492. * This is a helper method used by Template instances to generate subtemplates for Lambda sections. You can most
  493. * likely ignore it completely.
  494. *
  495. * @param string $source
  496. * @param string $delims (default: null)
  497. *
  498. * @return Mustache_Template
  499. */
  500. public function loadLambda($source, $delims = null)
  501. {
  502. if ($delims !== null) {
  503. $source = $delims . "\n" . $source;
  504. }
  505. return $this->loadSource($source);
  506. }
  507. /**
  508. * Instantiate and return a Mustache Template instance by source.
  509. *
  510. * @see Mustache_Engine::loadTemplate
  511. * @see Mustache_Engine::loadPartial
  512. * @see Mustache_Engine::loadLambda
  513. *
  514. * @param string $source
  515. *
  516. * @return Mustache_Template
  517. */
  518. private function loadSource($source)
  519. {
  520. $className = $this->getTemplateClassName($source);
  521. if (!isset($this->templates[$className])) {
  522. if (!class_exists($className, false)) {
  523. if ($fileName = $this->getCacheFilename($source)) {
  524. if (!is_file($fileName)) {
  525. $this->log(
  526. Mustache_Logger::DEBUG,
  527. 'Writing "{className}" class to template cache: "{fileName}"',
  528. array('className' => $className, 'fileName' => $fileName)
  529. );
  530. $this->writeCacheFile($fileName, $this->compile($source));
  531. }
  532. require_once $fileName;
  533. } else {
  534. $this->log(
  535. Mustache_Logger::WARNING,
  536. 'Template cache disabled, evaluating "{className}" class at runtime',
  537. array('className' => $className)
  538. );
  539. eval('?>'.$this->compile($source));
  540. }
  541. }
  542. $this->log(
  543. Mustache_Logger::DEBUG,
  544. 'Instantiating template: "{className}"',
  545. array('className' => $className)
  546. );
  547. $this->templates[$className] = new $className($this);
  548. }
  549. return $this->templates[$className];
  550. }
  551. /**
  552. * Helper method to tokenize a Mustache template.
  553. *
  554. * @see Mustache_Tokenizer::scan
  555. *
  556. * @param string $source
  557. *
  558. * @return array Tokens
  559. */
  560. private function tokenize($source)
  561. {
  562. return $this->getTokenizer()->scan($source);
  563. }
  564. /**
  565. * Helper method to parse a Mustache template.
  566. *
  567. * @see Mustache_Parser::parse
  568. *
  569. * @param string $source
  570. *
  571. * @return array Token tree
  572. */
  573. private function parse($source)
  574. {
  575. return $this->getParser()->parse($this->tokenize($source));
  576. }
  577. /**
  578. * Helper method to compile a Mustache template.
  579. *
  580. * @see Mustache_Compiler::compile
  581. *
  582. * @param string $source
  583. *
  584. * @return string generated Mustache template class code
  585. */
  586. private function compile($source)
  587. {
  588. $tree = $this->parse($source);
  589. $name = $this->getTemplateClassName($source);
  590. $this->log(
  591. Mustache_Logger::INFO,
  592. 'Compiling template to "{className}" class',
  593. array('className' => $name)
  594. );
  595. return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags);
  596. }
  597. /**
  598. * Helper method to generate a Mustache Template class cache filename.
  599. *
  600. * @param string $source
  601. *
  602. * @return string Mustache Template class cache filename
  603. */
  604. private function getCacheFilename($source)
  605. {
  606. if ($this->cache) {
  607. return sprintf('%s/%s.php', $this->cache, $this->getTemplateClassName($source));
  608. }
  609. }
  610. /**
  611. * Helper method to dump a generated Mustache Template subclass to the file cache.
  612. *
  613. * @throws Mustache_Exception_RuntimeException if unable to create the cache directory or write to $fileName.
  614. *
  615. * @param string $fileName
  616. * @param string $source
  617. *
  618. * @codeCoverageIgnore
  619. */
  620. private function writeCacheFile($fileName, $source)
  621. {
  622. $dirName = dirname($fileName);
  623. if (!is_dir($dirName)) {
  624. $this->log(
  625. Mustache_Logger::INFO,
  626. 'Creating Mustache template cache directory: "{dirName}"',
  627. array('dirName' => $dirName)
  628. );
  629. @mkdir($dirName, 0777, true);
  630. if (!is_dir($dirName)) {
  631. throw new Mustache_Exception_RuntimeException(sprintf('Failed to create cache directory "%s".', $dirName));
  632. }
  633. }
  634. $this->log(
  635. Mustache_Logger::DEBUG,
  636. 'Caching compiled template to "{fileName}"',
  637. array('fileName' => $fileName)
  638. );
  639. $tempFile = tempnam($dirName, basename($fileName));
  640. if (false !== @file_put_contents($tempFile, $source)) {
  641. if (@rename($tempFile, $fileName)) {
  642. $mode = isset($this->cacheFileMode) ? $this->cacheFileMode : (0666 & ~umask());
  643. @chmod($fileName, $mode);
  644. return;
  645. }
  646. $this->log(
  647. Mustache_Logger::ERROR,
  648. 'Unable to rename Mustache temp cache file: "{tempName}" -> "{fileName}"',
  649. array('tempName' => $tempFile, 'fileName' => $fileName)
  650. );
  651. }
  652. throw new Mustache_Exception_RuntimeException(sprintf('Failed to write cache file "%s".', $fileName));
  653. }
  654. /**
  655. * Add a log record if logging is enabled.
  656. *
  657. * @param integer $level The logging level
  658. * @param string $message The log message
  659. * @param array $context The log context
  660. */
  661. private function log($level, $message, array $context = array())
  662. {
  663. if (isset($this->logger)) {
  664. $this->logger->log($level, $message, $context);
  665. }
  666. }
  667. }