.mapOnDatabaseColumn (mandatory) * -------------------------------------------------------- * The value from the submitted form element with the identifier * '' will be written into this database column * * options.elements..skipIfValueIsEmpty (default: false) * ------------------------------------------------------ * Set this to true if the database column should not be written * if the value from the submitted form element with the identifier * '' is empty (think about password fields etc.) * * options.elements..hashed (default: false) * ------------------------------------------------------ * Set this to true if the value from the submitted form element * should be hashed before writing into the database. * * options.elements..saveFileIdentifierInsteadOfUid (default: false) * ------------------------------------------------------------------- * This setting only rules for form elements which creates a FAL object * like FileUpload or ImageUpload. * By default, the uid of the FAL object will be written into * the database column. Set this to true if you want to store the * FAL identifier (1:/user_uploads/some_uploaded_pic.jpg) instead. * * options.databaseColumnMappings * ------------------------------ * Use this to map database columns to static values (which can be * made dynamic through typoscript overrides of course). * Each key within options.databaseColumnMappings has to match with a * existing database column. * The value for each key within options.databaseColumnMappings is an * array with additional information. * * This mapping is done *before* the options.elements mapping. * This means if you map a database column to a value through * options.databaseColumnMappings and map a submitted form element * value to the same database column, the submitted form element value * will override the value you set within options.databaseColumnMappings. * * options.databaseColumnMappings..value * --------------------------------------------------------- * The value which will be written to the database column. * You can use the FormRuntime accessor feature to access every * getable property from the TYPO3\CMS\Form\Domain\Runtime\FormRuntime * Read the description within * TYPO3\CMS\Form\Domain\Finishers\AbstractFinisher::parseOption * In short: use something like {} to get the value * from the submitted form element with the identifier * * * Don't be confused. If you use the FormRuntime accessor feature within * options.databaseColumnMappings, the functionality is nearly equal * to the options.elements configuration. * * options.databaseColumnMappings..skipIfValueIsEmpty (default: false) * --------------------------------------------------------------------- * Set this to true if the database column should not be written * if the value from * options.databaseColumnMappings..value is empty. * * Example * ======= * * finishers: * - * identifier: SaveToDatabase * options: * table: 'fe_users' * mode: update * whereClause: * uid: 1 * databaseColumnMappings: * pid: * value: 1 * elements: * text-1: * mapOnDatabaseColumn: 'first_name' * text-2: * mapOnDatabaseColumn: 'last_name' * text-3: * mapOnDatabaseColumn: 'username' * advancedpassword-1: * mapOnDatabaseColumn: 'password' * skipIfValueIsEmpty: true * hashed: true * * Multiple database operations * ============================ * * You can write options as an array to perform multiple database operations. * * finishers: * - * identifier: SaveToDatabase * options: * 1: * table: 'my_table' * mode: insert * databaseColumnMappings: * some_column: * value: 'cool' * 2: * table: 'my_other_table' * mode: update * whereClause: * pid: 1 * databaseColumnMappings: * some_other_column: * value: '{SaveToDatabase.insertedUids.1}' * * This would perform 2 database operations. * One insert and one update. * You can access the inserted uids with '{SaveToDatabase.insertedUids.}' * If you perform an insert operation, the value of the inserted database row will be stored * within the FinisherVariableProvider. * references to the numeric key within options * within which the insert operation is executed. * * Scope: frontend */ class SaveToDatabaseFinisher extends AbstractFinisher { /** * @var array */ protected $defaultOptions = [ 'table' => null, 'mode' => 'insert', 'whereClause' => [], 'elements' => [], 'databaseColumnMappings' => [], ]; /** * @var \TYPO3\CMS\Core\Database\Connection */ protected $databaseConnection; /** * Executes this finisher * @see AbstractFinisher::execute() * * @throws FinisherException */ protected function executeInternal(): void { $options = []; if (isset($this->options['table'])) { $options[] = $this->options; } else { $options = $this->options; } foreach ($options as $optionKey => $option) { $this->options = $option; $this->process($optionKey); } } /** * Prepare data for saving to database */ protected function prepareData(array $elementsConfiguration, array $databaseData): array { foreach ($this->getFormValues() as $elementIdentifier => $elementValue) { if ( ($elementValue === null || $elementValue === '') && isset($elementsConfiguration[$elementIdentifier]) && isset($elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty']) && $elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'] === true ) { continue; } $element = $this->getElementByIdentifier($elementIdentifier); if ( !$element || !isset($elementsConfiguration[$elementIdentifier]) || !isset($elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn']) ) { continue; } if (isset($elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid'])) { $saveFileIdentifierInsteadOfUid = (bool)$elementsConfiguration[$elementIdentifier]['saveFileIdentifierInsteadOfUid']; } else { $saveFileIdentifierInsteadOfUid = false; } if ($elementValue instanceof FileReference) { $elementValue = $this->prepareFileForDatabase($elementValue, $saveFileIdentifierInsteadOfUid); } elseif ($elementValue instanceof ObjectStorage) { $fileIdentifiers = []; foreach ($elementValue as $singleElement) { if ($singleElement instanceof FileReference) { $fileIdentifiers[] = $this->prepareFileForDatabase($singleElement, $saveFileIdentifierInsteadOfUid); } } $elementValue = implode(',', $fileIdentifiers); } elseif (is_array($elementValue)) { $elementValue = implode(',', $elementValue); } elseif ($elementValue instanceof \DateTimeInterface) { $format = $elementsConfiguration[$elementIdentifier]['dateFormat'] ?? 'U'; $elementValue = $elementValue->format($format); } elseif ($elementValue && ($elementsConfiguration[$elementIdentifier]['hashed'] ?? false) === true) { $hashInstance = GeneralUtility::makeInstance(PasswordHashFactory::class)->getDefaultHashInstance('FE'); $elementValue = $hashInstance->getHashedPassword($elementValue); } $databaseData[$elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn']] = $elementValue; } return $databaseData; } /** * Perform the current database operation * @throws FinisherException */ protected function process(int $iterationCount): void { $this->throwExceptionOnInconsistentConfiguration(); $table = $this->parseOption('table'); $table = is_string($table) ? $table : ''; $elementsConfiguration = $this->parseOption('elements'); $elementsConfiguration = is_array($elementsConfiguration) ? $elementsConfiguration : []; $databaseColumnMappingsConfiguration = $this->parseOption('databaseColumnMappings'); $this->databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); $databaseData = []; foreach ($databaseColumnMappingsConfiguration as $databaseColumnName => $databaseColumnConfiguration) { $value = $this->parseOption('databaseColumnMappings.' . $databaseColumnName . '.value'); if ( empty($value) && ($databaseColumnConfiguration['skipIfValueIsEmpty'] ?? false) === true ) { continue; } $databaseData[$databaseColumnName] = $value; } $databaseData = $this->prepareData($elementsConfiguration, $databaseData); try { $this->saveToDatabase($databaseData, $table, $iterationCount); } catch (Exception $e) { throw new FinisherException( 'Failed to save data to database table: ' . $table . '. Error message:' . $e->getMessage(), 1754050114, $e ); } } /** * Save or insert the values from * $databaseData into the table $table * @throws Exception */ protected function saveToDatabase(array $databaseData, string $table, int $iterationCount): void { if (!empty($databaseData)) { if ($this->parseOption('mode') === 'update') { $whereClause = $this->parseOption('whereClause'); foreach ($whereClause as $columnName => $columnValue) { $whereClause[$columnName] = $this->parseOption('whereClause.' . $columnName); } $this->databaseConnection->update( $table, $databaseData, $whereClause ); } else { $this->databaseConnection->insert($table, $databaseData); try { $insertedUid = (int)$this->databaseConnection->lastInsertId(); } catch (Exception) { // Some database tables like sys_category_record_mm may not // have an "identity" (uid column). In this case DBAL may // throw an exception, which we gracefully handle here. $insertedUid = 0; } $this->finisherContext->getFinisherVariableProvider()->add( $this->shortFinisherIdentifier, 'insertedUids.' . $iterationCount, $insertedUid ); } } } /** * Throws an exception if some inconsistent configuration * are detected. * * @throws FinisherException */ protected function throwExceptionOnInconsistentConfiguration(): void { if ( $this->parseOption('mode') === 'update' && empty($this->parseOption('whereClause')) ) { throw new FinisherException( 'An empty option "whereClause" is not allowed in update mode.', 1480469086 ); } } /** * Returns the values of the submitted form */ protected function getFormValues(): array { return $this->finisherContext->getFormValues(); } /** * Returns a form element object for a given identifier. * * @return FormElementInterface|null */ protected function getElementByIdentifier(string $elementIdentifier): ?FormElementInterface { return $this ->finisherContext ->getFormRuntime() ->getFormDefinition() ->getElementByIdentifier($elementIdentifier); } protected function prepareFileForDatabase(FileReference $fileReference, bool $saveFileIdentifierInsteadOfUid = false): int|string { if ($saveFileIdentifierInsteadOfUid) { $elementValue = $fileReference->getOriginalResource()->getCombinedIdentifier(); } else { $elementValue = $fileReference->getOriginalResource()->getProperty('uid_local'); } return $elementValue; } }