vendor/symfony/templating/PhpEngine.php line 214

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