PhpEngine.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Templating;
  11. use Symfony\Component\Templating\Storage\Storage;
  12. use Symfony\Component\Templating\Storage\FileStorage;
  13. use Symfony\Component\Templating\Storage\StringStorage;
  14. use Symfony\Component\Templating\Helper\HelperInterface;
  15. use Symfony\Component\Templating\Loader\LoaderInterface;
  16. /**
  17. * PhpEngine is an engine able to render PHP templates.
  18. *
  19. * @author Fabien Potencier <fabien@symfony.com>
  20. */
  21. class PhpEngine implements EngineInterface, \ArrayAccess
  22. {
  23. protected $loader;
  24. protected $current;
  25. /**
  26. * @var HelperInterface[]
  27. */
  28. protected $helpers = array();
  29. protected $parents = array();
  30. protected $stack = array();
  31. protected $charset = 'UTF-8';
  32. protected $cache = array();
  33. protected $escapers = array();
  34. protected static $escaperCache = array();
  35. protected $globals = array();
  36. protected $parser;
  37. private $evalTemplate;
  38. private $evalParameters;
  39. /**
  40. * Constructor.
  41. *
  42. * @param TemplateNameParserInterface $parser A TemplateNameParserInterface instance
  43. * @param LoaderInterface $loader A loader instance
  44. * @param HelperInterface[] $helpers An array of helper instances
  45. */
  46. public function __construct(TemplateNameParserInterface $parser, LoaderInterface $loader, array $helpers = array())
  47. {
  48. $this->parser = $parser;
  49. $this->loader = $loader;
  50. $this->addHelpers($helpers);
  51. $this->initializeEscapers();
  52. foreach ($this->escapers as $context => $escaper) {
  53. $this->setEscaper($context, $escaper);
  54. }
  55. }
  56. /**
  57. * {@inheritdoc}
  58. *
  59. * @throws \InvalidArgumentException if the template does not exist
  60. */
  61. public function render($name, array $parameters = array())
  62. {
  63. $storage = $this->load($name);
  64. $key = hash('sha256', serialize($storage));
  65. $this->current = $key;
  66. $this->parents[$key] = null;
  67. // attach the global variables
  68. $parameters = array_replace($this->getGlobals(), $parameters);
  69. // render
  70. if (false === $content = $this->evaluate($storage, $parameters)) {
  71. throw new \RuntimeException(sprintf('The template "%s" cannot be rendered.', $this->parser->parse($name)));
  72. }
  73. // decorator
  74. if ($this->parents[$key]) {
  75. $slots = $this->get('slots');
  76. $this->stack[] = $slots->get('_content');
  77. $slots->set('_content', $content);
  78. $content = $this->render($this->parents[$key], $parameters);
  79. $slots->set('_content', array_pop($this->stack));
  80. }
  81. return $content;
  82. }
  83. /**
  84. * {@inheritdoc}
  85. */
  86. public function exists($name)
  87. {
  88. try {
  89. $this->load($name);
  90. } catch (\InvalidArgumentException $e) {
  91. return false;
  92. }
  93. return true;
  94. }
  95. /**
  96. * {@inheritdoc}
  97. */
  98. public function supports($name)
  99. {
  100. $template = $this->parser->parse($name);
  101. return 'php' === $template->get('engine');
  102. }
  103. /**
  104. * Evaluates a template.
  105. *
  106. * @param Storage $template The template to render
  107. * @param array $parameters An array of parameters to pass to the template
  108. *
  109. * @return string|false The evaluated template, or false if the engine is unable to render the template
  110. *
  111. * @throws \InvalidArgumentException
  112. */
  113. protected function evaluate(Storage $template, array $parameters = array())
  114. {
  115. $this->evalTemplate = $template;
  116. $this->evalParameters = $parameters;
  117. unset($template, $parameters);
  118. if (isset($this->evalParameters['this'])) {
  119. throw new \InvalidArgumentException('Invalid parameter (this)');
  120. }
  121. if (isset($this->evalParameters['view'])) {
  122. throw new \InvalidArgumentException('Invalid parameter (view)');
  123. }
  124. $view = $this;
  125. if ($this->evalTemplate instanceof FileStorage) {
  126. extract($this->evalParameters, EXTR_SKIP);
  127. $this->evalParameters = null;
  128. ob_start();
  129. require $this->evalTemplate;
  130. $this->evalTemplate = null;
  131. return ob_get_clean();
  132. } elseif ($this->evalTemplate instanceof StringStorage) {
  133. extract($this->evalParameters, EXTR_SKIP);
  134. $this->evalParameters = null;
  135. ob_start();
  136. eval('; ?>'.$this->evalTemplate.'<?php ;');
  137. $this->evalTemplate = null;
  138. return ob_get_clean();
  139. }
  140. return false;
  141. }
  142. /**
  143. * Gets a helper value.
  144. *
  145. * @param string $name The helper name
  146. *
  147. * @return HelperInterface The helper value
  148. *
  149. * @throws \InvalidArgumentException if the helper is not defined
  150. */
  151. public function offsetGet($name)
  152. {
  153. return $this->get($name);
  154. }
  155. /**
  156. * Returns true if the helper is defined.
  157. *
  158. * @param string $name The helper name
  159. *
  160. * @return bool true if the helper is defined, false otherwise
  161. */
  162. public function offsetExists($name)
  163. {
  164. return isset($this->helpers[$name]);
  165. }
  166. /**
  167. * Sets a helper.
  168. *
  169. * @param HelperInterface $name The helper instance
  170. * @param string $value An alias
  171. */
  172. public function offsetSet($name, $value)
  173. {
  174. $this->set($name, $value);
  175. }
  176. /**
  177. * Removes a helper.
  178. *
  179. * @param string $name The helper name
  180. *
  181. * @throws \LogicException
  182. */
  183. public function offsetUnset($name)
  184. {
  185. throw new \LogicException(sprintf('You can\'t unset a helper (%s).', $name));
  186. }
  187. /**
  188. * Adds some helpers.
  189. *
  190. * @param HelperInterface[] $helpers An array of helper
  191. */
  192. public function addHelpers(array $helpers)
  193. {
  194. foreach ($helpers as $alias => $helper) {
  195. $this->set($helper, is_int($alias) ? null : $alias);
  196. }
  197. }
  198. /**
  199. * Sets the helpers.
  200. *
  201. * @param HelperInterface[] $helpers An array of helper
  202. */
  203. public function setHelpers(array $helpers)
  204. {
  205. $this->helpers = array();
  206. $this->addHelpers($helpers);
  207. }
  208. /**
  209. * Sets a helper.
  210. *
  211. * @param HelperInterface $helper The helper instance
  212. * @param string $alias An alias
  213. */
  214. public function set(HelperInterface $helper, $alias = null)
  215. {
  216. $this->helpers[$helper->getName()] = $helper;
  217. if (null !== $alias) {
  218. $this->helpers[$alias] = $helper;
  219. }
  220. $helper->setCharset($this->charset);
  221. }
  222. /**
  223. * Returns true if the helper if defined.
  224. *
  225. * @param string $name The helper name
  226. *
  227. * @return bool true if the helper is defined, false otherwise
  228. */
  229. public function has($name)
  230. {
  231. return isset($this->helpers[$name]);
  232. }
  233. /**
  234. * Gets a helper value.
  235. *
  236. * @param string $name The helper name
  237. *
  238. * @return HelperInterface The helper instance
  239. *
  240. * @throws \InvalidArgumentException if the helper is not defined
  241. */
  242. public function get($name)
  243. {
  244. if (!isset($this->helpers[$name])) {
  245. throw new \InvalidArgumentException(sprintf('The helper "%s" is not defined.', $name));
  246. }
  247. return $this->helpers[$name];
  248. }
  249. /**
  250. * Decorates the current template with another one.
  251. *
  252. * @param string $template The decorator logical name
  253. */
  254. public function extend($template)
  255. {
  256. $this->parents[$this->current] = $template;
  257. }
  258. /**
  259. * Escapes a string by using the current charset.
  260. *
  261. * @param mixed $value A variable to escape
  262. * @param string $context The context name
  263. *
  264. * @return string The escaped value
  265. */
  266. public function escape($value, $context = 'html')
  267. {
  268. if (is_numeric($value)) {
  269. return $value;
  270. }
  271. // If we deal with a scalar value, we can cache the result to increase
  272. // the performance when the same value is escaped multiple times (e.g. loops)
  273. if (is_scalar($value)) {
  274. if (!isset(self::$escaperCache[$context][$value])) {
  275. self::$escaperCache[$context][$value] = call_user_func($this->getEscaper($context), $value);
  276. }
  277. return self::$escaperCache[$context][$value];
  278. }
  279. return call_user_func($this->getEscaper($context), $value);
  280. }
  281. /**
  282. * Sets the charset to use.
  283. *
  284. * @param string $charset The charset
  285. */
  286. public function setCharset($charset)
  287. {
  288. if ('UTF8' === $charset = strtoupper($charset)) {
  289. $charset = 'UTF-8'; // iconv on Windows requires "UTF-8" instead of "UTF8"
  290. }
  291. $this->charset = $charset;
  292. foreach ($this->helpers as $helper) {
  293. $helper->setCharset($this->charset);
  294. }
  295. }
  296. /**
  297. * Gets the current charset.
  298. *
  299. * @return string The current charset
  300. */
  301. public function getCharset()
  302. {
  303. return $this->charset;
  304. }
  305. /**
  306. * Adds an escaper for the given context.
  307. *
  308. * @param string $context The escaper context (html, js, ...)
  309. * @param callable $escaper A PHP callable
  310. */
  311. public function setEscaper($context, callable $escaper)
  312. {
  313. $this->escapers[$context] = $escaper;
  314. self::$escaperCache[$context] = array();
  315. }
  316. /**
  317. * Gets an escaper for a given context.
  318. *
  319. * @param string $context The context name
  320. *
  321. * @return callable $escaper A PHP callable
  322. *
  323. * @throws \InvalidArgumentException
  324. */
  325. public function getEscaper($context)
  326. {
  327. if (!isset($this->escapers[$context])) {
  328. throw new \InvalidArgumentException(sprintf('No registered escaper for context "%s".', $context));
  329. }
  330. return $this->escapers[$context];
  331. }
  332. /**
  333. * @param string $name
  334. * @param mixed $value
  335. */
  336. public function addGlobal($name, $value)
  337. {
  338. $this->globals[$name] = $value;
  339. }
  340. /**
  341. * Returns the assigned globals.
  342. *
  343. * @return array
  344. */
  345. public function getGlobals()
  346. {
  347. return $this->globals;
  348. }
  349. /**
  350. * Initializes the built-in escapers.
  351. *
  352. * Each function specifies a way for applying a transformation to a string
  353. * passed to it. The purpose is for the string to be "escaped" so it is
  354. * suitable for the format it is being displayed in.
  355. *
  356. * For example, the string: "It's required that you enter a username & password.\n"
  357. * If this were to be displayed as HTML it would be sensible to turn the
  358. * ampersand into '&amp;' and the apostrophe into '&aps;'. However if it were
  359. * going to be used as a string in JavaScript to be displayed in an alert box
  360. * it would be right to leave the string as-is, but c-escape the apostrophe and
  361. * the new line.
  362. *
  363. * For each function there is a define to avoid problems with strings being
  364. * incorrectly specified.
  365. */
  366. protected function initializeEscapers()
  367. {
  368. $flags = ENT_QUOTES | ENT_SUBSTITUTE;
  369. $this->escapers = array(
  370. 'html' =>
  371. /**
  372. * Runs the PHP function htmlspecialchars on the value passed.
  373. *
  374. * @param string $value the value to escape
  375. *
  376. * @return string the escaped value
  377. */
  378. function ($value) use ($flags) {
  379. // Numbers and Boolean values get turned into strings which can cause problems
  380. // with type comparisons (e.g. === or is_int() etc).
  381. return is_string($value) ? htmlspecialchars($value, $flags, $this->getCharset(), false) : $value;
  382. },
  383. 'js' =>
  384. /**
  385. * A function that escape all non-alphanumeric characters
  386. * into their \xHH or \uHHHH representations.
  387. *
  388. * @param string $value the value to escape
  389. *
  390. * @return string the escaped value
  391. */
  392. function ($value) {
  393. if ('UTF-8' != $this->getCharset()) {
  394. $value = iconv($this->getCharset(), 'UTF-8', $value);
  395. }
  396. $callback = function ($matches) {
  397. $char = $matches[0];
  398. // \xHH
  399. if (!isset($char[1])) {
  400. return '\\x'.substr('00'.bin2hex($char), -2);
  401. }
  402. // \uHHHH
  403. $char = iconv('UTF-8', 'UTF-16BE', $char);
  404. return '\\u'.substr('0000'.bin2hex($char), -4);
  405. };
  406. if (null === $value = preg_replace_callback('#[^\p{L}\p{N} ]#u', $callback, $value)) {
  407. throw new \InvalidArgumentException('The string to escape is not a valid UTF-8 string.');
  408. }
  409. if ('UTF-8' != $this->getCharset()) {
  410. $value = iconv('UTF-8', $this->getCharset(), $value);
  411. }
  412. return $value;
  413. },
  414. );
  415. self::$escaperCache = array();
  416. }
  417. /**
  418. * Gets the loader associated with this engine.
  419. *
  420. * @return LoaderInterface A LoaderInterface instance
  421. */
  422. public function getLoader()
  423. {
  424. return $this->loader;
  425. }
  426. /**
  427. * Loads the given template.
  428. *
  429. * @param string|TemplateReferenceInterface $name A template name or a TemplateReferenceInterface instance
  430. *
  431. * @return Storage A Storage instance
  432. *
  433. * @throws \InvalidArgumentException if the template cannot be found
  434. */
  435. protected function load($name)
  436. {
  437. $template = $this->parser->parse($name);
  438. $key = $template->getLogicalName();
  439. if (isset($this->cache[$key])) {
  440. return $this->cache[$key];
  441. }
  442. $storage = $this->loader->load($template);
  443. if (false === $storage) {
  444. throw new \InvalidArgumentException(sprintf('The template "%s" does not exist.', $template));
  445. }
  446. return $this->cache[$key] = $storage;
  447. }
  448. }