[TASK] Streamline phpdoc annotations in EXT:fluid
[Packages/TYPO3.CMS.git] / typo3 / sysext / fluid / Classes / ViewHelpers / Form / AbstractFormFieldViewHelper.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 use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
18
19 /**
20 * Abstract Form View Helper. Bundles functionality related to direct property access of objects in other Form ViewHelpers.
21 *
22 * If you set the "property" attribute to the name of the property to resolve from the object, this class will
23 * automatically set the name and value of a form element.
24 */
25 abstract class AbstractFormFieldViewHelper extends AbstractFormViewHelper
26 {
27 /**
28 * @var \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface
29 */
30 protected $configurationManager;
31
32 /**
33 * @var bool
34 */
35 protected $respectSubmittedDataValue = false;
36
37 /**
38 * @param \TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager
39 */
40 public function injectConfigurationManager(\TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface $configurationManager)
41 {
42 $this->configurationManager = $configurationManager;
43 }
44
45 /**
46 * Initialize arguments.
47 */
48 public function initializeArguments()
49 {
50 parent::initializeArguments();
51 $this->registerArgument('name', 'string', 'Name of input tag');
52 $this->registerArgument('value', 'mixed', 'Value of input tag');
53 $this->registerArgument(
54 'property',
55 'string',
56 'Name of Object Property. If used in conjunction with <f:form object="...">, "name" and "value" properties will be ignored.'
57 );
58 }
59
60 /**
61 * Getting the current configuration for respectSubmittedDataValue.
62 *
63 * @return bool
64 */
65 public function getRespectSubmittedDataValue()
66 {
67 return $this->respectSubmittedDataValue;
68 }
69
70 /**
71 * Define respectSubmittedDataValue to enable or disable the usage of the submitted values in the viewhelper.
72 *
73 * @param bool $respectSubmittedDataValue
74 */
75 public function setRespectSubmittedDataValue($respectSubmittedDataValue)
76 {
77 $this->respectSubmittedDataValue = $respectSubmittedDataValue;
78 }
79
80 /**
81 * Get the name of this form element.
82 * Either returns arguments['name'], or the correct name for Object Access.
83 *
84 * In case property is something like bla.blubb (hierarchical), then [bla][blubb] is generated.
85 *
86 * @return string Name
87 */
88 protected function getName()
89 {
90 $name = $this->getNameWithoutPrefix();
91 return $this->prefixFieldName($name);
92 }
93
94 /**
95 * Shortcut for retrieving the request from the controller context
96 *
97 * @return \TYPO3\CMS\Extbase\Mvc\Request
98 */
99 protected function getRequest()
100 {
101 return $this->renderingContext->getControllerContext()->getRequest();
102 }
103
104 /**
105 * Get the name of this form element, without prefix.
106 *
107 * @return string name
108 */
109 protected function getNameWithoutPrefix()
110 {
111 if ($this->isObjectAccessorMode()) {
112 $formObjectName = $this->viewHelperVariableContainer->get(
113 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
114 'formObjectName'
115 );
116 if (!empty($formObjectName)) {
117 $propertySegments = explode('.', $this->arguments['property'] ?? '');
118 $propertyPath = '';
119 foreach ($propertySegments as $segment) {
120 $propertyPath .= '[' . $segment . ']';
121 }
122 $name = $formObjectName . $propertyPath;
123 } else {
124 $name = $this->arguments['property'] ?? '';
125 }
126 } else {
127 $name = $this->arguments['name'] ?? '';
128 }
129 if ($this->hasArgument('value') && is_object($this->arguments['value'])) {
130 // @todo Use $this->persistenceManager->isNewObject() once it is implemented
131 if (null !== $this->persistenceManager->getIdentifierByObject($this->arguments['value'])) {
132 $name .= '[__identity]';
133 }
134 }
135 return $name;
136 }
137
138 /**
139 * Returns the current value of this Form ViewHelper and converts it to an identifier string in case it's an object
140 * The value is determined as follows:
141 * * If property mapping errors occurred and the form is re-displayed, the *last submitted* value is returned
142 * * Else the bound property is returned (only in objectAccessor-mode)
143 * * As fallback the "value" argument of this ViewHelper is used
144 *
145 * Note: This method should *not* be used for form elements that must not change the value attribute, e.g. (radio) buttons and checkboxes.
146 *
147 * @return mixed Value
148 */
149 protected function getValueAttribute()
150 {
151 $value = null;
152
153 if ($this->respectSubmittedDataValue) {
154 $value = $this->getValueFromSubmittedFormData($value);
155 } elseif ($this->hasArgument('value')) {
156 $value = $this->arguments['value'];
157 } elseif ($this->isObjectAccessorMode()) {
158 $value = $this->getPropertyValue();
159 }
160
161 $value = $this->convertToPlainValue($value);
162 return $value;
163 }
164
165 /**
166 * If property mapping errors occurred and the form is re-displayed, the *last submitted* value is returned by this
167 * method.
168 *
169 * Note:
170 * This method should *not* be used for form elements that must not change the value attribute, e.g. (radio)
171 * buttons and checkboxes. The default behaviour is not to use this method. You need to set
172 * respectSubmittedDataValue to TRUE to enable the form data handling for the viewhelper.
173 *
174 * @param mixed $value
175 * @return mixed Value
176 */
177 protected function getValueFromSubmittedFormData($value)
178 {
179 $submittedFormData = null;
180 if ($this->hasMappingErrorOccurred()) {
181 $submittedFormData = $this->getLastSubmittedFormData();
182 }
183 if ($submittedFormData !== null) {
184 $value = $submittedFormData;
185 } elseif ($this->hasArgument('value')) {
186 $value = $this->arguments['value'];
187 } elseif ($this->isObjectAccessorMode()) {
188 $value = $this->getPropertyValue();
189 }
190
191 return $value;
192 }
193
194 /**
195 * Converts an arbitrary value to a plain value
196 *
197 * @param mixed $value The value to convert
198 * @return mixed
199 */
200 protected function convertToPlainValue($value)
201 {
202 if (is_object($value)) {
203 $identifier = $this->persistenceManager->getIdentifierByObject($value);
204 if ($identifier !== null) {
205 $value = $identifier;
206 }
207 }
208 return $value;
209 }
210
211 /**
212 * Checks if a property mapping error has occurred in the last request.
213 *
214 * @return bool TRUE if a mapping error occurred, FALSE otherwise
215 */
216 protected function hasMappingErrorOccurred()
217 {
218 return $this->renderingContext->getControllerContext()->getRequest()->getOriginalRequest() !== null;
219 }
220
221 /**
222 * Get the form data which has last been submitted; only returns valid data in case
223 * a property mapping error has occurred. Check with hasMappingErrorOccurred() before!
224 *
225 * @return mixed
226 */
227 protected function getLastSubmittedFormData()
228 {
229 $propertyPath = rtrim(preg_replace('/(\\]\\[|\\[|\\])/', '.', $this->getNameWithoutPrefix()), '.');
230 $value = ObjectAccess::getPropertyPath(
231 $this->renderingContext->getControllerContext()->getRequest()->getOriginalRequest()->getArguments(),
232 $propertyPath
233 );
234 return $value;
235 }
236
237 /**
238 * Add additional identity properties in case the current property is hierarchical (of the form "bla.blubb").
239 * Then, [bla][__identity] has to be generated as well.
240 */
241 protected function addAdditionalIdentityPropertiesIfNeeded()
242 {
243 if (!$this->isObjectAccessorMode()) {
244 return;
245 }
246
247 if (!$this->viewHelperVariableContainer->exists(
248 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
249 'formObject'
250 )
251 ) {
252 return;
253 }
254 $propertySegments = explode('.', $this->arguments['property']);
255 // hierarchical property. If there is no "." inside (thus $propertySegments == 1), we do not need to do anything
256 if (count($propertySegments) < 2) {
257 return;
258 }
259 $formObject = $this->viewHelperVariableContainer->get(
260 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
261 'formObject'
262 );
263 $objectName = $this->viewHelperVariableContainer->get(
264 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
265 'formObjectName'
266 );
267 // If count == 2 -> we need to go through the for-loop exactly once
268 $propertySegmentsCount = count($propertySegments);
269 for ($i = 1; $i < $propertySegmentsCount; $i++) {
270 $object = ObjectAccess::getPropertyPath($formObject, implode('.', array_slice($propertySegments, 0, $i)));
271 $objectName .= '[' . $propertySegments[$i - 1] . ']';
272 $hiddenIdentityField = $this->renderHiddenIdentityField($object, $objectName);
273 // Add the hidden identity field to the ViewHelperVariableContainer
274 $additionalIdentityProperties = $this->viewHelperVariableContainer->get(
275 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
276 'additionalIdentityProperties'
277 );
278 $additionalIdentityProperties[$objectName] = $hiddenIdentityField;
279 $this->viewHelperVariableContainer->addOrUpdate(
280 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
281 'additionalIdentityProperties',
282 $additionalIdentityProperties
283 );
284 }
285 }
286
287 /**
288 * Get the current property of the object bound to this form.
289 *
290 * @return mixed Value
291 */
292 protected function getPropertyValue()
293 {
294 if (!$this->viewHelperVariableContainer->exists(
295 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
296 'formObject'
297 )
298 ) {
299 return null;
300 }
301 $formObject = $this->viewHelperVariableContainer->get(
302 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
303 'formObject'
304 );
305 return ObjectAccess::getPropertyPath($formObject, $this->arguments['property']);
306 }
307
308 /**
309 * Internal method which checks if we should evaluate a domain object or just output arguments['name'] and arguments['value']
310 *
311 * @return bool TRUE if we should evaluate the domain object, FALSE otherwise.
312 */
313 protected function isObjectAccessorMode()
314 {
315 return $this->hasArgument('property') && $this->viewHelperVariableContainer->exists(
316 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
317 'formObjectName'
318 );
319 }
320
321 /**
322 * Add an CSS class if this view helper has errors
323 */
324 protected function setErrorClassAttribute()
325 {
326 if ($this->hasArgument('class')) {
327 $cssClass = $this->arguments['class'] . ' ';
328 } else {
329 $cssClass = '';
330 }
331
332 $mappingResultsForProperty = $this->getMappingResultsForProperty();
333 if ($mappingResultsForProperty->hasErrors()) {
334 if ($this->hasArgument('errorClass')) {
335 $cssClass .= $this->arguments['errorClass'];
336 } else {
337 $cssClass .= 'error';
338 }
339 $this->tag->addAttribute('class', $cssClass);
340 }
341 }
342
343 /**
344 * Get errors for the property and form name of this view helper
345 *
346 * @return \TYPO3\CMS\Extbase\Error\Result Array of errors
347 */
348 protected function getMappingResultsForProperty()
349 {
350 if (!$this->isObjectAccessorMode()) {
351 return new \TYPO3\CMS\Extbase\Error\Result();
352 }
353 $originalRequestMappingResults = $this->getRequest()->getOriginalRequestMappingResults();
354 $formObjectName = $this->viewHelperVariableContainer->get(
355 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
356 'formObjectName'
357 );
358 return $originalRequestMappingResults->forProperty($formObjectName)->forProperty($this->arguments['property']);
359 }
360
361 /**
362 * Renders a hidden field with the same name as the element, to make sure the empty value is submitted
363 * in case nothing is selected. This is needed for checkbox and multiple select fields
364 *
365 * @return string the hidden field.
366 */
367 protected function renderHiddenFieldForEmptyValue()
368 {
369 $hiddenFieldNames = [];
370 if ($this->viewHelperVariableContainer->exists(
371 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
372 'renderedHiddenFields'
373 )
374 ) {
375 $hiddenFieldNames = $this->viewHelperVariableContainer->get(
376 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
377 'renderedHiddenFields'
378 );
379 }
380 $fieldName = $this->getName();
381 if (substr($fieldName, -2) === '[]') {
382 $fieldName = substr($fieldName, 0, -2);
383 }
384 if (!in_array($fieldName, $hiddenFieldNames)) {
385 $hiddenFieldNames[] = $fieldName;
386 $this->viewHelperVariableContainer->addOrUpdate(
387 \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class,
388 'renderedHiddenFields',
389 $hiddenFieldNames
390 );
391 return '<input type="hidden" name="' . htmlspecialchars($fieldName) . '" value="" />';
392 }
393 return '';
394 }
395 }