FormRenderer.php 12 KB

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