4 * This file is part of the Symfony package.
6 * (c) Fabien Potencier <fabien@symfony.com>
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Symfony\Component\Form
;
14 use Symfony\Component\Form\Exception\LogicException
;
15 use Symfony\Component\Form\Exception\BadMethodCallException
;
16 use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface
;
19 * Renders a form into HTML using a rendering engine.
21 * @author Bernhard Schussek <bschussek@gmail.com>
23 class FormRenderer
implements FormRendererInterface
25 const CACHE_KEY_VAR
= 'unique_block_prefix';
28 * @var FormRendererEngineInterface
33 * @var CsrfProviderInterface
35 private $csrfProvider;
40 private $blockNameHierarchyMap = array();
45 private $hierarchyLevelMap = array();
50 private $variableStack = array();
52 public function __construct(FormRendererEngineInterface
$engine, CsrfProviderInterface
$csrfProvider = null)
54 $this->engine
= $engine;
55 $this->csrfProvider
= $csrfProvider;
61 public function getEngine()
69 public function setTheme(FormView
$view, $themes)
71 $this->engine
->setTheme($view, $themes);
77 public function renderCsrfToken($intention)
79 if (null === $this->csrfProvider
) {
80 throw new BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.');
83 return $this->csrfProvider
->generateCsrfToken($intention);
89 public function renderBlock(FormView
$view, $blockName, array $variables = array())
91 $resource = $this->engine
->getResourceForBlockName($view, $blockName);
94 throw new LogicException(sprintf('No block "%s" found while rendering the form.', $blockName));
97 $viewCacheKey = $view->vars
[self
::CACHE_KEY_VAR
];
99 // The variables are cached globally for a view (instead of for the
101 if (!isset($this->variableStack
[$viewCacheKey])) {
102 $this->variableStack
[$viewCacheKey] = array();
104 // The default variable scope contains all view variables, merged with
105 // the variables passed explicitly to the helper
106 $scopeVariables = $view->vars
;
110 // Reuse the current scope and merge it with the explicitly passed variables
111 $scopeVariables = end($this->variableStack
[$viewCacheKey]);
116 // Merge the passed with the existing attributes
117 if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
118 $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
121 // Merge the passed with the exist *label* attributes
122 if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
123 $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
126 // Do not use array_replace_recursive(), otherwise array variables
127 // cannot be overwritten
128 $variables = array_replace($scopeVariables, $variables);
130 $this->variableStack
[$viewCacheKey][] = $variables;
133 $html = $this->engine
->renderBlock($view, $resource, $blockName, $variables);
136 array_pop($this->variableStack
[$viewCacheKey]);
139 unset($this->variableStack
[$viewCacheKey]);
148 public function searchAndRenderBlock(FormView
$view, $blockNameSuffix, array $variables = array())
150 $renderOnlyOnce = 'row' === $blockNameSuffix || 'widget' === $blockNameSuffix;
152 if ($renderOnlyOnce && $view->isRendered()) {
156 // The cache key for storing the variables and types
157 $viewCacheKey = $view->vars
[self
::CACHE_KEY_VAR
];
158 $viewAndSuffixCacheKey = $viewCacheKey.$blockNameSuffix;
160 // In templates, we have to deal with two kinds of block hierarchies:
162 // +---------+ +---------+
163 // | Theme B | -------> | Theme A |
164 // +---------+ +---------+
166 // form_widget -------> form_widget
169 // choice_widget -----> choice_widget
171 // The first kind of hierarchy is the theme hierarchy. This allows to
172 // override the block "choice_widget" from Theme A in the extending
173 // Theme B. This kind of inheritance needs to be supported by the
174 // template engine and, for example, offers "parent()" or similar
175 // functions to fall back from the custom to the parent implementation.
177 // The second kind of hierarchy is the form type hierarchy. This allows
178 // to implement a custom "choice_widget" block (no matter in which theme),
179 // or to fallback to the block of the parent type, which would be
180 // "form_widget" in this example (again, no matter in which theme).
181 // If the designer wants to explicitly fallback to "form_widget" in his
182 // custom "choice_widget", for example because he only wants to wrap
183 // a <div> around the original implementation, he can simply call the
184 // widget() function again to render the block for the parent type.
186 // The second kind is implemented in the following blocks.
187 if (!isset($this->blockNameHierarchyMap
[$viewAndSuffixCacheKey])) {
189 // Calculate the hierarchy of template blocks and start on
190 // the bottom level of the hierarchy (= "_<id>_<section>" block)
191 $blockNameHierarchy = array();
192 foreach ($view->vars
['block_prefixes'] as $blockNamePrefix) {
193 $blockNameHierarchy[] = $blockNamePrefix.'_'.$blockNameSuffix;
195 $hierarchyLevel = count($blockNameHierarchy) - 1;
197 $hierarchyInit = true;
200 // If a block recursively calls searchAndRenderBlock() again, resume rendering
201 // using the parent type in the hierarchy.
202 $blockNameHierarchy = $this->blockNameHierarchyMap
[$viewAndSuffixCacheKey];
203 $hierarchyLevel = $this->hierarchyLevelMap
[$viewAndSuffixCacheKey] - 1;
205 $hierarchyInit = false;
208 // The variables are cached globally for a view (instead of for the
210 if (!isset($this->variableStack
[$viewCacheKey])) {
211 $this->variableStack
[$viewCacheKey] = array();
213 // The default variable scope contains all view variables, merged with
214 // the variables passed explicitly to the helper
215 $scopeVariables = $view->vars
;
219 // Reuse the current scope and merge it with the explicitly passed variables
220 $scopeVariables = end($this->variableStack
[$viewCacheKey]);
225 // Load the resource where this block can be found
226 $resource = $this->engine
->getResourceForBlockNameHierarchy($view, $blockNameHierarchy, $hierarchyLevel);
228 // Update the current hierarchy level to the one at which the resource was
229 // found. For example, if looking for "choice_widget", but only a resource
230 // is found for its parent "form_widget", then the level is updated here
231 // to the parent level.
232 $hierarchyLevel = $this->engine
->getResourceHierarchyLevel($view, $blockNameHierarchy, $hierarchyLevel);
234 // The actually existing block name in $resource
235 $blockName = $blockNameHierarchy[$hierarchyLevel];
237 // Escape if no resource exists for this block
239 throw new LogicException(sprintf(
240 'Unable to render the form as none of the following blocks exist: "%s".',
241 implode('", "', array_reverse($blockNameHierarchy))
245 // Merge the passed with the existing attributes
246 if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
247 $variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
250 // Merge the passed with the exist *label* attributes
251 if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
252 $variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
255 // Do not use array_replace_recursive(), otherwise array variables
256 // cannot be overwritten
257 $variables = array_replace($scopeVariables, $variables);
259 // In order to make recursive calls possible, we need to store the block hierarchy,
260 // the current level of the hierarchy and the variables so that this method can
261 // resume rendering one level higher of the hierarchy when it is called recursively.
263 // We need to store these values in maps (associative arrays) because within a
264 // call to widget() another call to widget() can be made, but for a different view
265 // object. These nested calls should not override each other.
266 $this->blockNameHierarchyMap
[$viewAndSuffixCacheKey] = $blockNameHierarchy;
267 $this->hierarchyLevelMap
[$viewAndSuffixCacheKey] = $hierarchyLevel;
269 // We also need to store the variables for the view so that we can render other
270 // blocks for the same view using the same variables as in the outer block.
271 $this->variableStack
[$viewCacheKey][] = $variables;
274 $html = $this->engine
->renderBlock($view, $resource, $blockName, $variables);
277 array_pop($this->variableStack
[$viewCacheKey]);
279 // Clear the caches if they were filled for the first time within
280 // this function call
281 if ($hierarchyInit) {
282 unset($this->blockNameHierarchyMap
[$viewAndSuffixCacheKey]);
283 unset($this->hierarchyLevelMap
[$viewAndSuffixCacheKey]);
287 unset($this->variableStack
[$viewCacheKey]);
290 if ($renderOnlyOnce) {
291 $view->setRendered();
300 public function humanize($text)
302 return ucfirst(trim(strtolower(preg_replace(array('/([A-Z])/', '/[_\s]+/'), array('_$1', ' '), $text))));