[FEATURE] EXT:form - Extend SaveToDatabase finisher 56/51456/9
authorRalf Zimmermann <ralf.zimmermann@tritum.de>
Sun, 29 Jan 2017 16:40:47 +0000 (17:40 +0100)
committerSusanne Moog <susanne.moog@typo3.org>
Mon, 6 Feb 2017 14:34:08 +0000 (15:34 +0100)
This feature extends the SaveToDatabase finisher with
the following functions:

* Perform multiple database operations
* Access the inserted uids from previous database inserts
* Add a special option value '{__currentTimestamp}'
* Add a variable container object which is passed through all finishers

The issue contains examples for testing.

Resolves: #79530
Releases: master
Change-Id: Ic2a569194d69434e0320c670cd879744c864b911
Reviewed-on: https://review.typo3.org/51456
Tested-by: TYPO3com <no-reply@typo3.com>
Tested-by: Bjoern Jacob <bjoern.jacob@tritum.de>
Reviewed-by: Daniel Lorenz <daniel.lorenz@extco.de>
Tested-by: Andreas Steiger <typo3@andreassteiger.de>
Reviewed-by: Susanne Moog <susanne.moog@typo3.org>
Tested-by: Susanne Moog <susanne.moog@typo3.org>
typo3/sysext/core/Documentation/Changelog/master/Feature-79530-EXTform-ExtendSaveToDatabaseFinisher.rst [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Finishers/AbstractFinisher.php
typo3/sysext/form/Classes/Domain/Finishers/EmailFinisher.php
typo3/sysext/form/Classes/Domain/Finishers/FinisherContext.php
typo3/sysext/form/Classes/Domain/Finishers/FinisherVariableProvider.php [new file with mode: 0644]
typo3/sysext/form/Classes/Domain/Finishers/SaveToDatabaseFinisher.php
typo3/sysext/form/Configuration/Yaml/BaseSetup.yaml
typo3/sysext/form/Configuration/Yaml/FormEditorSetup.yaml
typo3/sysext/form/Tests/Unit/Domain/Finishers/AbstractFinisherTest.php
typo3/sysext/form/Tests/Unit/Domain/Finishers/SaveToDatabaseFinisherTest.php [new file with mode: 0644]

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-79530-EXTform-ExtendSaveToDatabaseFinisher.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-79530-EXTform-ExtendSaveToDatabaseFinisher.rst
new file mode 100644 (file)
index 0000000..39f79e7
--- /dev/null
@@ -0,0 +1,105 @@
+.. include:: ../../Includes.txt
+
+============================================================
+Feature: #79530 - EXT:form -  Extend SaveToDatabase finisher
+============================================================
+
+See :issue:`79530`
+
+Description
+===========
+
+The SaveToDatabase finisher has been extended by the following functions:
+
+Perform multiple database operations
+------------------------------------
+
+You can set options as an array to perform multiple database oprtations.
+
+     finishers:
+       -
+         identifier: SaveToDatabase
+         options:
+           -
+             table: 'my_table'
+             mode: insert
+             databaseColumnMappings:
+               some_column:
+                 value: 'cool'
+           -
+             table: 'my_other_table'
+             mode: update
+             whereClause:
+               pid: 1
+             databaseColumnMappings:
+               some_other_column:
+                 uid_foreign: '{SaveToDatabase.insertedUids.0}'
+
+Access the inserted uids from previous database inserts
+-------------------------------------------------------
+
+You can access the inserted uids via '{SaveToDatabase.insertedUids.<theArrayKeyNumberWithinOptions>}'.
+If you perform an insert operation, the value of the inserted database row will be stored within the FinisherVariableProvider.
+'<theArrayKeyNumberWithinOptions>' references to the numeric key from the 'options' array within which the insert operation is executed.
+
+Add a special option value '{__currentTimestamp}'
+-------------------------------------------------
+
+You can write '{__currentTimestamp}' as an option value which returns the current timestamp.
+
+     finishers:
+       -
+         identifier: SaveToDatabase
+         options:
+           -
+             table: 'my_table'
+             mode: insert
+             databaseColumnMappings:
+               tstamp:
+                 value: '{__currentTimestamp}'
+
+Add a variable container object which is passed through all finishers
+---------------------------------------------------------------------
+
+There is a simple data storage object availabe within the '\TYPO3\CMS\Form\Domain\Finishers\FinisherContext'.
+Access is from within a finisher with
+
+        $this->finisherContext->getFinisherVariableProvider()
+
+Each finisher can write and/ or read data from this object.
+All data has to be prefixed with the finisher identifier, so you can determine what data from what finisher you want.
+
+Prototype to add some data:
+
+    $this->finisherContext->getFinisherVariableProvider()->add(
+        $this->shortFinisherIdentifier,
+        'some.data',
+        $yourData
+    );
+
+Prototype to get some data:
+
+    $otherFinisherData = $this->finisherContext->getFinisherVariableProvider()->get(
+        'SomeFinisherIdentifier',
+        'some.data'
+    );
+
+Access this data within the form definition:
+
+For example, a finisher with identifier 'SomeFinisherIdentifier' writes data with the key 'some.data'
+
+     finishers:
+       -
+         identifier: SaveToDatabase
+         options:
+           -
+             table: 'my_table'
+             mode: insert
+             databaseColumnMappings:
+               tstamp:
+                 value: '{SomeFinisherIdentifier.some.key}'
+
+You can read more about the configuration options within the SaveToDatabase inline documentation.
+Please see \TYPO3\CMS\Form\Domain\Finishers\SaveToDatabaseFinisher.
+
+.. index:: Frontend, ext:form
\ No newline at end of file
index b60ec5e..808b35d 100644 (file)
@@ -42,6 +42,11 @@ abstract class AbstractFinisher implements FinisherInterface
     protected $finisherIdentifier = '';
 
     /**
+     * @var string
+     */
+    protected $shortFinisherIdentifier = '';
+
+    /**
      * The options which have been set from the outside. Instead of directly
      * accessing them, you should rather use parseOption().
      *
@@ -105,6 +110,7 @@ abstract class AbstractFinisher implements FinisherInterface
     final public function execute(FinisherContext $finisherContext)
     {
         $this->finisherIdentifier = (new \ReflectionClass($this))->getShortName();
+        $this->shortFinisherIdentifier = preg_replace('/Finisher$/', '', $this->finisherIdentifier);
         $this->finisherContext = $finisherContext;
         $this->executeInternal();
     }
@@ -164,16 +170,30 @@ abstract class AbstractFinisher implements FinisherInterface
 
         // You can encapsulate a option value with {}.
         // This enables you to access every getable property from the
-        // TYPO3\CMS\Form\Domain\Runtime.
+        // TYPO3\CMS\Form\Domain\Runtime\FormRuntime.
         //
         // For example: {formState.formValues.<elemenIdentifier>}
         // or {<elemenIdentifier>}
         //
         // Both examples are equal to "$formRuntime->getFormState()->getFormValues()[<elemenIdentifier>]"
         // If the value is not a string nothing will be replaced.
+        // There is a special option value '{__currentTimestamp}'.
+        // This will be replaced with the current timestamp.
         $optionValue = preg_replace_callback('/{([^}]+)}/', function ($match) use ($formRuntime) {
-            $value = ObjectAccess::getPropertyPath($formRuntime, $match[1]);
-            if (!is_string($value)) {
+            if ($match[1] === '__currentTimestamp') {
+                $value = time();
+            } else {
+                // try to resolve the path '{...}' within the FormRuntime
+                $value = ObjectAccess::getPropertyPath($formRuntime, $match[1]);
+                if ($value === null) {
+                    // try to resolve the path '{...}' within the FinisherVariableProvider
+                    $value = ObjectAccess::getPropertyPath(
+                        $this->finisherContext->getFinisherVariableProvider(),
+                        $match[1]
+                    );
+                }
+            }
+            if (!is_string($value) && !is_int($value)) {
                 $value = '{' . $match[1] . '}';
             }
             return $value;
index 04d77d6..e3aeef7 100644 (file)
@@ -172,6 +172,7 @@ class EmailFinisher extends AbstractFinisher
 
         $standaloneView = $this->objectManager->get(StandaloneView::class);
         $standaloneView->setTemplatePathAndFilename($this->options['templatePathAndFilename']);
+        $standaloneView->assign('finisherVariableProvider', $this->finisherContext->getFinisherVariableProvider());
 
         if (isset($this->options['partialRootPaths']) && is_array($this->options['partialRootPaths'])) {
             $standaloneView->setPartialRootPaths($this->options['partialRootPaths']);
index 13d3da7..96e3585 100644 (file)
@@ -17,7 +17,9 @@ namespace TYPO3\CMS\Form\Domain\Finishers;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Extbase\Mvc\Controller\ControllerContext;
+use TYPO3\CMS\Extbase\Object\ObjectManager;
 use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
 
 /**
@@ -53,6 +55,13 @@ class FinisherContext
     protected $controllerContext;
 
     /**
+     * The assigned controller context which might be needed by the finisher.
+     *
+     * @var FinisherVariableProvider
+     */
+    protected $finisherVariableProvider;
+
+    /**
      * @param FormRuntime $formRuntime
      * @internal
      */
@@ -63,6 +72,15 @@ class FinisherContext
     }
 
     /**
+     * @api
+     */
+    public function initializeObject()
+    {
+        $this->finisherVariableProvider = GeneralUtility::makeInstance(ObjectManager::class)
+            ->get(FinisherVariableProvider::class);
+    }
+
+    /**
      * Cancels the finisher invocation after the current finisher
      *
      * @return void
@@ -114,4 +132,13 @@ class FinisherContext
     {
         return $this->controllerContext;
     }
+
+    /**
+     * @return FinisherVariableProvider
+     * @api
+     */
+    public function getFinisherVariableProvider(): FinisherVariableProvider
+    {
+        return $this->finisherVariableProvider;
+    }
 }
diff --git a/typo3/sysext/form/Classes/Domain/Finishers/FinisherVariableProvider.php b/typo3/sysext/form/Classes/Domain/Finishers/FinisherVariableProvider.php
new file mode 100644 (file)
index 0000000..c5fabea
--- /dev/null
@@ -0,0 +1,186 @@
+<?php
+declare(strict_types=1);
+namespace TYPO3\CMS\Form\Domain\Finishers;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+
+/**
+ * Store data for usage between the finishers.
+ *
+ * Scope: frontend
+ * **This class is NOT meant to be sub classed by developers.**
+ * @internal
+ */
+final class FinisherVariableProvider implements \ArrayAccess
+{
+
+    /**
+     * Two-dimensional object array storing the values. The first dimension is the finisher identifier,
+     * and the second dimension is the identifier for the data the finisher wants to store.
+     *
+     * @var array
+     */
+    protected $objects = [];
+
+    /**
+     * Add a variable to the finisher container.
+     *
+     * @param string $finisherIdentifier
+     * @param string $key
+     * @param mixed $value
+     * @return void
+     * @api
+     */
+    public function add(string $finisherIdentifier, string $key, $value)
+    {
+        $this->addOrUpdate($finisherIdentifier, $key, $value);
+    }
+
+    /**
+     * Add a variable to the Variable Container.
+     * In case the value is already inside, it is silently overridden.
+     *
+     * @param string $finisherIdentifier
+     * @param string $key
+     * @param mixed $value
+     * @return void
+     */
+    public function addOrUpdate(string $finisherIdentifier, string $key, $value)
+    {
+        if (!array_key_exists($finisherIdentifier, $this->objects)) {
+            $this->objects[$finisherIdentifier] = [];
+        }
+        $this->objects[$finisherIdentifier] = ArrayUtility::setValueByPath(
+            $this->objects[$finisherIdentifier],
+            $key,
+            $value,
+            '.'
+        );
+    }
+
+    /**
+     * Gets a variable which is stored
+     *
+     * @param string $finisherIdentifier
+     * @param string $key
+     * @param mixed $default
+     * @return mixed
+     * @api
+     */
+    public function get(string $finisherIdentifier, string $key, $default = null)
+    {
+        if ($this->exists($finisherIdentifier, $key)) {
+            return ArrayUtility::getValueByPath($this->objects[$finisherIdentifier], $key, '.');
+        }
+        return $default;
+    }
+
+    /**
+     * Determine whether there is a variable stored for the given key
+     *
+     * @param string $finisherIdentifier
+     * @param string $key
+     * @return bool
+     * @api
+     */
+    public function exists($finisherIdentifier, $key): bool
+    {
+        try {
+            ArrayUtility::getValueByPath($this->objects[$finisherIdentifier], $key, '.');
+        } catch (\RuntimeException $e) {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Remove a value from the variable container
+     *
+     * @param string $finisherIdentifier
+     * @param string $key
+     * @return void
+     * @api
+     */
+    public function remove(string $finisherIdentifier, string $key)
+    {
+        if ($this->exists($finisherIdentifier, $key)) {
+            $this->objects[$finisherIdentifier] = ArrayUtility::removeByPath(
+                $this->objects[$finisherIdentifier],
+                $key,
+                '.'
+            );
+        }
+    }
+
+    /**
+     * Clean up for serializing.
+     *
+     * @return array
+     */
+    public function __sleep()
+    {
+        return ['objects'];
+    }
+
+    /**
+     * Whether an offset exists
+     *
+     * @link http://php.net/manual/en/arrayaccess.offsetexists.php
+     * @param mixed $offset An offset to check for.
+     * @return bool TRUE on success or FALSE on failure.
+     */
+    public function offsetExists($offset)
+    {
+        return isset($this->objects[$offset]);
+    }
+
+    /**
+     * Offset to retrieve
+     *
+     * @link http://php.net/manual/en/arrayaccess.offsetget.php
+     * @param mixed $offset The offset to retrieve.
+     * @return mixed Can return all value types.
+     */
+    public function offsetGet($offset)
+    {
+        return $this->objects[$offset];
+    }
+
+    /**
+     * Offset to set
+     *
+     * @link http://php.net/manual/en/arrayaccess.offsetset.php
+     * @param mixed $offset The offset to assign the value to.
+     * @param mixed $value The value to set.
+     * @return void
+     */
+    public function offsetSet($offset, $value)
+    {
+        $this->objects[$offset] = $value;
+    }
+
+    /**
+     * Offset to unset
+     *
+     * @link http://php.net/manual/en/arrayaccess.offsetunset.php
+     * @param mixed $offset The offset to unset.
+     * @return void
+     */
+    public function offsetUnset($offset)
+    {
+        unset($this->objects[$offset]);
+    }
+}
index cb15100..1402251 100644 (file)
@@ -25,6 +25,148 @@ use TYPO3\CMS\Form\Domain\Model\FormElements\FormElementInterface;
  * This finisher saves the data from a submitted form into
  * a database table.
  *
+ * Configuration
+ * =============
+ *
+ * options.table (mandatory)
+ * -------------
+ *   Save or update values into this table
+ *
+ * options.mode (default: insert)
+ * ------------
+ *   Possible values are 'insert' or 'update'.
+ *
+ *   insert: will create a new database row with the values from the
+ *           submitted form and/or some predefined values.
+ *           @see options.elements and options.databaseFieldMappings
+ *   update: will update a given database row with the values from the
+ *           submitted form and/or some predefined values.
+ *           'options.whereClause' is then required.
+ *
+ * options.whereClause
+ * -------------------
+ *   This where clause will be used for an database update action
+ *
+ * options.elements
+ * ----------------
+ *   Use this to map form element values to existing database columns.
+ *   Each key within options.elements has to match with a
+ *   form element identifier within your form definition.
+ *   The value for each key within options.elements is an array with
+ *   additional informations.
+ *
+ * options.elements.<elementIdentifier>.mapOnDatabaseColumn (mandatory)
+ * --------------------------------------------------------
+ *   The value from the submitted form element with the identifier
+ *   '<elementIdentifier>' will be written into this database column
+ *
+ * options.elements.<elementIdentifier>.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
+ *   '<elementIdentifier>' is empty (think about password fields etc.)
+ *
+ * options.elements.<elementIdentifier>.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 informations.
+ *
+ *   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.<databaseColumnName>.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 {<elementIdentifier>} to get the value
+ *   from the submitted form element with the identifier
+ *   <elementIdentifier>
+ *
+ *   Don't be confused. If you use the FormRuntime accessor feature within
+ *   options.databaseColumnMappings, the functionality is nearly equal
+ *   to the the options.elements configuration.
+ *
+ * options.databaseColumnMappings.<databaseColumnName>.skipIfValueIsEmpty (default: false)
+ * ---------------------------------------------------------------------
+ *   Set this to true if the database column should not be written
+ *   if the value from
+ *   options.databaseColumnMappings.<databaseColumnName>.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
+ *
+ * Multiple database operations
+ * ============================
+ *
+ * You can write options as an array to perform multiple database oprtations.
+ *
+ *  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:
+ *              uid_foreign: '{SaveToDatabase.insertedUids.1}'
+ *
+ * This would perform 2 database operations.
+ * One insert and one update.
+ * You cann access the inserted uids with '{SaveToDatabase.insertedUids.<theArrayKeyNumberWithinOptions>}'
+ * If you perform an insert operation, the value of the inserted database row will be stored
+ * within the FinisherVariableProvider.
+ * <theArrayKeyNumberWithinOptions> references to the numeric key within options
+ * within which the insert operation is executed.
+ *
  * Scope: frontend
  */
 class SaveToDatabaseFinisher extends AbstractFinisher
@@ -38,9 +180,15 @@ class SaveToDatabaseFinisher extends AbstractFinisher
         'mode' => 'insert',
         'whereClause' => [],
         'elements' => [],
+        'databaseColumnMappings' => [],
     ];
 
     /**
+     * @var \TYPO3\CMS\Core\Database\Connection
+     */
+    protected $databaseConnection = null;
+
+    /**
      * Executes this finisher
      * @see AbstractFinisher::execute()
      *
@@ -49,50 +197,58 @@ class SaveToDatabaseFinisher extends AbstractFinisher
      */
     protected function executeInternal()
     {
-        if (
-            $this->options['mode'] === 'update'
-            && empty($this->options['whereClause'])
-        ) {
-            throw new FinisherException(
-                'An empty option "whereClause" is not allowed in update mode.',
-                1480469086
-            );
+        if (!is_array($this->options)) {
+            $options[] = $this->options;
+        } else {
+            $options = $this->options;
         }
 
+        foreach ($options as $optionKey => $option) {
+            $this->options = $option;
+            $this->process($optionKey);
+        }
+    }
+
+    /**
+     * Perform the current database operation
+     *
+     * @param int $iterationCount
+     * @return void
+     */
+    protected function process(int $iterationCount)
+    {
+        $this->throwExceptionOnInconsistentConfiguration();
+
         $table = $this->parseOption('table');
         $elementsConfiguration = $this->parseOption('elements');
+        $databaseColumnMappingsConfiguration = $this->parseOption('databaseColumnMappings');
 
-        $databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
-        $schemaManager = $databaseConnection->getSchemaManager();
+        $this->databaseConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table);
 
-        if ($schemaManager->tablesExist([$table]) === false) {
-            throw new FinisherException('The table "' . $table . '" does not exist.', 1476362091);
-        }
-
-        $databaseColumns = $schemaManager->listTableColumns($table);
-        foreach ($elementsConfiguration as $elementIdentifier => $elementConfiguration) {
-            if (!array_key_exists($elementConfiguration['mapOnDatabaseColumn'], $databaseColumns)) {
-                throw new FinisherException(
-                    'The column "' . $elementConfiguration['mapOnDatabaseColumn'] . '" does not exist in table "' . $table . '".',
-                    1476362572
-                );
+        $databaseData = [];
+        foreach ($databaseColumnMappingsConfiguration as $databaseColumnName => $databaseColumnConfiguration) {
+            $value = $this->parseOption('databaseColumnMappings.' . $databaseColumnName . '.value');
+            if (
+                empty($value)
+                && $databaseColumnConfiguration['skipIfValueIsEmpty'] === true
+            ) {
+                continue;
             }
-        }
 
-        $formRuntime = $this->finisherContext->getFormRuntime();
+            $databaseData[$databaseColumnName] = $value;
+        }
 
-        $insertData = [];
-        foreach ($this->finisherContext->getFormValues() as $elementIdentifier => $elementValue) {
+        foreach ($this->getFormValues() as $elementIdentifier => $elementValue) {
             if (
                 $elementValue === null
                 && isset($elementsConfiguration[$elementIdentifier])
-                && isset($elementsConfiguration[$elementIdentifier]['skipIfValueIsNull'])
-                && $elementsConfiguration[$elementIdentifier]['skipIfValueIsNull'] === true
+                && isset($elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'])
+                && $elementsConfiguration[$elementIdentifier]['skipIfValueIsEmpty'] === true
             ) {
                 continue;
             }
 
-            $element = $formRuntime->getFormDefinition()->getElementByIdentifier($elementIdentifier);
+            $element = $this->getElementByIdentifier($elementIdentifier);
             if (
                 !$element instanceof FormElementInterface
                 || !isset($elementsConfiguration[$elementIdentifier])
@@ -114,19 +270,88 @@ class SaveToDatabaseFinisher extends AbstractFinisher
                     $elementValue = $elementValue->getOriginalResource()->getProperty('uid_local');
                 }
             }
-            $insertData[$elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn']] = $elementValue;
+            $databaseData[$elementsConfiguration[$elementIdentifier]['mapOnDatabaseColumn']] = $elementValue;
         }
 
-        if (!empty($insertData)) {
+        $this->saveToDatabase($databaseData, $table, $iterationCount);
+    }
+
+    /**
+     * Save or insert the values from
+     * $databaseData into the table $table
+     *
+     * @param [] $databaseData
+     * @param string $table
+     * @param int $iterationCount
+     * @return void
+     */
+    protected function saveToDatabase(array $databaseData, string $table, int $iterationCount)
+    {
+        if (!empty($databaseData)) {
             if ($this->options['mode'] === 'update') {
-                $databaseConnection->update(
+                $whereClause = $this->options['whereClause'];
+                foreach ($whereClause as $columnName => $columnValue) {
+                    $whereClause[$columnName] = $this->parseOption('whereClause.' . $columnName);
+                }
+                $this->databaseConnection->update(
                     $table,
-                    $insertData,
-                    $this->options['whereClause']
+                    $databaseData,
+                    $whereClause
                 );
             } else {
-                $databaseConnection->insert($table, $insertData);
+                $this->databaseConnection->insert($table, $databaseData);
+                $insertedUid = (int)$this->databaseConnection->lastInsertId($table);
+                $this->finisherContext->getFinisherVariableProvider()->add(
+                    $this->shortFinisherIdentifier,
+                    'insertedUids.' . $iterationCount,
+                    $insertedUid
+                );
             }
         }
     }
+
+    /**
+     * Throws an exception if some inconsistent configuration
+     * are detected.
+     *
+     * @return void
+     * @throws FinisherException
+     */
+    protected function throwExceptionOnInconsistentConfiguration()
+    {
+        if (
+            $this->options['mode'] === 'update'
+            && empty($this->options['whereClause'])
+        ) {
+            throw new FinisherException(
+                'An empty option "whereClause" is not allowed in update mode.',
+                1480469086
+            );
+        }
+    }
+
+    /**
+     * Returns the values of the submitted form
+     *
+     * @return []
+     */
+    protected function getFormValues(): array
+    {
+        return $this->finisherContext->getFormValues();
+    }
+
+    /**
+     * Returns a form element object for a given identifier.
+     *
+     * @param string $elementIdentifier
+     * @return NULL|FormElementInterface
+     */
+    protected function getElementByIdentifier(string $elementIdentifier)
+    {
+        return $this
+            ->finisherContext
+            ->getFormRuntime()
+            ->getFormDefinition()
+            ->getElementByIdentifier($elementIdentifier);
+    }
 }
index 736b978..193b72e 100644 (file)
@@ -213,9 +213,13 @@ TYPO3:
                 #whereClause: []
                 #elements:
                 #  <elementIdentifier>:
-                #    mapOnDatabaseColumn: sender_name
+                #    mapOnDatabaseColumn: <databaseColumnName>
                 #    saveFileIdentifierInsteadOfUid: false
-                #    skipIfValueIsNull: false
+                #    skipIfValueIsEmpty: false
+                #databaseColumnMappings:
+                #  <databaseColumnName>:
+                #    value: 'someValue'
+                #    skipIfValueIsEmpty: false
 
           ### VALIDATORS ###
           validatorsDefinition:
index efae3d9..b2051b7 100644 (file)
@@ -643,9 +643,7 @@ TYPO3:
                 iconIdentifier: 't3-form-icon-finisher'
                 label: 'formEditor.elements.Form.finisher.SaveToDatabase.editor.header.label'
                 predefinedDefaults:
-                  options:
-                    table: ''
-                    elements:
+                  options: []
 
             DeleteUploads:
               formEditor:
index 39337ec..a790a7f 100644 (file)
@@ -263,4 +263,33 @@ class AbstractFinisherTest extends \TYPO3\Components\TestingFramework\Core\Unit\
 
         $this->assertSame($expected, $mockAbstractFinisher->_call('parseOption', 'subject'));
     }
+
+    /**
+     * @test
+     */
+    public function parseOptionReturnsTimestampIfOptionValueIsATimestampRequestTrigger()
+    {
+        $mockAbstractFinisher = $this->getAccessibleMockForAbstractClass(
+            AbstractFinisher::class,
+            [],
+            '',
+            false
+        );
+
+        $mockAbstractFinisher->_set('options', [
+            'crdate' => '{__currentTimestamp}'
+        ]);
+
+        $finisherContextProphecy = $this->prophesize(FinisherContext::class);
+
+        $formRuntimeProphecy = $this->prophesize(FormRuntime::class);
+
+        $finisherContextProphecy->getFormRuntime(Argument::cetera())
+            ->willReturn($formRuntimeProphecy->reveal());
+
+        $mockAbstractFinisher->_set('finisherContext', $finisherContextProphecy->reveal());
+
+        $expected = '#^([0-9]{10})$#';
+        $this->assertEquals(1, preg_match($expected, $mockAbstractFinisher->_call('parseOption', 'crdate')));
+    }
 }
diff --git a/typo3/sysext/form/Tests/Unit/Domain/Finishers/SaveToDatabaseFinisherTest.php b/typo3/sysext/form/Tests/Unit/Domain/Finishers/SaveToDatabaseFinisherTest.php
new file mode 100644 (file)
index 0000000..5924012
--- /dev/null
@@ -0,0 +1,45 @@
+<?php
+namespace TYPO3\CMS\Form\Tests\Unit\Domain\Finishers;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Form\Domain\Finishers\Exception\FinisherException;
+use TYPO3\CMS\Form\Domain\Finishers\SaveToDatabaseFinisher;
+
+/**
+ * Test case
+ */
+class SaveToDatabaseFinisherTest extends \TYPO3\Components\TestingFramework\Core\Unit\UnitTestCase
+{
+
+    /**
+     * @test
+     */
+    public function throwExceptionOnInconsistentConfigurationThrowExceptionOnInconsistentConfiguration()
+    {
+        $this->expectException(FinisherException::class);
+        $this->expectExceptionCode(1480469086);
+
+        $mockSaveToDatabaseFinisher = $this->getAccessibleMock(SaveToDatabaseFinisher::class, [
+            'dummy'
+        ], [], '', false);
+
+        $mockSaveToDatabaseFinisher->_set('options', [
+            'mode' => 'update',
+            'whereClause' => '',
+        ]);
+
+        $mockSaveToDatabaseFinisher->_call('throwExceptionOnInconsistentConfiguration');
+    }
+}