[TASK] Rename "view helper" to "ViewHelper"
[Packages/TYPO3.CMS.git] / typo3 / sysext / fluid / Classes / ViewHelpers / Form / SelectViewHelper.php
1 <?php
2 namespace TYPO3\CMS\Fluid\ViewHelpers\Form;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * This ViewHelper generates a <select> dropdown list for the use with a form.
19 *
20 * Basic usage
21 * ===========
22 *
23 * The most straightforward way is to supply an associative array as the "options" parameter.
24 * The array key is used as option key, and the value is used as human-readable name.
25 *
26 * Basic usage::
27 *
28 * <f:form.select name="paymentOptions" options="{payPal: 'PayPal International Services', visa: 'VISA Card'}" />
29 *
30 * Pre-select a value
31 * ------------------
32 *
33 * To pre-select a value, set "value" to the option key which should be selected.
34 * Default value::
35 *
36 * <f:form.select name="paymentOptions" options="{payPal: 'PayPal International Services', visa: 'VISA Card'}" value="visa" />
37 *
38 * Generates a dropdown box like above, except that "VISA Card" is selected.
39 *
40 * If the select box is a multi-select box (multiple="1"), then "value" can be an array as well.
41 *
42 * Custom options and option group rendering
43 * -----------------------------------------
44 *
45 * Child nodes can be used to create a completely custom set of ``<option>`` and ``<optgroup>`` tags in a way compatible with
46 * the HMAC generation. To do so, leave out the ``options`` argument and use child ViewHelpers:
47 * Custom options and optgroup::
48 *
49 * <f:form.select name="myproperty">
50 * <f:form.select.option value="1">Option one</f:form.select.option>
51 * <f:form.select.option value="2">Option two</f:form.select.option>
52 * <f:form.select.optgroup>
53 * <f:form.select.option value="3">Grouped option one</f:form.select.option>
54 * <f:form.select.option value="4">Grouped option twi</f:form.select.option>
55 * </f:form.select.optgroup>
56 * </f:form.select>
57 *
58 * Note: do not use vanilla ``<option>`` or ``<optgroup>`` tags! They will invalidate the HMAC generation!
59 *
60 * Usage on domain objects
61 * -----------------------
62 *
63 * If you want to output domain objects, you can just pass them as array into the "options" parameter.
64 * To define what domain object value should be used as option key, use the "optionValueField" variable. Same goes for optionLabelField.
65 * If neither is given, the Identifier (UID/uid) and the __toString() method are tried as fallbacks.
66 *
67 * If the optionValueField variable is set, the getter named after that value is used to retrieve the option key.
68 * If the optionLabelField variable is set, the getter named after that value is used to retrieve the option value.
69 *
70 * If the prependOptionLabel variable is set, an option item is added in first position, bearing an empty string or -
71 * If provided, the value of the prependOptionValue variable as value.
72 *
73 * Domain objects::
74 *
75 * <f:form.select name="users" options="{userArray}" optionValueField="id" optionLabelField="firstName" />
76 *
77 * In the above example, the userArray is an array of "User" domain objects, with no array key specified.
78 *
79 * So, in the above example, the method $user->getId() is called to retrieve the key, and $user->getFirstName() to retrieve the displayed value of each entry.
80 *
81 * The "value" property now expects a domain object, and tests for object equivalence.
82 */
83 class SelectViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormFieldViewHelper
84 {
85 /**
86 * @var string
87 */
88 protected $tagName = 'select';
89
90 /**
91 * @var mixed
92 */
93 protected $selectedValue;
94
95 /**
96 * Initialize arguments.
97 */
98 public function initializeArguments()
99 {
100 parent::initializeArguments();
101 $this->registerUniversalTagAttributes();
102 $this->registerTagAttribute('size', 'string', 'Size of input field');
103 $this->registerTagAttribute('disabled', 'string', 'Specifies that the input element should be disabled when the page loads');
104 $this->registerArgument('options', 'array', 'Associative array with internal IDs as key, and the values are displayed in the select box. Can be combined with or replaced by child f:form.select.* nodes.');
105 $this->registerArgument('optionsAfterContent', 'boolean', 'If true, places auto-generated option tags after those rendered in the tag content. If false, automatic options come first.', false, false);
106 $this->registerArgument('optionValueField', 'string', 'If specified, will call the appropriate getter on each object to determine the value.');
107 $this->registerArgument('optionLabelField', 'string', 'If specified, will call the appropriate getter on each object to determine the label.');
108 $this->registerArgument('sortByOptionLabel', 'boolean', 'If true, List will be sorted by label.', false, false);
109 $this->registerArgument('selectAllByDefault', 'boolean', 'If specified options are selected if none was set before.', false, false);
110 $this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this ViewHelper', false, 'f3-form-error');
111 $this->registerArgument('prependOptionLabel', 'string', 'If specified, will provide an option at first position with the specified label.');
112 $this->registerArgument('prependOptionValue', 'string', 'If specified, will provide an option at first position with the specified value.');
113 $this->registerArgument('multiple', 'boolean', 'If set multiple options may be selected.', false, false);
114 $this->registerArgument('required', 'boolean', 'If set no empty value is allowed.', false, false);
115 }
116
117 /**
118 * Render the tag.
119 *
120 * @return string rendered tag.
121 */
122 public function render()
123 {
124 if (isset($this->arguments['required']) && $this->arguments['required']) {
125 $this->tag->addAttribute('required', 'required');
126 }
127 $name = $this->getName();
128 if (isset($this->arguments['multiple']) && $this->arguments['multiple']) {
129 $this->tag->addAttribute('multiple', 'multiple');
130 $name .= '[]';
131 }
132 $this->tag->addAttribute('name', $name);
133 $options = $this->getOptions();
134
135 $this->addAdditionalIdentityPropertiesIfNeeded();
136 $this->setErrorClassAttribute();
137 $content = '';
138 // register field name for token generation.
139 // in case it is a multi-select, we need to register the field name
140 // as often as there are elements in the box
141 if (isset($this->arguments['multiple']) && $this->arguments['multiple']) {
142 $content .= $this->renderHiddenFieldForEmptyValue();
143 $optionsCount = count($options);
144 for ($i = 0; $i < $optionsCount; $i++) {
145 $this->registerFieldNameForFormTokenGeneration($name);
146 }
147 // save the parent field name so that any child f:form.select.option
148 // tag will know to call registerFieldNameForFormTokenGeneration
149 // this is the reason why "self::class" is used instead of static::class (no LSB)
150 $this->viewHelperVariableContainer->addOrUpdate(
151 self::class,
152 'registerFieldNameForFormTokenGeneration',
153 $name
154 );
155 } else {
156 $this->registerFieldNameForFormTokenGeneration($name);
157 }
158
159 $this->viewHelperVariableContainer->addOrUpdate(self::class, 'selectedValue', $this->getSelectedValue());
160 $prependContent = $this->renderPrependOptionTag();
161 $tagContent = $this->renderOptionTags($options);
162 $childContent = $this->renderChildren();
163 $this->viewHelperVariableContainer->remove(self::class, 'selectedValue');
164 $this->viewHelperVariableContainer->remove(self::class, 'registerFieldNameForFormTokenGeneration');
165 if (isset($this->arguments['optionsAfterContent']) && $this->arguments['optionsAfterContent']) {
166 $tagContent = $childContent . $tagContent;
167 } else {
168 $tagContent .= $childContent;
169 }
170 $tagContent = $prependContent . $tagContent;
171
172 $this->tag->forceClosingTag(true);
173 $this->tag->setContent($tagContent);
174 $content .= $this->tag->render();
175 return $content;
176 }
177
178 /**
179 * Render prepended option tag
180 *
181 * @return string rendered prepended empty option
182 */
183 protected function renderPrependOptionTag()
184 {
185 $output = '';
186 if ($this->hasArgument('prependOptionLabel')) {
187 $value = $this->hasArgument('prependOptionValue') ? $this->arguments['prependOptionValue'] : '';
188 $label = $this->arguments['prependOptionLabel'];
189 $output .= $this->renderOptionTag($value, $label, false) . LF;
190 }
191 return $output;
192 }
193
194 /**
195 * Render the option tags.
196 *
197 * @param array $options the options for the form.
198 * @return string rendered tags.
199 */
200 protected function renderOptionTags($options)
201 {
202 $output = '';
203 foreach ($options as $value => $label) {
204 $isSelected = $this->isSelected($value);
205 $output .= $this->renderOptionTag($value, $label, $isSelected) . LF;
206 }
207 return $output;
208 }
209
210 /**
211 * Render the option tags.
212 *
213 * @return array an associative array of options, key will be the value of the option tag
214 */
215 protected function getOptions()
216 {
217 if (!is_array($this->arguments['options']) && !$this->arguments['options'] instanceof \Traversable) {
218 return [];
219 }
220 $options = [];
221 $optionsArgument = $this->arguments['options'];
222 foreach ($optionsArgument as $key => $value) {
223 if (is_object($value) || is_array($value)) {
224 if ($this->hasArgument('optionValueField')) {
225 $key = \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getPropertyPath($value, $this->arguments['optionValueField']);
226 if (is_object($key)) {
227 if (method_exists($key, '__toString')) {
228 $key = (string)$key;
229 } else {
230 throw new \TYPO3Fluid\Fluid\Core\ViewHelper\Exception('Identifying value for object of class "' . get_class($value) . '" was an object.', 1247827428);
231 }
232 }
233 } elseif ($this->persistenceManager->getIdentifierByObject($value) !== null) {
234 // @todo use $this->persistenceManager->isNewObject() once it is implemented
235 $key = $this->persistenceManager->getIdentifierByObject($value);
236 } elseif (method_exists($value, '__toString')) {
237 $key = (string)$value;
238 } else {
239 throw new \TYPO3Fluid\Fluid\Core\ViewHelper\Exception('No identifying value for object of class "' . get_class($value) . '" found.', 1247826696);
240 }
241 if ($this->hasArgument('optionLabelField')) {
242 $value = \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getPropertyPath($value, $this->arguments['optionLabelField']);
243 if (is_object($value)) {
244 if (method_exists($value, '__toString')) {
245 $value = (string)$value;
246 } else {
247 throw new \TYPO3Fluid\Fluid\Core\ViewHelper\Exception('Label value for object of class "' . get_class($value) . '" was an object without a __toString() method.', 1247827553);
248 }
249 }
250 } elseif (method_exists($value, '__toString')) {
251 $value = (string)$value;
252 } elseif ($this->persistenceManager->getIdentifierByObject($value) !== null) {
253 // @todo use $this->persistenceManager->isNewObject() once it is implemented
254 $value = $this->persistenceManager->getIdentifierByObject($value);
255 }
256 }
257 $options[$key] = $value;
258 }
259 if ($this->arguments['sortByOptionLabel']) {
260 asort($options, SORT_LOCALE_STRING);
261 }
262 return $options;
263 }
264
265 /**
266 * Render the option tags.
267 *
268 * @param mixed $value Value to check for
269 * @return bool TRUE if the value should be marked a s selected; FALSE otherwise
270 */
271 protected function isSelected($value)
272 {
273 $selectedValue = $this->getSelectedValue();
274 if ($value === $selectedValue || (string)$value === $selectedValue) {
275 return true;
276 }
277 if ($this->hasArgument('multiple')) {
278 if ($selectedValue === null && $this->arguments['selectAllByDefault'] === true) {
279 return true;
280 }
281 if (is_array($selectedValue) && in_array($value, $selectedValue)) {
282 return true;
283 }
284 }
285 return false;
286 }
287
288 /**
289 * Retrieves the selected value(s)
290 *
291 * @return mixed value string or an array of strings
292 */
293 protected function getSelectedValue()
294 {
295 $this->setRespectSubmittedDataValue(true);
296 $value = $this->getValueAttribute();
297 if (!is_array($value) && !$value instanceof \Traversable) {
298 return $this->getOptionValueScalar($value);
299 }
300 $selectedValues = [];
301 foreach ($value as $selectedValueElement) {
302 $selectedValues[] = $this->getOptionValueScalar($selectedValueElement);
303 }
304 return $selectedValues;
305 }
306
307 /**
308 * Get the option value for an object
309 *
310 * @param mixed $valueElement
311 * @return string
312 */
313 protected function getOptionValueScalar($valueElement)
314 {
315 if (is_object($valueElement)) {
316 if ($this->hasArgument('optionValueField')) {
317 return \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getPropertyPath($valueElement, $this->arguments['optionValueField']);
318 }
319 // @todo use $this->persistenceManager->isNewObject() once it is implemented
320 if ($this->persistenceManager->getIdentifierByObject($valueElement) !== null) {
321 return $this->persistenceManager->getIdentifierByObject($valueElement);
322 }
323 return (string)$valueElement;
324 }
325 return $valueElement;
326 }
327
328 /**
329 * Render one option tag
330 *
331 * @param string $value value attribute of the option tag (will be escaped)
332 * @param string $label content of the option tag (will be escaped)
333 * @param bool $isSelected specifies wheter or not to add selected attribute
334 * @return string the rendered option tag
335 */
336 protected function renderOptionTag($value, $label, $isSelected)
337 {
338 $output = '<option value="' . htmlspecialchars($value) . '"';
339 if ($isSelected) {
340 $output .= ' selected="selected"';
341 }
342 $output .= '>' . htmlspecialchars($label) . '</option>';
343 return $output;
344 }
345 }