vendor/symfony/form/FormRenderer.php line 73

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 FormRendererEngineInterface $engine;
  24.     private ?CsrfTokenManagerInterface $csrfTokenManager;
  25.     private array $blockNameHierarchyMap = [];
  26.     private array $hierarchyLevelMap = [];
  27.     private array $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(): FormRendererEngineInterface
  37.     {
  38.         return $this->engine;
  39.     }
  40.     /**
  41.      * {@inheritdoc}
  42.      */
  43.     public function setTheme(FormView $viewmixed $themesbool $useDefaultThemes true)
  44.     {
  45.         $this->engine->setTheme($view$themes$useDefaultThemes);
  46.     }
  47.     /**
  48.      * {@inheritdoc}
  49.      */
  50.     public function renderCsrfToken(string $tokenId): string
  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 $viewstring $blockName, array $variables = []): string
  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 $viewstring $blockNameSuffix, array $variables = []): string
  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.             throw new BadMethodCallException(sprintf('Field "%s" has already been rendered, save the result of previous render call to a variable and output that instead.'$view->vars['name']));
  110.         }
  111.         // The cache key for storing the variables and types
  112.         $viewCacheKey $view->vars[self::CACHE_KEY_VAR];
  113.         $viewAndSuffixCacheKey $viewCacheKey.$blockNameSuffix;
  114.         // In templates, we have to deal with two kinds of block hierarchies:
  115.         //
  116.         //   +---------+          +---------+
  117.         //   | Theme B | -------> | Theme A |
  118.         //   +---------+          +---------+
  119.         //
  120.         //   form_widget -------> form_widget
  121.         //       ^
  122.         //       |
  123.         //  choice_widget -----> choice_widget
  124.         //
  125.         // The first kind of hierarchy is the theme hierarchy. This allows to
  126.         // override the block "choice_widget" from Theme A in the extending
  127.         // Theme B. This kind of inheritance needs to be supported by the
  128.         // template engine and, for example, offers "parent()" or similar
  129.         // functions to fall back from the custom to the parent implementation.
  130.         //
  131.         // The second kind of hierarchy is the form type hierarchy. This allows
  132.         // to implement a custom "choice_widget" block (no matter in which theme),
  133.         // or to fallback to the block of the parent type, which would be
  134.         // "form_widget" in this example (again, no matter in which theme).
  135.         // If the designer wants to explicitly fallback to "form_widget" in their
  136.         // custom "choice_widget", for example because they only want to wrap
  137.         // a <div> around the original implementation, they can call the
  138.         // widget() function again to render the block for the parent type.
  139.         //
  140.         // The second kind is implemented in the following blocks.
  141.         if (!isset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey])) {
  142.             // INITIAL CALL
  143.             // Calculate the hierarchy of template blocks and start on
  144.             // the bottom level of the hierarchy (= "_<id>_<section>" block)
  145.             $blockNameHierarchy = [];
  146.             foreach ($view->vars['block_prefixes'] as $blockNamePrefix) {
  147.                 $blockNameHierarchy[] = $blockNamePrefix.'_'.$blockNameSuffix;
  148.             }
  149.             $hierarchyLevel \count($blockNameHierarchy) - 1;
  150.             $hierarchyInit true;
  151.         } else {
  152.             // RECURSIVE CALL
  153.             // If a block recursively calls searchAndRenderBlock() again, resume rendering
  154.             // using the parent type in the hierarchy.
  155.             $blockNameHierarchy $this->blockNameHierarchyMap[$viewAndSuffixCacheKey];
  156.             $hierarchyLevel $this->hierarchyLevelMap[$viewAndSuffixCacheKey] - 1;
  157.             $hierarchyInit false;
  158.         }
  159.         // The variables are cached globally for a view (instead of for the
  160.         // current suffix)
  161.         if (!isset($this->variableStack[$viewCacheKey])) {
  162.             $this->variableStack[$viewCacheKey] = [];
  163.             // The default variable scope contains all view variables, merged with
  164.             // the variables passed explicitly to the helper
  165.             $scopeVariables $view->vars;
  166.             $varInit true;
  167.         } else {
  168.             // Reuse the current scope and merge it with the explicitly passed variables
  169.             $scopeVariables end($this->variableStack[$viewCacheKey]);
  170.             $varInit false;
  171.         }
  172.         // Load the resource where this block can be found
  173.         $resource $this->engine->getResourceForBlockNameHierarchy($view$blockNameHierarchy$hierarchyLevel);
  174.         // Update the current hierarchy level to the one at which the resource was
  175.         // found. For example, if looking for "choice_widget", but only a resource
  176.         // is found for its parent "form_widget", then the level is updated here
  177.         // to the parent level.
  178.         $hierarchyLevel $this->engine->getResourceHierarchyLevel($view$blockNameHierarchy$hierarchyLevel);
  179.         // The actually existing block name in $resource
  180.         $blockName $blockNameHierarchy[$hierarchyLevel];
  181.         // Escape if no resource exists for this block
  182.         if (!$resource) {
  183.             if (\count($blockNameHierarchy) !== \count(array_unique($blockNameHierarchy))) {
  184.                 throw new LogicException(sprintf('Unable to render the form because the block names array contains duplicates: "%s".'implode('", "'array_reverse($blockNameHierarchy))));
  185.             }
  186.             throw new LogicException(sprintf('Unable to render the form as none of the following blocks exist: "%s".'implode('", "'array_reverse($blockNameHierarchy))));
  187.         }
  188.         // Merge the passed with the existing attributes
  189.         if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
  190.             $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
  191.         }
  192.         // Merge the passed with the exist *label* attributes
  193.         if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
  194.             $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
  195.         }
  196.         // Do not use array_replace_recursive(), otherwise array variables
  197.         // cannot be overwritten
  198.         $variables array_replace($scopeVariables$variables);
  199.         // In order to make recursive calls possible, we need to store the block hierarchy,
  200.         // the current level of the hierarchy and the variables so that this method can
  201.         // resume rendering one level higher of the hierarchy when it is called recursively.
  202.         //
  203.         // We need to store these values in maps (associative arrays) because within a
  204.         // call to widget() another call to widget() can be made, but for a different view
  205.         // object. These nested calls should not override each other.
  206.         $this->blockNameHierarchyMap[$viewAndSuffixCacheKey] = $blockNameHierarchy;
  207.         $this->hierarchyLevelMap[$viewAndSuffixCacheKey] = $hierarchyLevel;
  208.         // We also need to store the variables for the view so that we can render other
  209.         // blocks for the same view using the same variables as in the outer block.
  210.         $this->variableStack[$viewCacheKey][] = $variables;
  211.         // Do the rendering
  212.         $html $this->engine->renderBlock($view$resource$blockName$variables);
  213.         // Clear the stack
  214.         array_pop($this->variableStack[$viewCacheKey]);
  215.         // Clear the caches if they were filled for the first time within
  216.         // this function call
  217.         if ($hierarchyInit) {
  218.             unset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey], $this->hierarchyLevelMap[$viewAndSuffixCacheKey]);
  219.         }
  220.         if ($varInit) {
  221.             unset($this->variableStack[$viewCacheKey]);
  222.         }
  223.         if ($renderOnlyOnce) {
  224.             $view->setRendered();
  225.         }
  226.         return $html;
  227.     }
  228.     /**
  229.      * {@inheritdoc}
  230.      */
  231.     public function humanize(string $text): string
  232.     {
  233.         return ucfirst(strtolower(trim(preg_replace(['/([A-Z])/''/[_\s]+/'], ['_$1'' '], $text))));
  234.     }
  235.     /**
  236.      * @internal
  237.      */
  238.     public function encodeCurrency(Environment $environmentstring $textstring $widget ''): string
  239.     {
  240.         if ('UTF-8' === $charset $environment->getCharset()) {
  241.             $text htmlspecialchars($text\ENT_QUOTES \ENT_SUBSTITUTE'UTF-8');
  242.         } else {
  243.             $text htmlentities($text\ENT_QUOTES \ENT_SUBSTITUTE'UTF-8');
  244.             $text iconv('UTF-8'$charset$text);
  245.             $widget iconv('UTF-8'$charset$widget);
  246.         }
  247.         return str_replace('{{ widget }}'$widget$text);
  248.     }
  249. }