-
-
Notifications
You must be signed in to change notification settings - Fork 188
/
AbstractFormFieldViewHelper.php
304 lines (283 loc) · 11.8 KB
/
AbstractFormFieldViewHelper.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
<?php
namespace Neos\FluidAdaptor\ViewHelpers\Form;
/*
* This file is part of the Neos.FluidAdaptor package.
*
* (c) Contributors of the Neos Project - www.neos.io
*
* This package is Open Source Software. For the full copyright and license
* information, please view the LICENSE file which was distributed with this
* source code.
*/
use Neos\Error\Messages\Result;
use Neos\Flow\Mvc\ActionRequest;
use Neos\FluidAdaptor\ViewHelpers\FormViewHelper;
use Neos\Utility\ObjectAccess;
/**
* Abstract Form View Helper. Bundles functionality related to direct property access of objects in other Form ViewHelpers.
*
* If you set the "property" attribute to the name of the property to resolve from the object, this class will
* automatically set the name and value of a form element.
*
* @api
*/
abstract class AbstractFormFieldViewHelper extends AbstractFormViewHelper
{
/**
* @return void
* @throws \Neos\FluidAdaptor\Core\ViewHelper\Exception
* @api
*/
public function initializeArguments()
{
parent::initializeArguments();
$this->registerArgument('name', 'string', 'Name of input tag');
$this->registerArgument('value', 'mixed', 'Value of input tag');
$this->registerArgument('property', 'string', 'Name of Object Property. If used in conjunction with <f:form object="...">, "name" and "value" properties will be ignored.');
}
/**
* Get the name of this form element.
* Either returns arguments['name'], or the correct name for Object Access.
*
* In case property is something like bla.blubb (hierarchical), then [bla][blubb] is generated.
*
* @return string Name
*/
protected function getName(): string
{
$name = $this->getNameWithoutPrefix();
return $this->prefixFieldName($name);
}
/**
* Shortcut for retrieving the request from the controller context
*
* @return ActionRequest
*/
protected function getRequest(): ActionRequest
{
return $this->controllerContext->getRequest();
}
/**
* Get the name of this form element, without prefix.
*
* @return string name
*/
protected function getNameWithoutPrefix(): string
{
if ($this->isObjectAccessorMode()) {
$propertySegments = explode('.', $this->arguments['property']);
$formObjectName = $this->viewHelperVariableContainer->get(FormViewHelper::class, 'formObjectName');
if (!empty($formObjectName)) {
array_unshift($propertySegments, $formObjectName);
}
$name = array_shift($propertySegments);
foreach ($propertySegments as $segment) {
$name .= '[' . $segment . ']';
}
} else {
$name = $this->arguments['name'];
}
if ($this->hasArgument('value')) {
/** @var object $value */
$value = $this->arguments['value'];
$multiple = $this->hasArgument('multiple') && $this->arguments['multiple'] === true;
if (!$multiple
&& is_object($value)
&& $this->persistenceManager->getIdentifierByObject($value) !== null
&& (!$this->persistenceManager->isNewObject($value))) {
$name .= '[__identity]';
}
}
return (string)$name;
}
/**
* Returns the current value of this Form ViewHelper and converts it to an identifier string in case it's an object
* The value is determined as follows:
* * If property mapping errors occurred and the form is re-displayed, the *last submitted* value is returned
* * Else the bound property is returned (only in objectAccessor-mode)
* * As fallback the "value" argument of this ViewHelper is used
*
* @param boolean $ignoreSubmittedFormData By default the submitted form value has precedence over value/property argument upon re-display. With this flag set the submitted data is not evaluated (e.g. for checkbox and hidden fields where the value attribute should not be changed)
* @return mixed Value
*/
protected function getValueAttribute($ignoreSubmittedFormData = false)
{
$value = null;
$submittedFormData = null;
if (!$ignoreSubmittedFormData && $this->hasMappingErrorOccurred()) {
$submittedFormData = $this->getLastSubmittedFormData();
}
if ($submittedFormData !== null) {
$value = $submittedFormData;
} elseif ($this->hasArgument('value')) {
$value = $this->arguments['value'];
} elseif ($this->isObjectAccessorMode()) {
$value = $this->getPropertyValue();
}
if (is_object($value)) {
$identifier = $this->persistenceManager->getIdentifierByObject($value);
if ($identifier !== null) {
$value = $identifier;
}
}
return $value;
}
/**
* Checks if a property mapping error has occurred in the last request.
*
* @return boolean true if a mapping error occurred, false otherwise
*/
protected function hasMappingErrorOccurred(): bool
{
/** @var $validationResults Result */
$validationResults = $this->getRequest()->getInternalArgument('__submittedArgumentValidationResults');
return ($validationResults instanceof Result && $validationResults->hasErrors());
}
/**
* Get the form data which has last been submitted; only returns valid data in case
* a property mapping error has occurred. Check with hasMappingErrorOccurred() before!
*
* @return mixed
*/
protected function getLastSubmittedFormData()
{
$submittedArguments = $this->getRequest()->getInternalArgument('__submittedArguments');
if ($submittedArguments !== null) {
return ObjectAccess::getPropertyPath($submittedArguments, $this->getPropertyPath());
}
}
/**
* Add additional identity properties in case the current property is hierarchical (of the form "bla.blubb").
* Then, [bla][__identity] has to be generated as well.
*
* @return void
*/
protected function addAdditionalIdentityPropertiesIfNeeded(): void
{
if (!$this->isObjectAccessorMode()) {
return;
}
if (!$this->viewHelperVariableContainer->exists(FormViewHelper::class, 'formObject')) {
return;
}
$propertySegments = explode('.', $this->arguments['property']);
// hierarchical property. If there is no "." inside (thus $propertySegments == 1), we do not need to do anything
if (count($propertySegments) < 2) {
return;
}
$formObject = $this->viewHelperVariableContainer->get(FormViewHelper::class, 'formObject');
$objectName = $this->viewHelperVariableContainer->get(FormViewHelper::class, 'formObjectName');
// If count == 2 -> we need to go through the for-loop exactly once
for ($i = 1, $segmentCount = count($propertySegments); $i < $segmentCount; $i++) {
$object = ObjectAccess::getPropertyPath($formObject, implode('.', array_slice($propertySegments, 0, $i)));
$objectName .= '[' . $propertySegments[$i - 1] . ']';
$hiddenIdentityField = $this->renderHiddenIdentityField($object, $objectName);
// Add the hidden identity field to the ViewHelperVariableContainer
$additionalIdentityProperties = $this->viewHelperVariableContainer->get(FormViewHelper::class, 'additionalIdentityProperties');
$additionalIdentityProperties[$objectName] = $hiddenIdentityField;
$this->viewHelperVariableContainer->addOrUpdate(FormViewHelper::class, 'additionalIdentityProperties', $additionalIdentityProperties);
}
}
/**
* Get the current property of the object bound to this form.
*
* @return mixed Value
*/
protected function getPropertyValue()
{
if (!$this->viewHelperVariableContainer->exists(FormViewHelper::class, 'formObject')) {
return null;
}
$formObject = $this->viewHelperVariableContainer->get(FormViewHelper::class, 'formObject');
$propertyNameOrPath = $this->arguments['property'];
return ObjectAccess::getPropertyPath($formObject, $propertyNameOrPath);
}
/**
* Returns the "absolute" property path of the property bound to this ViewHelper.
* For <f:form... property="foo.bar" /> this will be "<formObjectName>.foo.bar"
* For <f:form... name="foo[bar][baz]" /> this will be "foo.bar.baz"
*
* @return string
*/
protected function getPropertyPath(): string
{
if ($this->isObjectAccessorMode()) {
$formObjectName = (string)$this->viewHelperVariableContainer->get(FormViewHelper::class, 'formObjectName');
if ($formObjectName === '') {
return $this->arguments['property'];
}
return $formObjectName . '.' . $this->arguments['property'];
}
return rtrim(preg_replace('/(\]\[|\[|\])/', '.', $this->getNameWithoutPrefix()), '.');
}
/**
* Internal method which checks if we should evaluate a domain object or just output arguments['name'] and arguments['value']
*
* @return boolean true if we should evaluate the domain object, false otherwise.
*/
protected function isObjectAccessorMode(): bool
{
return $this->hasArgument('property') && $this->viewHelperVariableContainer->exists(FormViewHelper::class, 'formObjectName');
}
/**
* Add an CSS class if this view helper has errors
*
* @return void
*/
protected function setErrorClassAttribute(): void
{
if ($this->hasArgument('class')) {
$cssClass = $this->arguments['class'] . ' ';
} else {
$cssClass = '';
}
$mappingResultsForProperty = $this->getMappingResultsForProperty();
if ($mappingResultsForProperty->hasErrors()) {
if ($this->hasArgument('errorClass')) {
$cssClass .= $this->arguments['errorClass'];
} else {
$cssClass .= 'error';
}
$this->tag->addAttribute('class', $cssClass);
}
}
/**
* Get errors for the property and form name of this view helper
*
* @return Result
*/
protected function getMappingResultsForProperty(): Result
{
/** @var $validationResults Result */
$validationResults = $this->getRequest()->getInternalArgument('__submittedArgumentValidationResults');
if (!$validationResults instanceof Result) {
return new Result();
}
return $validationResults->forProperty($this->getPropertyPath());
}
/**
* Renders a hidden field with the same name as the element, to make sure the empty value is submitted
* in case nothing is selected. This is needed for checkbox and multiple select fields
*
* @return void
*/
protected function renderHiddenFieldForEmptyValue(): void
{
$emptyHiddenFieldNames = [];
if ($this->viewHelperVariableContainer->exists(FormViewHelper::class, 'emptyHiddenFieldNames')) {
$emptyHiddenFieldNames = $this->viewHelperVariableContainer->get(FormViewHelper::class, 'emptyHiddenFieldNames');
}
$fieldName = $this->getName();
if (substr($fieldName, -2) === '[]') {
$fieldName = substr($fieldName, 0, -2);
}
if (!isset($emptyHiddenFieldNames[$fieldName])) {
$disabled = false;
if ($this->tag->hasAttribute('disabled')) {
$disabled = $this->tag->getAttribute('disabled');
}
$emptyHiddenFieldNames[$fieldName] = $disabled;
$this->viewHelperVariableContainer->addOrUpdate(FormViewHelper::class, 'emptyHiddenFieldNames', $emptyHiddenFieldNames);
}
}
}