[BUGFIX] Evaluate select multiple attribute as boolean
[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 script is backported from the TYPO3 Flow package "TYPO3.Fluid". *
6 * *
7 * It is free software; you can redistribute it and/or modify it under *
8 * the terms of the GNU Lesser General Public License, either version 3 *
9 * of the License, or (at your option) any later version. *
10 * *
11 * The TYPO3 project - inspiring people to share! *
12 * */
13 /**
14 * This view helper generates a <select> dropdown list for the use with a form.
15 *
16 * = Basic usage =
17 *
18 * The most straightforward way is to supply an associative array as the "options" parameter.
19 * The array key is used as option key, and the value is used as human-readable name.
20 *
21 * <code title="Basic usage">
22 * <f:form.select name="paymentOptions" options="{payPal: 'PayPal International Services', visa: 'VISA Card'}" />
23 * </code>
24 *
25 * = Pre-select a value =
26 *
27 * To pre-select a value, set "value" to the option key which should be selected.
28 * <code title="Default value">
29 * <f:form.select name="paymentOptions" options="{payPal: 'PayPal International Services', visa: 'VISA Card'}" value="visa" />
30 * </code>
31 * Generates a dropdown box like above, except that "VISA Card" is selected.
32 *
33 * If the select box is a multi-select box (multiple="1"), then "value" can be an array as well.
34 *
35 * = Usage on domain objects =
36 *
37 * If you want to output domain objects, you can just pass them as array into the "options" parameter.
38 * To define what domain object value should be used as option key, use the "optionValueField" variable. Same goes for optionLabelField.
39 * If neither is given, the Identifier (UID/uid) and the __toString() method are tried as fallbacks.
40 *
41 * If the optionValueField variable is set, the getter named after that value is used to retrieve the option key.
42 * If the optionLabelField variable is set, the getter named after that value is used to retrieve the option value.
43 *
44 * If the prependOptionLabel variable is set, an option item is added in first position, bearing an empty string or -
45 * If provided, the value of the prependOptionValue variable as value.
46 *
47 * <code title="Domain objects">
48 * <f:form.select name="users" options="{userArray}" optionValueField="id" optionLabelField="firstName" />
49 * </code>
50 * In the above example, the userArray is an array of "User" domain objects, with no array key specified.
51 *
52 * 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.
53 *
54 * The "value" property now expects a domain object, and tests for object equivalence.
55 *
56 * @api
57 */
58 class SelectViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\Form\AbstractFormFieldViewHelper
59 {
60 /**
61 * @var string
62 */
63 protected $tagName = 'select';
64
65 /**
66 * @var mixed
67 */
68 protected $selectedValue = null;
69
70 /**
71 * Initialize arguments.
72 *
73 * @return void
74 * @api
75 */
76 public function initializeArguments()
77 {
78 parent::initializeArguments();
79 $this->registerUniversalTagAttributes();
80 $this->registerTagAttribute('size', 'string', 'Size of input field');
81 $this->registerTagAttribute('disabled', 'string', 'Specifies that the input element should be disabled when the page loads');
82 $this->registerArgument('options', 'array', 'Associative array with internal IDs as key, and the values are displayed in the select box', true);
83 $this->registerArgument('optionValueField', 'string', 'If specified, will call the appropriate getter on each object to determine the value.');
84 $this->registerArgument('optionLabelField', 'string', 'If specified, will call the appropriate getter on each object to determine the label.');
85 $this->registerArgument('sortByOptionLabel', 'boolean', 'If true, List will be sorted by label.', false, false);
86 $this->registerArgument('selectAllByDefault', 'boolean', 'If specified options are selected if none was set before.', false, false);
87 $this->registerArgument('errorClass', 'string', 'CSS class to set if there are errors for this view helper', false, 'f3-form-error');
88 $this->registerArgument('prependOptionLabel', 'string', 'If specified, will provide an option at first position with the specified label.');
89 $this->registerArgument('prependOptionValue', 'string', 'If specified, will provide an option at first position with the specified value.');
90 $this->registerArgument('multiple', 'boolean', 'If set multiple options may be selected.', false, false);
91 }
92
93 /**
94 * Render the tag.
95 *
96 * @return string rendered tag.
97 * @api
98 */
99 public function render()
100 {
101 $name = $this->getName();
102 if ($this->arguments['multiple']) {
103 $this->tag->addAttribute('multiple', 'multiple');
104 $name .= '[]';
105 }
106 $this->tag->addAttribute('name', $name);
107 $options = $this->getOptions();
108 if (empty($options)) {
109 $options = array('' => '');
110 }
111 $this->tag->setContent($this->renderOptionTags($options));
112 $this->addAdditionalIdentityPropertiesIfNeeded();
113 $this->setErrorClassAttribute();
114 $content = '';
115 // register field name for token generation.
116 // in case it is a multi-select, we need to register the field name
117 // as often as there are elements in the box
118 if ($this->arguments['multiple']) {
119 $content .= $this->renderHiddenFieldForEmptyValue();
120 for ($i = 0; $i < count($options); $i++) {
121 $this->registerFieldNameForFormTokenGeneration($name);
122 }
123 } else {
124 $this->registerFieldNameForFormTokenGeneration($name);
125 }
126 $content .= $this->tag->render();
127 return $content;
128 }
129
130 /**
131 * Render the option tags.
132 *
133 * @param array $options the options for the form.
134 * @return string rendered tags.
135 */
136 protected function renderOptionTags($options)
137 {
138 $output = '';
139 if ($this->hasArgument('prependOptionLabel')) {
140 $value = $this->hasArgument('prependOptionValue') ? $this->arguments['prependOptionValue'] : '';
141 $label = $this->arguments['prependOptionLabel'];
142 $output .= $this->renderOptionTag($value, $label, false) . LF;
143 }
144 foreach ($options as $value => $label) {
145 $isSelected = $this->isSelected($value);
146 $output .= $this->renderOptionTag($value, $label, $isSelected) . LF;
147 }
148 return $output;
149 }
150
151 /**
152 * Render the option tags.
153 *
154 * @return array an associative array of options, key will be the value of the option tag
155 */
156 protected function getOptions()
157 {
158 if (!is_array($this->arguments['options']) && !$this->arguments['options'] instanceof \Traversable) {
159 return array();
160 }
161 $options = array();
162 $optionsArgument = $this->arguments['options'];
163 foreach ($optionsArgument as $key => $value) {
164 if (is_object($value) || is_array($value)) {
165 if ($this->hasArgument('optionValueField')) {
166 $key = \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getPropertyPath($value, $this->arguments['optionValueField']);
167 if (is_object($key)) {
168 if (method_exists($key, '__toString')) {
169 $key = (string)$key;
170 } else {
171 throw new \TYPO3\CMS\Fluid\Core\ViewHelper\Exception('Identifying value for object of class "' . get_class($value) . '" was an object.', 1247827428);
172 }
173 }
174 // @todo use $this->persistenceManager->isNewObject() once it is implemented
175 } elseif ($this->persistenceManager->getIdentifierByObject($value) !== null) {
176 $key = $this->persistenceManager->getIdentifierByObject($value);
177 } elseif (method_exists($value, '__toString')) {
178 $key = (string)$value;
179 } else {
180 throw new \TYPO3\CMS\Fluid\Core\ViewHelper\Exception('No identifying value for object of class "' . get_class($value) . '" found.', 1247826696);
181 }
182 if ($this->hasArgument('optionLabelField')) {
183 $value = \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getPropertyPath($value, $this->arguments['optionLabelField']);
184 if (is_object($value)) {
185 if (method_exists($value, '__toString')) {
186 $value = (string)$value;
187 } else {
188 throw new \TYPO3\CMS\Fluid\Core\ViewHelper\Exception('Label value for object of class "' . get_class($value) . '" was an object without a __toString() method.', 1247827553);
189 }
190 }
191 } elseif (method_exists($value, '__toString')) {
192 $value = (string)$value;
193 // @todo use $this->persistenceManager->isNewObject() once it is implemented
194 } elseif ($this->persistenceManager->getIdentifierByObject($value) !== null) {
195 $value = $this->persistenceManager->getIdentifierByObject($value);
196 }
197 }
198 $options[$key] = $value;
199 }
200 if ($this->arguments['sortByOptionLabel']) {
201 asort($options, SORT_LOCALE_STRING);
202 }
203 return $options;
204 }
205
206 /**
207 * Render the option tags.
208 *
209 * @param mixed $value Value to check for
210 * @return bool TRUE if the value should be marked a s selected; FALSE otherwise
211 */
212 protected function isSelected($value)
213 {
214 $selectedValue = $this->getSelectedValue();
215 if ($value === $selectedValue || (string)$value === $selectedValue) {
216 return true;
217 }
218 if ($this->hasArgument('multiple')) {
219 if (is_null($selectedValue) && $this->arguments['selectAllByDefault'] === true) {
220 return true;
221 } elseif (is_array($selectedValue) && in_array($value, $selectedValue)) {
222 return true;
223 }
224 }
225 return false;
226 }
227
228 /**
229 * Retrieves the selected value(s)
230 *
231 * @return mixed value string or an array of strings
232 */
233 protected function getSelectedValue()
234 {
235 $this->setRespectSubmittedDataValue(true);
236 $value = $this->getValueAttribute();
237 if (!is_array($value) && !$value instanceof \Traversable) {
238 return $this->getOptionValueScalar($value);
239 }
240 $selectedValues = array();
241 foreach ($value as $selectedValueElement) {
242 $selectedValues[] = $this->getOptionValueScalar($selectedValueElement);
243 }
244 return $selectedValues;
245 }
246
247 /**
248 * Get the option value for an object
249 *
250 * @param mixed $valueElement
251 * @return string
252 */
253 protected function getOptionValueScalar($valueElement)
254 {
255 if (is_object($valueElement)) {
256 if ($this->hasArgument('optionValueField')) {
257 return \TYPO3\CMS\Extbase\Reflection\ObjectAccess::getPropertyPath($valueElement, $this->arguments['optionValueField']);
258 } else {
259 // @todo use $this->persistenceManager->isNewObject() once it is implemented
260 if ($this->persistenceManager->getIdentifierByObject($valueElement) !== null) {
261 return $this->persistenceManager->getIdentifierByObject($valueElement);
262 } else {
263 return (string)$valueElement;
264 }
265 }
266 } else {
267 return $valueElement;
268 }
269 }
270
271 /**
272 * Render one option tag
273 *
274 * @param string $value value attribute of the option tag (will be escaped)
275 * @param string $label content of the option tag (will be escaped)
276 * @param bool $isSelected specifies wheter or not to add selected attribute
277 * @return string the rendered option tag
278 */
279 protected function renderOptionTag($value, $label, $isSelected)
280 {
281 $output = '<option value="' . htmlspecialchars($value) . '"';
282 if ($isSelected) {
283 $output .= ' selected="selected"';
284 }
285 $output .= '>' . htmlspecialchars($label) . '</option>';
286 return $output;
287 }
288 }