1e88a4137cf167cd9d6e03e91905cfd232eceb45
[Packages/TYPO3.CMS.git] / typo3 / sysext / form / Classes / Domain / Finishers / SaveToDatabaseFinisher.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Form\Domain\Finishers;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use TYPO3\CMS\Core\Database\ConnectionPool;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20 use TYPO3\CMS\Extbase\Domain\Model\FileReference;
21 use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
22 use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
23
24 /**
25 * This finisher saves the data from a submitted form into
26 * a database table.
27 *
28 * Configuration
29 * =============
30 *
31 * options.table (mandatory)
32 * -------------
33 * Save or update values into this table
34 *
35 * options.mode (default: insert)
36 * ------------
37 * Possible values are 'insert' or 'update'.
38 *
39 * insert: will create a new database row with the values from the
40 * submitted form and/or some predefined values.
41 * @see options.elements and options.databaseFieldMappings
42 * update: will update a given database row with the values from the
43 * submitted form and/or some predefined values.
44 * 'options.whereClause' is then required.
45 *
46 * options.whereClause
47 * -------------------
48 * This where clause will be used for an database update action
49 *
50 * options.elements
51 * ----------------
52 * Use this to map form element values to existing database columns.
53 * Each key within options.elements has to match with a
54 * form element identifier within your form definition.
55 * The value for each key within options.elements is an array with
56 * additional informations.
57 *
58 * options.elements.<elementIdentifier>.mapOnDatabaseColumn (mandatory)
59 * --------------------------------------------------------
60 * The value from the submitted form element with the identifier
61 * '<elementIdentifier>' will be written into this database column
62 *
63 * options.elements.<elementIdentifier>.skipIfValueIsEmpty (default: false)
64 * ------------------------------------------------------
65 * Set this to true if the database column should not be written
66 * if the value from the submitted form element with the identifier
67 * '<elementIdentifier>' is empty (think about password fields etc.)
68 *
69 * options.elements.<elementIdentifier>.saveFileIdentifierInsteadOfUid (default: false)
70 * -------------------------------------------------------------------
71 * This setting only rules for form elements which creates a FAL object
72 * like FileUpload or ImageUpload.
73 * By default, the uid of the FAL object will be written into
74 * the database column. Set this to true if you want to store the
75 * FAL identifier (1:/user_uploads/some_uploaded_pic.jpg) instead.
76 *
77 * options.databaseColumnMappings
78 * ------------------------------
79 * Use this to map database columns to static values (which can be
80 * made dynamic through typoscript overrides of course).
81 * Each key within options.databaseColumnMappings has to match with a
82 * existing database column.
83 * The value for each key within options.databaseColumnMappings is an
84 * array with additional informations.
85 *
86 * This mapping is done *before* the options.elements mapping.
87 * This means if you map a database column to a value through
88 * options.databaseColumnMappings and map a submitted form element
89 * value to the same database column, the submitted form element value
90 * will override the value you set within options.databaseColumnMappings.
91 *
92 * options.databaseColumnMappings.<databaseColumnName>.value
93 * ---------------------------------------------------------
94 * The value which will be written to the database column.
95 * You can use the FormRuntime accessor feature to access every
96 * getable property from the TYPO3\CMS\Form\Domain\Runtime\FormRuntime
97 * Read the description within
98 * TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher::parseOption
99 * In short: use something like {<elementIdentifier>} to get the value
100 * from the submitted form element with the identifier
101 * <elementIdentifier>
102 *
103 * Don't be confused. If you use the FormRuntime accessor feature within
104 * options.databaseColumnMappings, the functionality is nearly equal
105 * to the the options.elements configuration.
106 *
107 * options.databaseColumnMappings.<databaseColumnName>.skipIfValueIsEmpty (default: false)
108 * ---------------------------------------------------------------------
109 * Set this to true if the database column should not be written
110 * if the value from
111 * options.databaseColumnMappings.<databaseColumnName>.value is empty.
112 *
113 * Example
114 * =======
115 *
116 * finishers:
117 * -
118 * identifier: SaveToDatabase
119 * options:
120 * table: 'fe_users'
121 * mode: update
122 * whereClause:
123 * uid: 1
124 * databaseColumnMappings:
125 * pid:
126 * value: 1
127 * elements:
128 * text-1:
129 * mapOnDatabaseColumn: 'first_name'
130 * text-2:
131 * mapOnDatabaseColumn: 'last_name'
132 * text-3:
133 * mapOnDatabaseColumn: 'username'
134 * advancedpassword-1:
135 * mapOnDatabaseColumn: 'password'
136 * skipIfValueIsEmpty: true
137 *
138 * Multiple database operations
139 * ============================
140 *
141 * You can write options as an array to perform multiple database oprtations.
142 *
143 * finishers:
144 * -
145 * identifier: SaveToDatabase
146 * options:
147 * 1:
148 * table: 'my_table'
149 * mode: insert
150 * databaseColumnMappings:
151 * some_column:
152 * value: 'cool'
153 * 2:
154 * table: 'my_other_table'
155 * mode: update
156 * whereClause:
157 * pid: 1
158 * databaseColumnMappings:
159 * some_other_column:
160 * uid_foreign: '{SaveToDatabase.insertedUids.1}'
161 *
162 * This would perform 2 database operations.
163 * One insert and one update.
164 * You cann access the inserted uids with '{SaveToDatabase.insertedUids.<theArrayKeyNumberWithinOptions>}'
165 * If you perform an insert operation, the value of the inserted database row will be stored
166 * within the FinisherVariableProvider.
167 * <theArrayKeyNumberWithinOptions> references to the numeric key within options
168 * within which the insert operation is executed.
169 *
170 * Scope: frontend
171 */
172 class SaveToDatabaseFinisher extends AbstractFinisher
173 {
174
175 /**
176 * @var array
177 */
178 protected $defaultOptions = [
179 'table' => null,
180 'mode' => 'insert',
181 'whereClause' => [],
182 'elements' => [],
183 'databaseColumnMappings' => [],
184 ];
185
186 /**
187 * @var \TYPO3\CMS\Core\Database\Connection
188 */
189 protected $databaseConnection;
190
191 /**
192 * Executes this finisher
193 * @see AbstractFinisher::execute()
194 *
195 * @throws FinisherException
196 */
197 protected function executeInternal()
198 {
199 if (isset($this->options['table'])) {
200 $options[] = $this->options;
201 } else {
202 $options = $this->options;
203 }
204
205 foreach ($options as $optionKey => $option) {
206 $this->options = $option;
207 $this->process($optionKey);
208 }
209 }
210
211 /**
212 * Prepare data for saving to database
213 *
214 * @param array $elementsConfiguration
215 * @param array $databaseData
216 * @return mixed
217 */
218 protected function prepareData(array $elementsConfiguration, array $databaseData)
219 {
220 foreach ($this->getFormValues() as $elementIdentifier => $elementValue) {
221 if (
222 ($elementValue === null || $elementValue === '')
223 && isset($elementsConfiguration[$elementIdentifier])
224 && isset($elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'])
225 && $elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'] === true
226 ) {
227 continue;
228 }
229
230 $element = $this->getElementByIdentifier($elementIdentifier);
231 if (
232 !$element instanceof FormElementInterface
233 || !isset($elementsConfiguration[$elementIdentifier])
234 || !isset($elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn'])
235 ) {
236 continue;
237 }
238
239 if ($elementValue instanceof FileReference) {
240 if (isset($elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'])) {
241 $saveFileIdentifierInsteadOfUid = (bool)$elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'];
242 } else {
243 $saveFileIdentifierInsteadOfUid = false;
244 }
245
246 if ($saveFileIdentifierInsteadOfUid) {
247 $elementValue = $elementValue->getOriginalResource()->getCombinedIdentifier();
248 } else {
249 $elementValue = $elementValue->getOriginalResource()->getProperty('uid_local');
250 }
251 } elseif (is_array($elementValue)) {
252 $elementValue = implode(',', $elementValue);
253 } elseif ($elementValue instanceof \DateTimeInterface) {
254 $format = $elementsConfiguration[$elementIdentifier]['dateFormat'] ?? 'U';
255 $elementValue = $elementValue->format($format);
256 }
257
258 $databaseData[$elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn']] = $elementValue;
259 }
260 return $databaseData;
261 }
262
263 /**
264 * Perform the current database operation
265 *
266 * @param int $iterationCount
267 */
268 protected function process(int $iterationCount)
269 {
270 $this->throwExceptionOnInconsistentConfiguration();
271
272 $table = $this->parseOption('table');
273 $elementsConfiguration = $this->parseOption('elements');
274 $databaseColumnMappingsConfiguration = $this->parseOption('databaseColumnMappings');
275
276 $this->databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
277
278 $databaseData = [];
279 foreach ($databaseColumnMappingsConfiguration as $databaseColumnName => $databaseColumnConfiguration) {
280 $value = $this->parseOption('databaseColumnMappings.' . $databaseColumnName . '.value');
281 if (
282 empty($value)
283 && $databaseColumnConfiguration['skipIfValueIsEmpty'] === true
284 ) {
285 continue;
286 }
287
288 $databaseData[$databaseColumnName] = $value;
289 }
290
291 $databaseData = $this->prepareData($elementsConfiguration, $databaseData);
292
293 $this->saveToDatabase($databaseData, $table, $iterationCount);
294 }
295
296 /**
297 * Save or insert the values from
298 * $databaseData into the table $table
299 *
300 * @param [] $databaseData
301 * @param string $table
302 * @param int $iterationCount
303 */
304 protected function saveToDatabase(array $databaseData, string $table, int $iterationCount)
305 {
306 if (!empty($databaseData)) {
307 if ($this->options['mode'] === 'update') {
308 $whereClause = $this->options['whereClause'];
309 foreach ($whereClause as $columnName => $columnValue) {
310 $whereClause[$columnName] = $this->parseOption('whereClause.' . $columnName);
311 }
312 $this->databaseConnection->update(
313 $table,
314 $databaseData,
315 $whereClause
316 );
317 } else {
318 $this->databaseConnection->insert($table, $databaseData);
319 $insertedUid = (int)$this->databaseConnection->lastInsertId($table);
320 $this->finisherContext->getFinisherVariableProvider()->add(
321 $this->shortFinisherIdentifier,
322 'insertedUids.' . $iterationCount,
323 $insertedUid
324 );
325 }
326 }
327 }
328
329 /**
330 * Throws an exception if some inconsistent configuration
331 * are detected.
332 *
333 * @throws FinisherException
334 */
335 protected function throwExceptionOnInconsistentConfiguration()
336 {
337 if (
338 $this->options['mode'] === 'update'
339 && empty($this->options['whereClause'])
340 ) {
341 throw new FinisherException(
342 'An empty option "whereClause" is not allowed in update mode.',
343 1480469086
344 );
345 }
346 }
347
348 /**
349 * Returns the values of the submitted form
350 *
351 * @return []
352 */
353 protected function getFormValues(): array
354 {
355 return $this->finisherContext->getFormValues();
356 }
357
358 /**
359 * Returns a form element object for a given identifier.
360 *
361 * @param string $elementIdentifier
362 * @return FormElementInterface|null
363 */
364 protected function getElementByIdentifier(string $elementIdentifier)
365 {
366 return $this
367 ->finisherContext
368 ->getFormRuntime()
369 ->getFormDefinition()
370 ->getElementByIdentifier($elementIdentifier);
371 }
372 }