FormRenderer.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  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 $engine, CsrfTokenManagerInterface $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, bool $useDefaultThemes = true)
  44. {
  45. $this->engine->setTheme($view, $themes, $useDefaultThemes);
  46. }
  47. /**
  48. * {@inheritdoc}
  49. */
  50. public function renderCsrfToken(string $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, string $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, string $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. 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)
  232. {
  233. return ucfirst(strtolower(trim(preg_replace(['/([A-Z])/', '/[_\s]+/'], ['_$1', ' '], $text))));
  234. }
  235. /**
  236. * @internal
  237. */
  238. public function encodeCurrency(Environment $environment, string $text, string $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. }