]>
Commit | Line | Data |
---|---|---|
4f5b44bd NL |
1 | <?php |
2 | ||
3 | /* | |
4 | * This file is part of the Symfony package. | |
5 | * | |
6 | * (c) Fabien Potencier <fabien@symfony.com> | |
7 | * | |
8 | * For the full copyright and license information, please view the LICENSE | |
9 | * file that was distributed with this source code. | |
10 | */ | |
11 | ||
12 | namespace Symfony\Component\Form; | |
13 | ||
14 | use Symfony\Component\Form\Exception\LogicException; | |
15 | use Symfony\Component\Form\Exception\BadMethodCallException; | |
16 | use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface; | |
17 | ||
18 | /** | |
19 | * Renders a form into HTML using a rendering engine. | |
20 | * | |
21 | * @author Bernhard Schussek <bschussek@gmail.com> | |
22 | */ | |
23 | class FormRenderer implements FormRendererInterface | |
24 | { | |
25 | const CACHE_KEY_VAR = 'unique_block_prefix'; | |
26 | ||
27 | /** | |
28 | * @var FormRendererEngineInterface | |
29 | */ | |
30 | private $engine; | |
31 | ||
32 | /** | |
33 | * @var CsrfProviderInterface | |
34 | */ | |
35 | private $csrfProvider; | |
36 | ||
37 | /** | |
38 | * @var array | |
39 | */ | |
40 | private $blockNameHierarchyMap = array(); | |
41 | ||
42 | /** | |
43 | * @var array | |
44 | */ | |
45 | private $hierarchyLevelMap = array(); | |
46 | ||
47 | /** | |
48 | * @var array | |
49 | */ | |
50 | private $variableStack = array(); | |
51 | ||
52 | public function __construct(FormRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null) | |
53 | { | |
54 | $this->engine = $engine; | |
55 | $this->csrfProvider = $csrfProvider; | |
56 | } | |
57 | ||
58 | /** | |
59 | * {@inheritdoc} | |
60 | */ | |
61 | public function getEngine() | |
62 | { | |
63 | return $this->engine; | |
64 | } | |
65 | ||
66 | /** | |
67 | * {@inheritdoc} | |
68 | */ | |
69 | public function setTheme(FormView $view, $themes) | |
70 | { | |
71 | $this->engine->setTheme($view, $themes); | |
72 | } | |
73 | ||
74 | /** | |
75 | * {@inheritdoc} | |
76 | */ | |
77 | public function renderCsrfToken($intention) | |
78 | { | |
79 | if (null === $this->csrfProvider) { | |
80 | throw new BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.'); | |
81 | } | |
82 | ||
83 | return $this->csrfProvider->generateCsrfToken($intention); | |
84 | } | |
85 | ||
86 | /** | |
87 | * {@inheritdoc} | |
88 | */ | |
89 | public function renderBlock(FormView $view, $blockName, array $variables = array()) | |
90 | { | |
91 | $resource = $this->engine->getResourceForBlockName($view, $blockName); | |
92 | ||
93 | if (!$resource) { | |
94 | throw new LogicException(sprintf('No block "%s" found while rendering the form.', $blockName)); | |
95 | } | |
96 | ||
97 | $viewCacheKey = $view->vars[self::CACHE_KEY_VAR]; | |
98 | ||
99 | // The variables are cached globally for a view (instead of for the | |
100 | // current suffix) | |
101 | if (!isset($this->variableStack[$viewCacheKey])) { | |
102 | $this->variableStack[$viewCacheKey] = array(); | |
103 | ||
104 | // The default variable scope contains all view variables, merged with | |
105 | // the variables passed explicitly to the helper | |
106 | $scopeVariables = $view->vars; | |
107 | ||
108 | $varInit = true; | |
109 | } else { | |
110 | // Reuse the current scope and merge it with the explicitly passed variables | |
111 | $scopeVariables = end($this->variableStack[$viewCacheKey]); | |
112 | ||
113 | $varInit = false; | |
114 | } | |
115 | ||
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']); | |
119 | } | |
120 | ||
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']); | |
124 | } | |
125 | ||
126 | // Do not use array_replace_recursive(), otherwise array variables | |
127 | // cannot be overwritten | |
128 | $variables = array_replace($scopeVariables, $variables); | |
129 | ||
130 | $this->variableStack[$viewCacheKey][] = $variables; | |
131 | ||
132 | // Do the rendering | |
133 | $html = $this->engine->renderBlock($view, $resource, $blockName, $variables); | |
134 | ||
135 | // Clear the stack | |
136 | array_pop($this->variableStack[$viewCacheKey]); | |
137 | ||
138 | if ($varInit) { | |
139 | unset($this->variableStack[$viewCacheKey]); | |
140 | } | |
141 | ||
142 | return $html; | |
143 | } | |
144 | ||
145 | /** | |
146 | * {@inheritdoc} | |
147 | */ | |
148 | public function searchAndRenderBlock(FormView $view, $blockNameSuffix, array $variables = array()) | |
149 | { | |
150 | $renderOnlyOnce = 'row' === $blockNameSuffix || 'widget' === $blockNameSuffix; | |
151 | ||
152 | if ($renderOnlyOnce && $view->isRendered()) { | |
153 | return ''; | |
154 | } | |
155 | ||
156 | // The cache key for storing the variables and types | |
157 | $viewCacheKey = $view->vars[self::CACHE_KEY_VAR]; | |
158 | $viewAndSuffixCacheKey = $viewCacheKey.$blockNameSuffix; | |
159 | ||
160 | // In templates, we have to deal with two kinds of block hierarchies: | |
161 | // | |
162 | // +---------+ +---------+ | |
163 | // | Theme B | -------> | Theme A | | |
164 | // +---------+ +---------+ | |
165 | // | |
166 | // form_widget -------> form_widget | |
167 | // ^ | |
168 | // | | |
169 | // choice_widget -----> choice_widget | |
170 | // | |
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. | |
176 | // | |
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. | |
185 | // | |
186 | // The second kind is implemented in the following blocks. | |
187 | if (!isset($this->blockNameHierarchyMap[$viewAndSuffixCacheKey])) { | |
188 | // INITIAL CALL | |
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; | |
194 | } | |
195 | $hierarchyLevel = count($blockNameHierarchy) - 1; | |
196 | ||
197 | $hierarchyInit = true; | |
198 | } else { | |
199 | // RECURSIVE CALL | |
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; | |
204 | ||
205 | $hierarchyInit = false; | |
206 | } | |
207 | ||
208 | // The variables are cached globally for a view (instead of for the | |
209 | // current suffix) | |
210 | if (!isset($this->variableStack[$viewCacheKey])) { | |
211 | $this->variableStack[$viewCacheKey] = array(); | |
212 | ||
213 | // The default variable scope contains all view variables, merged with | |
214 | // the variables passed explicitly to the helper | |
215 | $scopeVariables = $view->vars; | |
216 | ||
217 | $varInit = true; | |
218 | } else { | |
219 | // Reuse the current scope and merge it with the explicitly passed variables | |
220 | $scopeVariables = end($this->variableStack[$viewCacheKey]); | |
221 | ||
222 | $varInit = false; | |
223 | } | |
224 | ||
225 | // Load the resource where this block can be found | |
226 | $resource = $this->engine->getResourceForBlockNameHierarchy($view, $blockNameHierarchy, $hierarchyLevel); | |
227 | ||
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); | |
233 | ||
234 | // The actually existing block name in $resource | |
235 | $blockName = $blockNameHierarchy[$hierarchyLevel]; | |
236 | ||
237 | // Escape if no resource exists for this block | |
238 | if (!$resource) { | |
239 | throw new LogicException(sprintf( | |
240 | 'Unable to render the form as none of the following blocks exist: "%s".', | |
241 | implode('", "', array_reverse($blockNameHierarchy)) | |
242 | )); | |
243 | } | |
244 | ||
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']); | |
248 | } | |
249 | ||
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']); | |
253 | } | |
254 | ||
255 | // Do not use array_replace_recursive(), otherwise array variables | |
256 | // cannot be overwritten | |
257 | $variables = array_replace($scopeVariables, $variables); | |
258 | ||
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. | |
262 | // | |
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; | |
268 | ||
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; | |
272 | ||
273 | // Do the rendering | |
274 | $html = $this->engine->renderBlock($view, $resource, $blockName, $variables); | |
275 | ||
276 | // Clear the stack | |
277 | array_pop($this->variableStack[$viewCacheKey]); | |
278 | ||
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]); | |
284 | } | |
285 | ||
286 | if ($varInit) { | |
287 | unset($this->variableStack[$viewCacheKey]); | |
288 | } | |
289 | ||
290 | if ($renderOnlyOnce) { | |
291 | $view->setRendered(); | |
292 | } | |
293 | ||
294 | return $html; | |
295 | } | |
296 | ||
297 | /** | |
298 | * {@inheritdoc} | |
299 | */ | |
300 | public function humanize($text) | |
301 | { | |
302 | return ucfirst(trim(strtolower(preg_replace(array('/([A-Z])/', '/[_\s]+/'), array('_$1', ' '), $text)))); | |
303 | } | |
304 | } |