Tuesday, June 10, 2014

TYPO3 - How to prevent empty FlexForm values from getting saved

When you create an ExtBase extension you can use a FlexForm to enable the user to change some of the TypoScript settings you define for the extension directly in the plugin settings. As an example, you could assume that your ExtBase extension has the following TypoScript settings:

plugin.tx_myext {
  settings {
    myFirstSetting = A value
    mySecondSetting = Another value
  }
}

If you add a FlexForm to your extension with two input fields, where the user can change both "myFirstSetting" and "mySecondSetting", then you will be able to access the user configured settings in your extension controllers through an array which is available in $this->settings. But - if the user just configures one of the two settings resets an already set setting in FlexForm to an empty value, then the second setting will be saved with an empty value and you have to merge the TypoScript and FlexForm settings manually in your controller and manually control, which setting overrides each other.

There has been a lot of discussion about this problem in the TYPO3 mailinglist and on forge and as far as I could find out, a solution is'nt implemented very easy, since you have to decide for each individual field, if you want to let an empty Flexform setting override a predefined TypoScript setting. Georg Ringer did a lot of work on handling this override-behaviour in his great News-Extension, where you define a TypoScript setting named "overrideFlexformSettingsIfEmpty", which contains all fields which should be overridden by TypoScript values, if the FlexForm contains empty values.

There is nothing wrong with the solution show in the News-Extension and in the ExtBase Wiki, but I took a different approach and tried to find out if it is possible to prevent empty FlexForm values from getting saved to the database when the plugin configuration is saved, so you don't have to do the merge/override manually.

Outgoing from the example described above the table tt_content contains a record for the ExtBase plugin, which holds the FlexForm configuration as XML. For the case, that the user just configured one of the two settings by FlexForm, the content in the field pi_flexform contains the following:


<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<T3FlexForms>
    <data>
        <sheet index="sDEF">
            <language index="lDEF">
                <field index="settings.myFirstSetting">
                    <value index="vDEF">A value</value>
                </field>
                <field index="settings.mySecondSetting">
                    <value index="vDEF"></value>
                </field>
            </language>
        </sheet>
    </data>
</T3FlexForms>

You will notice, that the setting mySecondSetting contains an empty value. This is the field I want to remove from the XML when saving the plugin configuration, since the setting should be overridden by the default value configured in TypoScript.

To do so, I used the TYPO3 core hook processDatamap_postProcessFieldArray, which allows me to modify the values being written to the database.

In ext_localconf.php I define the class to use for the DataHandler hook.


// DataHandler hook
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass']['NAMESPACE.' . $_EXTKEY] = 'NAMESPACE\MyExt\Hooks\DataHandlerHooks';

Next I created the class containing the new processDatamap_postProcessFieldArray function. The contents of the file is as following.


<?php
namespace NAMESPACE\MyExt\Hooks;

/***************************************************************
 *
 *  Copyright notice
 *
 *  (c) 2014 Torben Hansen <[email protected]>
 *
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * Hooks for DataHandler
 */
class DataHandlerHooks {

 /**
  * Checks if the fields defined in $checkFields are set in the data-array of pi_flexform. If a field is
  * present and contains an empty value, the field is unset.
  *
  * Structure of the checkFields array:
  *
  * array('sheet' => array('field1', 'field2'));
  *
  * @param string $status
  * @param string $table
  * @param string $id
  * @param array $fieldArray
  * @param \TYPO3\CMS\Core\DataHandling\DataHandler $reference
  *
  * @return void
  */
 public function processDatamap_postProcessFieldArray($status, $table, $id, &$fieldArray, &$reference) {
  if ($table === 'tt_content' && $status == 'update' && isset($fieldArray['pi_flexform'])) {
   $checkFields = array(
    'sDEF' => array(
     'settings.myFirstSetting',
     'settings.mySecondSetting'
    ),
   );

   $flexformData =  GeneralUtility::xml2array($fieldArray['pi_flexform']);

   foreach ($checkFields as $sheet => $fields) {
    foreach($fields as $field) {
     if (isset($flexformData['data'][$sheet]['lDEF'][$field]['vDEF']) &&
      $flexformData['data'][$sheet]['lDEF'][$field]['vDEF'] === '') {
      unset($flexformData['data'][$sheet]['lDEF'][$field]);
     }
    }

    // If remaining sheet does not contain fields, then remove the sheet
    if (isset($flexformData['data'][$sheet]['lDEF']) && $flexformData['data'][$sheet]['lDEF'] === array()) {
     unset($flexformData['data'][$sheet]);
    }
   }

   /** @var \TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools $flexFormTools */
   $flexFormTools = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Configuration\\FlexForm\\FlexFormTools');
   $fieldArray['pi_flexform'] = $flexFormTools->flexArray2Xml($flexformData, TRUE);
  }
 }

}

In the function I have defined an array which holds sheet- and fieldnames, which should be removed if empty. Next I iterate through all sheets and fields configured in $checkFields to check, if they really are empty and if so, I just remove them from the array.

You may notice, that I converted the given XML structure to an array, so I can easily iterate through all sheets/fields. When everything is finished, I use the TYPO3 FlexFormTools to convert the array back to FlexForm XML.

After implementing the new function by the hook, the resulting FlexForm does not contain fields with empty values as defined in my function and I can now use $this->settings in my controller (or retrieving the settings through the configurationManager) without the need to manually merge/override TypoScript and FlexForm settings.

Please note, that I just tested the above with TYPO3 6.2.