vendor/symfony/form/FormRenderer.php line 130

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\Form;
  11. use Symfony\Component\Form\Exception\BadMethodCallException;
  12. use Symfony\Component\Form\Exception\LogicException;
  13. use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
  14. use Twig\Environment;
  15. /**
  16.  * Renders a form into HTML using a rendering engine.
  17.  *
  18.  * @author Bernhard Schussek <bschussek@gmail.com>
  19.  */
  20. class FormRenderer implements FormRendererInterface
  21. {
  22.     public const CACHE_KEY_VAR 'unique_block_prefix';
  23.     private $engine;
  24.     private $csrfTokenManager;
  25.     private $blockNameHierarchyMap = [];
  26.     private $hierarchyLevelMap = [];
  27.     private $variableStack = [];
  28.     public function __construct(FormRendererEngineInterface $engineCsrfTokenManagerInterface $csrfTokenManager null)
  29.     {
  30.         $this->engine $engine;
  31.         $this->csrfTokenManager $csrfTokenManager;
  32.     }
  33.     /**
  34.      * {@inheritdoc}
  35.      */
  36.     public function getEngine()
  37.     {
  38.         return $this->engine;
  39.     }
  40.     /**
  41.      * {@inheritdoc}
  42.      */
  43.     public function setTheme(FormView $view$themes$useDefaultThemes true)
  44.     {
  45.         $this->engine->setTheme($view$themes$useDefaultThemes);
  46.     }
  47.     /**
  48.      * {@inheritdoc}
  49.      */
  50.     public function renderCsrfToken($tokenId)
  51.     {
  52.         if (null === $this->csrfTokenManager) {
  53.             throw new BadMethodCallException('CSRF tokens can only be generated if a CsrfTokenManagerInterface is injected in FormRenderer::__construct(). Try running "composer require symfony/security-csrf".');
  54.         }
  55.         return $this->csrfTokenManager->getToken($tokenId)->getValue();
  56.     }
  57.     /**
  58.      * {@inheritdoc}
  59.      */
  60.     public function renderBlock(FormView $view$blockName, array $variables = [])
  61.     {
  62.         $resource $this->engine->getResourceForBlockName($view$blockName);
  63.         if (!$resource) {
  64.             throw new LogicException(sprintf('No block "%s" found while rendering the form.'$blockName));
  65.         }
  66.         $viewCacheKey $view->vars[self::CACHE_KEY_VAR];
  67.         // The variables are cached globally for a view (instead of for the
  68.         // current suffix)
  69.         if (!isset($this->variableStack[$viewCacheKey])) {
  70.             $this->variableStack[$viewCacheKey] = [];
  71.             // The default variable scope contains all view variables, merged with
  72.             // the variables passed explicitly to the helper
  73.             $scopeVariables $view->vars;
  74.             $varInit true;
  75.         } else {
  76.             // Reuse the current scope and merge it with the explicitly passed variables
  77.             $scopeVariables end($this->variableStack[$viewCacheKey]);
  78.             $varInit false;
  79.         }
  80.         // Merge the passed with the existing attributes
  81.         if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
  82.             $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
  83.         }
  84.         // Merge the passed with the exist *label* attributes
  85.         if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
  86.             $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
  87.         }
  88.         // Do not use array_replace_recursive(), otherwise array variables
  89.         // cannot be overwritten
  90.         $variables array_replace($scopeVariables$variables);
  91.         $this->variableStack[$viewCacheKey][] = $variables;
  92.         // Do the rendering
  93.         $html $this->engine->renderBlock($view$resource$blockName$variables);
  94.         // Clear the stack
  95.         array_pop($this->variableStack[$viewCacheKey]);
  96.         if ($varInit) {
  97.             unset($this->variableStack[$viewCacheKey]);
  98.         }
  99.         return $html;
  100.     }
  101.     /**
  102.      * {@inheritdoc}
  103.      */
  104.     public function searchAndRenderBlock(FormView $view$blockNameSuffix, array $variables = [])
  105.     {
  106.         $renderOnlyOnce 'row' === $blockNameSuffix || 'widget' === $blockNameSuffix;
  107.         if ($renderOnlyOnce && $view->isRendered()) {
  108.             // This is not allowed, because it would result in rendering same IDs multiple times, which is not valid.
  109.             @trigger_error(sprintf('You are calling "form_%s" for field "%s" which has already been rendered before, trying to render fields which were already rendered is deprecated since Symfony 4.2 and will throw an exception in 5.0.'$blockNameSuffix$view->vars['name']), \E_USER_DEPRECATED);
  110.             // throw new BadMethodCallException(sprintf('Field "%s" has already been rendered. Save result of previous  render call to variable and output that instead.', $view->vars['name']));
  111.             return '';
  112.         }
  113.         // The cache key for storing the variables and types
  114.         $viewCacheKey $view->vars[self::CACHE_KEY_VAR];
  115.         $viewAndSuffixCacheKey $viewCacheKey.$blockNameSuffix;
  116.         // In templates, we have to deal with two kinds of block hierarchies:
  117.         //
  118.         //   +---------+          +---------+
  119.         //   | Theme B | -------> | Theme A |
  120.         //   +---------+          +---------+
  121.         //
  122.         //   form_widget -------> form_widget
  123.         //       ^
  124.         //       |
  125.         //  choice_widget -----> choice_widget
  126.         //
  127.         // The first kind of hierarchy is the theme hierarchy. This allows to
  128.         // override the block "choice_widget" from Theme A in the extending
  129.         // Theme B. This kind of inheritance needs to be supported by the
  130.         // template engine and, for example, offers "parent()" or similar
  131.         // functions to fall back from the custom to the parent implementation.
  132.         //
  133.         // The second kind of hierarchy is the form type hierarchy. This allows
  134.         // to implement a custom "choice_widget" block (no matter in which theme),
  135.         // or to fallback to the block of the parent type, which would be
  136.         // "form_widget" in this example (again, no matter in which theme).
  137.         // If the designer wants to explicitly fallback to "form_widget" in their
  138.         // custom "choice_widget", for example because they only want to wrap
  139.         // a <div> around the original implementation, they can call the
  140.         // widget() function again to render the block for the parent type.
  141.         //
  142.         // The second kind is implemented in the following blocks.
  143.         if (!isset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey])) {
  144.             // INITIAL CALL
  145.             // Calculate the hierarchy of template blocks and start on
  146.             // the bottom level of the hierarchy (= "_<id>_<section>" block)
  147.             $blockNameHierarchy = [];
  148.             foreach ($view->vars['block_prefixes'] as $blockNamePrefix) {
  149.                 $blockNameHierarchy[] = $blockNamePrefix.'_'.$blockNameSuffix;
  150.             }
  151.             $hierarchyLevel = \count($blockNameHierarchy) - 1;
  152.             $hierarchyInit true;
  153.         } else {
  154.             // RECURSIVE CALL
  155.             // If a block recursively calls searchAndRenderBlock() again, resume rendering
  156.             // using the parent type in the hierarchy.
  157.             $blockNameHierarchy $this->blockNameHierarchyMap[$viewAndSuffixCacheKey];
  158.             $hierarchyLevel $this->hierarchyLevelMap[$viewAndSuffixCacheKey] - 1;
  159.             $hierarchyInit false;
  160.         }
  161.         // The variables are cached globally for a view (instead of for the
  162.         // current suffix)
  163.         if (!isset($this->variableStack[$viewCacheKey])) {
  164.             $this->variableStack[$viewCacheKey] = [];
  165.             // The default variable scope contains all view variables, merged with
  166.             // the variables passed explicitly to the helper
  167.             $scopeVariables $view->vars;
  168.             $varInit true;
  169.         } else {
  170.             // Reuse the current scope and merge it with the explicitly passed variables
  171.             $scopeVariables end($this->variableStack[$viewCacheKey]);
  172.             $varInit false;
  173.         }
  174.         // Load the resource where this block can be found
  175.         $resource $this->engine->getResourceForBlockNameHierarchy($view$blockNameHierarchy$hierarchyLevel);
  176.         // Update the current hierarchy level to the one at which the resource was
  177.         // found. For example, if looking for "choice_widget", but only a resource
  178.         // is found for its parent "form_widget", then the level is updated here
  179.         // to the parent level.
  180.         $hierarchyLevel $this->engine->getResourceHierarchyLevel($view$blockNameHierarchy$hierarchyLevel);
  181.         // The actually existing block name in $resource
  182.         $blockName $blockNameHierarchy[$hierarchyLevel];
  183.         // Escape if no resource exists for this block
  184.         if (!$resource) {
  185.             if (\count($blockNameHierarchy) !== \count(array_unique($blockNameHierarchy))) {
  186.                 throw new LogicException(sprintf('Unable to render the form because the block names array contains duplicates: "%s".'implode('", "'array_reverse($blockNameHierarchy))));
  187.             }
  188.             throw new LogicException(sprintf('Unable to render the form as none of the following blocks exist: "%s".'implode('", "'array_reverse($blockNameHierarchy))));
  189.         }
  190.         // Merge the passed with the existing attributes
  191.         if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
  192.             $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
  193.         }
  194.         // Merge the passed with the exist *label* attributes
  195.         if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
  196.             $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
  197.         }
  198.         // Do not use array_replace_recursive(), otherwise array variables
  199.         // cannot be overwritten
  200.         $variables array_replace($scopeVariables$variables);
  201.         // In order to make recursive calls possible, we need to store the block hierarchy,
  202.         // the current level of the hierarchy and the variables so that this method can
  203.         // resume rendering one level higher of the hierarchy when it is called recursively.
  204.         //
  205.         // We need to store these values in maps (associative arrays) because within a
  206.         // call to widget() another call to widget() can be made, but for a different view
  207.         // object. These nested calls should not override each other.
  208.         $this->blockNameHierarchyMap[$viewAndSuffixCacheKey] = $blockNameHierarchy;
  209.         $this->hierarchyLevelMap[$viewAndSuffixCacheKey] = $hierarchyLevel;
  210.         // We also need to store the variables for the view so that we can render other
  211.         // blocks for the same view using the same variables as in the outer block.
  212.         $this->variableStack[$viewCacheKey][] = $variables;
  213.         // Do the rendering
  214.         $html $this->engine->renderBlock($view$resource$blockName$variables);
  215.         // Clear the stack
  216.         array_pop($this->variableStack[$viewCacheKey]);
  217.         // Clear the caches if they were filled for the first time within
  218.         // this function call
  219.         if ($hierarchyInit) {
  220.             unset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey], $this->hierarchyLevelMap[$viewAndSuffixCacheKey]);
  221.         }
  222.         if ($varInit) {
  223.             unset($this->variableStack[$viewCacheKey]);
  224.         }
  225.         if ($renderOnlyOnce) {
  226.             $view->setRendered();
  227.         }
  228.         return $html;
  229.     }
  230.     /**
  231.      * {@inheritdoc}
  232.      */
  233.     public function humanize($text)
  234.     {
  235.         return ucfirst(strtolower(trim(preg_replace(['/([A-Z])/''/[_\s]+/'], ['_$1'' '], $text))));
  236.     }
  237.     /**
  238.      * @internal
  239.      */
  240.     public function encodeCurrency(Environment $environmentstring $textstring $widget ''): string
  241.     {
  242.         if ('UTF-8' === $charset $environment->getCharset()) {
  243.             $text htmlspecialchars($text, \ENT_QUOTES | \ENT_SUBSTITUTE'UTF-8');
  244.         } else {
  245.             $text htmlentities($text, \ENT_QUOTES | \ENT_SUBSTITUTE'UTF-8');
  246.             $text iconv('UTF-8'$charset$text);
  247.             $widget iconv('UTF-8'$charset$widget);
  248.         }
  249.         return str_replace('{{ widget }}'$widget$text);
  250.     }
  251. }