[BUGFIX] FormEngine: Inline new intermediate record placeholders 58/43958/8
authorChristian Kuhn <lolli@schwarzbu.ch>
Sat, 10 Oct 2015 00:13:46 +0000 (02:13 +0200)
committerAnja Leichsenring <aleichsenring@ab-softlab.de>
Sun, 11 Oct 2015 13:07:38 +0000 (15:07 +0200)
The patch adds the inline configuration to the data compiler array
when children data is compiled instead of setting this only for
the render engine. Data provider can now use this data. The data
provider that initializes new rows is refactored and now sets
the uid of a "child-child" if an intermediate child is compiled.
This fixes a bug in the placeholder handling that now resolves
placeholder data for new inline records created by inline ajax
controller correctly.

Resolves: #70577
Releases: master
Change-Id: I7c424e159a954824a947e1df3cfbb410e15d2b78
Reviewed-on: http://review.typo3.org/43958
Reviewed-by: Mathias Schreiber <mathias.schreiber@wmdb.de>
Tested-by: Mathias Schreiber <mathias.schreiber@wmdb.de>
Reviewed-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring <aleichsenring@ab-softlab.de>
typo3/sysext/backend/Classes/Controller/FormInlineAjaxController.php
typo3/sysext/backend/Classes/Form/Container/InlineControlContainer.php
typo3/sysext/backend/Classes/Form/FormDataCompiler.php
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRowInitializeNew.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaInline.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/DatabaseRowInitializeNewTest.php

index cc79b06..8e9dc34 100644 (file)
@@ -118,7 +118,11 @@ class FormInlineAjaxController
             'vanillaUid' => $childVanillaUid,
             'inlineFirstPid' => $inlineFirstPid,
             'inlineOverruleTypesArray' => $inlineOverruleTypesArray,
+            'inlineParentConfig' => $parentConfig,
         ];
+        if ($childChildUid) {
+            $formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
+        }
         $childData = $formDataCompiler->compile($formDataCompilerInput);
 
         // Set default values for new created records
@@ -196,8 +200,8 @@ class FormInlineAjaxController
             // @todo: prepared in $childData. Otherwise fields that use this relation like for instance
             // @todo: the placeholder relation does not work for "fresh" children. This stuff here
             // @todo: probably needs to be moved somewhere after "initializeNew" data provider.
-            // There is an existing child-child, but it should not be rendered directly. Still, the intermediate
-            // record must point to the child table
+            // There is an existing child-child, but it should not be rendered directly since useCombination
+            // is off. Still, the intermediate record must point to the child table
             if ($childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['type'] === 'select') {
                 $childData['databaseRow'][$parentConfig['foreign_selector']] = [
                     $childChildUid,
@@ -211,7 +215,6 @@ class FormInlineAjaxController
         }
 
         $childData['inlineParentUid'] = (int)$parent['uid'];
-        $childData['inlineParentConfig'] = $parentConfig;
         // @todo: needed?
         $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
         // @todo: needed?
@@ -300,10 +303,10 @@ class FormInlineAjaxController
         /** @var FormDataCompiler $formDataCompiler */
         $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
         $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
-        $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
         // Set flag in config so that only the fields are rendered
         // @todo: Solve differently / rename / whatever
-        $parentConfig['renderFieldsOnly'] = true;
+        $parentData['processedTca']['columns'][$parentFieldName]['config']['renderFieldsOnly'] = true;
+        $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
 
         // Child, a record from this table should be rendered
         $child = $inlineStackProcessor->getUnstableStructure();
@@ -311,7 +314,6 @@ class FormInlineAjaxController
         $childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid']);
 
         $childData['inlineParentUid'] = (int)$parent['uid'];
-        $childData['inlineParentConfig'] = $parentConfig;
         // @todo: needed?
         $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
         // @todo: needed?
@@ -435,7 +437,6 @@ class FormInlineAjaxController
                 $childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid);
 
                 $childData['inlineParentUid'] = (int)$parent['uid'];
-                $childData['inlineParentConfig'] = $parentConfig;
                 // @todo: needed?
                 $childData['inlineStructure'] = $inlineStackProcessor->getStructure();
                 // @todo: needed?
@@ -555,6 +556,7 @@ class FormInlineAjaxController
             'tableName' => $childTableName,
             'vanillaUid' => (int)$childUid,
             'inlineFirstPid' => $parentData['inlineFirstPid'],
+            'inlineParentConfig' => $parentConfig,
             'inlineOverruleTypesArray' => $inlineOverruleTypesArray,
         ];
         // For foreign_selector with useCombination $mainChild is the mm record
index 7d61d45..d41d356 100644 (file)
@@ -240,6 +240,7 @@ class InlineControlContainer extends AbstractContainer
         $sortableRecordUids = [];
         foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) {
             $options['inlineParentUid'] = $row['uid'];
+            // @todo: this can be removed if this container no longer sets additional info to $config
             $options['inlineParentConfig'] = $config;
             $options['inlineData'] = $this->inlineData;
             $options['inlineStructure'] = $inlineStackProcessor->getStructure();
index c67ca9f..21de064 100644 (file)
@@ -53,6 +53,8 @@ class FormDataCompiler
      *
      * @param array $initialData Initial set of data to map into result array
      * @return array Result with data
+     * @throws \InvalidArgumentException
+     * @throws \UnexpectedValueException
      */
     public function compile(array $initialData)
     {
@@ -120,7 +122,7 @@ class FormDataCompiler
      */
     protected function initializeResultArray()
     {
-        return array(
+        return [
             // Either "edit" or "new"
             'command' => '',
             // Table name of the handled row
@@ -145,33 +147,33 @@ class FormDataCompiler
             // For "new" this is the fully initialized row with defaults
             // The database row. For "edit" fixVersioningPid() was applied already.
             // @todo: rename to valueStructure or handledData or similar
-            'databaseRow' => array(),
+            'databaseRow' => [],
             // The "effective" page uid we're working on. This is the uid of a page if a page is edited, or the uid
             // of the parent page if a page or other record is added, or 0 if a record is added or edited below root node.
             'effectivePid' => 0,
             // Rootline of page the record that is handled is located at as created by BackendUtility::BEgetRootline()
-            'rootline' => array(),
+            'rootline' => [],
             // For "edit", this is the permission bitmask of the page that is edited, or of the page a record is located at
             // For "new", this is the permission bitmask of the page the record is added to
             // @todo: Remove if not needed on a lower level
             'userPermissionOnPage' => 0,
             // Full user TsConfig
-            'userTsConfig' => array(),
+            'userTsConfig' => [],
             // Full page TSConfig of the page that is edited or of the parent page if a record is added.
             // This includes any defaultPageTSconfig and is merged with user TsConfig page. section. After type
             // of handled record was determined, record type specific settings [TCEFORM.][tableName.][field.][types.][type.]
             // are merged into [TCEFORM.][tableName.][field.]. Array keys still contain the concatenation dots.
-            'pageTsConfig' => array(),
+            'pageTsConfig' => [],
             // Not changed TCA of handled table
-            'vanillaTableTca' => array(),
+            'vanillaTableTca' => [],
             // Not changed TCA of parent page row if record is edited or added below a page and not root node
             'vanillaParentPageTca' => null,
             // List of available system languages. Array key is the system language uid, value array
             // contains details of the record, with iso code resolved. Key is the sys_language_uid uid.
-            'systemLanguageRows' => array(),
+            'systemLanguageRows' => [],
             // If the page that is handled has "page_language_overlay" records (page has localizations in
             // different languages), then this array holds those rows.
-            'pageLanguageOverlayRows' => array(),
+            'pageLanguageOverlayRows' => [],
             // If the handled row is a localized row, this entry hold the default language row array
             'defaultLanguageRow' => null,
             // If the handled row is a localized row and a transOrigDiffSourceField is defined, this
@@ -182,25 +184,36 @@ class FormDataCompiler
             'defaultLanguageDiffRow' => null,
             // With userTS options.additionalPreviewLanguages set, field values of additional languages
             // can be shown. This array holds those additional language records, Array key is sys_language_uid.
-            'additionalLanguageRows' => array(),
+            'additionalLanguageRows' => [],
             // The tca record type value of the record. Forced to string, there can be "named" type values.
             'recordTypeValue' => '0',
             // TCA of table with processed fields. After processing, this array contains merged and resolved
             // array data, items were resolved, only used types are set, renderTypes are set.
-            'processedTca' => array(),
+            'processedTca' => [],
             // List of columns to be processed by data provider. Array value is the column name.
-            'columnsToProcess' => array(),
+            'columnsToProcess' => [],
             // If set to TRUE, no wizards are calculated and rendered later
             'disabledWizards' => false,
             // BackendUser->uc['inlineView'] - This array holds status of expand / collapsed inline items
             // @todo: better documentation of nesting behaviour and bug fixing in this area
-            'inlineExpandCollapseStateArray' => array(),
+            'inlineExpandCollapseStateArray' => [],
             // The "entry" pid for inline records. Nested inline records can potentially hang around on different
             // pid's, but the entry pid is needed for AJAX calls, so that they would know where the action takes place on the page structure.
             'inlineFirstPid' => null,
             // This array of fields will be set as hidden-fields instead of rendered normally!
             // This is used by EditDocumentController to force some field values if set as "overrideVals" in _GP
             'overrideValues' => [],
+
+            // The "config" section of an inline parent, prepared and sanitized by TcaInlineConfiguration provider
+            'inlineParentConfig' => [],
+            // Uid of a "child-child" if a new record of an intermediate table is compiled to an existing child. This
+            // happens if foreign_selector in parent inline config is set. It will be used by default database row
+            // data providers to set this as value for the foreign_selector field on the intermediate table. One use
+            // case is FAL, where for instance a tt_content parent adds relation to an existing sys_file record and
+            // should set the uid of the existing sys_file record as uid_local - the foreign_selector of this inline
+            // configuration - of the new intermediate sys_file_reference record. Data provider that are called later
+            // will then use this relation to resolve for instance input placeholder relation values.
+            'inlineChildChildUid' => null,
             // This is the "foreign_types" section of a parent inline configuration that can be used to
             // overrule the types TCA section of a child element.
             'inlineOverruleTypesArray' => [],
@@ -213,7 +226,7 @@ class FormDataCompiler
             // child record that was not yet localized.
             'inlineIsDefaultLanguage' => false,
             // If set, inline children will be resolved. This is set to FALSE in inline ajax context where new children
-            // are created an existing children don't matter much.
+            // are created and existing children don't matter much.
             'inlineResolveExistingChildren' => true,
             // @todo - for input placeholder inline to suppress an infinite loop, this *may* become obsolete if
             // @todo compilation of certain fields is possible
@@ -226,6 +239,6 @@ class FormDataCompiler
             'inlineData' => [],
             'inlineStructure' => [],
 
-        );
+        ];
     }
 }
index 3f02584..54e51c2 100644 (file)
@@ -28,44 +28,86 @@ class DatabaseRowInitializeNew implements FormDataProviderInterface
      *
      * @param array $result
      * @return array
+     * @throws \UnexpectedValueException
      */
     public function addData(array $result)
     {
         if ($result['command'] !== 'new') {
             return $result;
         }
+        if (!is_array($result['databaseRow'])) {
+            throw new \UnexpectedValueException(
+                'databaseRow of table ' . $result['tableName'] . ' is not an array',
+                1444431128
+            );
+        }
 
-        $databaseRow = array();
+        $result = $this->setDefaultsFromUserTsConfig($result);
+        $result = $this->setDefaultsFromPageTsConfig($result);
+        $result = $this->setDefaultsFromNeighborRow($result);
+        $result = $this->setDefaultsFromDevVals($result);
+        $result = $this->setDefaultsFromInlineRelations($result);
 
-        $tableName = $result['tableName'];
-        $tableNameWithDot = $tableName . '.';
+        // Set pid to vanillaUid. This means, it *can* be negative, if the record is added relative to another record
+        // @todo: For inline records it should be possible to set the pid here via TCAdefaults, but
+        // @todo: those values would be overwritten by this 'pid' setter
+        $result['databaseRow']['pid'] = $result['vanillaUid'];
+
+        return $result;
+    }
 
+    /**
+     * Set defaults defined by user ts "TCAdefaults"
+     *
+     * @param array $result Result array
+     * @return array Modified result array
+     */
+    protected function setDefaultsFromUserTsConfig(array $result)
+    {
+        $tableNameWithDot = $result['tableName'] . '.';
         // Apply default values from user typo script "TCAdefaults" if any
         if (isset($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot])
             && is_array($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot])
         ) {
             foreach ($result['userTsConfig']['TCAdefaults.'][$tableNameWithDot] as $fieldName => $fieldValue) {
                 if (isset($result['vanillaTableTca']['columns'][$fieldName])) {
-                    $databaseRow[$fieldName] = $fieldValue;
+                    $result['databaseRow'][$fieldName] = $fieldValue;
                 }
             }
         }
+        return $result;
+    }
 
-        // Apply defaults from pageTsConfig
-        // @todo: For inline records it should be possible to set the pid here via TCAdefaults, but
-        // @todo: those values would be overwritten by 'pid' setter later below again.
+    /**
+     * Set defaults defined by page ts "TCAdefaults"
+     *
+     * @param array $result Result array
+     * @return array Modified result array
+     */
+    protected function setDefaultsFromPageTsConfig(array $result)
+    {
+        $tableNameWithDot = $result['tableName'] . '.';
         if (isset($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
             && is_array($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot])
         ) {
             foreach ($result['pageTsConfig']['TCAdefaults.'][$tableNameWithDot] as $fieldName => $fieldValue) {
                 if (isset($result['vanillaTableTca']['columns'][$fieldName])) {
-                    $databaseRow[$fieldName] = $fieldValue;
+                    $result['databaseRow'][$fieldName] = $fieldValue;
                 }
             }
         }
+        return $result;
+    }
 
-        // If a neighbor row is given (if vanillaUid was negative), field can be initialized with values
-        // from neighbor for fields registered in TCA['ctrl']['useColumnsForDefaultValues'].
+    /**
+     * If a neighbor row is given (if vanillaUid was negative), field can be initialized with values
+     * from neighbor for fields registered in TCA['ctrl']['useColumnsForDefaultValues'].
+     *
+     * @param array $result Result array
+     * @return array Modified result array
+     */
+    protected function setDefaultsFromNeighborRow(array $result)
+    {
         if (is_array($result['neighborRow'])
             && !empty($result['vanillaTableTca']['ctrl']['useColumnsForDefaultValues'])
         ) {
@@ -74,28 +116,82 @@ class DatabaseRowInitializeNew implements FormDataProviderInterface
                 if (isset($result['vanillaTableTca']['columns'][$fieldName])
                     && isset($result['neighborRow'][$fieldName])
                 ) {
-                    $databaseRow[$fieldName] = $result['neighborRow'][$fieldName];
+                    $result['databaseRow'][$fieldName] = $result['neighborRow'][$fieldName];
                 }
             }
         }
+        return $result;
+    }
 
-        // Apply default values from GET / POST
-        // @todo: Fetch this stuff from request object as soon as modules were moved to PSR-7
+    /**
+     * Apply default values from GET / POST
+     *
+     * @todo: Fetch this stuff from request object as soon as modules were moved to PSR-7,
+     * @todo: or hand values over via $result array, so the _GP access is transferred to
+     * @todo: controllers concern.
+     *
+     * @param array $result Result array
+     * @return array Modified result array
+     */
+    protected function setDefaultsFromDevVals(array $result)
+    {
+        $tableName = $result['tableName'];
         $defaultValuesFromGetPost = GeneralUtility::_GP('defVals');
         if (isset($defaultValuesFromGetPost[$tableName])
             && is_array($defaultValuesFromGetPost[$tableName])
         ) {
             foreach ($defaultValuesFromGetPost[$tableName] as $fieldName => $fieldValue) {
                 if (isset($result['vanillaTableTca']['columns'][$fieldName])) {
-                    $databaseRow[$fieldName] = $fieldValue;
+                    $result['databaseRow'][$fieldName] = $fieldValue;
                 }
             }
         }
+        return $result;
+    }
 
-        // Set pid to vanillaUid. This means, it *can* be negative, if the record is added relative to another record
-        $databaseRow['pid'] = $result['vanillaUid'];
+    /**
+     * Inline scenario if a new intermediate record to an existing child-child is
+     * compiled. Set "foreign_selector" field of this intermediate row to given
+     * "childChildUid". See TcaDataCompiler array comment of inlineChildChildUid
+     * for more details.
+     *
+     * @param array $result Result array
+     * @return array Modified result array
+     * @throws \UnexpectedValueException
+     */
+    protected function setDefaultsFromInlineRelations(array $result)
+    {
+        if ($result['inlineChildChildUid'] === null) {
+            return $result;
+        }
+        if (!is_int($result['inlineChildChildUid'])) {
+            throw new \UnexpectedValueException(
+                'An inlineChildChildUid is given for table ' . $result['tableName'] . ', but is not an integer',
+                1444434103
+            );
+        }
+        if (!isset($result['inlineParentConfig']['foreign_selector'])) {
+            throw new \UnexpectedValueException(
+                'An inlineChildChildUid is given for table ' . $result['tableName'] . ', but no foreign_selector in inlineParentConfig',
+                1444434102
+            );
+        }
+        $selectorFieldName = $result['inlineParentConfig']['foreign_selector'];
+        if (!isset($result['vanillaTableTca']['columns'][$selectorFieldName]['config']['type'])
+            || ($result['vanillaTableTca']['columns'][$selectorFieldName]['config']['type'] !== 'select'
+                && $result['vanillaTableTca']['columns'][$selectorFieldName]['config']['type'] !== 'group'
+            )
+        ) {
+            throw new \UnexpectedValueException(
+                $selectorFieldName . ' is target type of a foreign_selector field to table ' . $result['tableName'] . ' and must be either a select or group type field',
+                1444434104
+            );
+        }
+
+        if ($result['inlineChildChildUid']) {
+            $result['databaseRow'][$selectorFieldName] = $result['inlineChildChildUid'];
+        }
 
-        $result['databaseRow'] = $databaseRow;
         return $result;
     }
 }
index 78b2705..c628a44 100644 (file)
@@ -222,6 +222,7 @@ class TcaInline extends AbstractDatabaseRecordProvider implements FormDataProvid
             'vanillaUid' => (int)$childUid,
             'inlineFirstPid' => $result['inlineFirstPid'],
             'inlineOverruleTypesArray' => $inlineOverruleTypesArray,
+            'inlineParentConfig' => $parentConfig,
         ];
         // For foreign_selector with useCombination $mainChild is the mm record
         // and $combinationChild is the child-child. For "normal" relations, $mainChild
index cb80ae0..f49fa3a 100644 (file)
@@ -58,6 +58,37 @@ class DatabaseRowInitializeNewTest extends UnitTestCase
     /**
      * @test
      */
+    public function addDataThrowsExceptionIfDatabaseRowIsNotArray()
+    {
+        $input = [
+            'command' => 'new',
+            'databaseRow' => 'not-an-array',
+        ];
+        $this->setExpectedException(\UnexpectedValueException::class, $this->anything(), 1444431128);
+        $this->subject->addData($input);
+    }
+
+    /**
+     * @test
+     */
+    public function addDataKeepsGivenDefaultsIfCommandIsNew()
+    {
+        $input = [
+            'command' => 'new',
+            'tableName' => 'aTable',
+            'vanillaUid' => 23,
+            'databaseRow' => [
+                'aField' => 42,
+            ],
+        ];
+        $expected = $input;
+        $expected['databaseRow']['pid'] = 23;
+        $this->assertSame($expected, $this->subject->addData($input));
+    }
+
+    /**
+     * @test
+     */
     public function addDataSetsDefaultDataFormUserTsIfColumnIsDenfinedInTca()
     {
         $input = [
@@ -481,6 +512,90 @@ class DatabaseRowInitializeNewTest extends UnitTestCase
     /**
      * @test
      */
+    public function addDataThrowsExceptionWithGivenChildChildUidButMissingInlineConfig()
+    {
+        $input = [
+            'command' => 'new',
+            'databaseRow' => [],
+            'inlineChildChildUid' => 42,
+        ];
+        $this->setExpectedException(\UnexpectedValueException::class, $this->anything(), 1444434102);
+        $this->subject->addData($input);
+    }
+
+    /**
+     * @test
+     */
+    public function addDataThrowsExceptionWithGivenChildChildUidButIsNotInteger()
+    {
+        $input = [
+            'command' => 'new',
+            'databaseRow' => [],
+            'inlineChildChildUid' => '42',
+        ];
+        $this->setExpectedException(\UnexpectedValueException::class, $this->anything(), 1444434103);
+        $this->subject->addData($input);
+    }
+
+    /**
+     * @test
+     */
+    public function addDataSetsForeignSelectorFieldToValueOfChildChildUid()
+    {
+        $input = [
+            'command' => 'new',
+            'tableName' => 'aTable',
+            'databaseRow' => [],
+            'inlineChildChildUid' => 42,
+            'inlineParentConfig' => [
+                'foreign_selector' => 'theForeignSelectorField',
+            ],
+            'vanillaTableTca' => [
+                'columns' => [
+                    'theForeignSelectorField' => [
+                        'config' => [
+                            'type' => 'group',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        $expected = $input;
+        $expected['databaseRow']['theForeignSelectorField'] = 42;
+        $expected['databaseRow']['pid'] = null;
+        $this->assertSame($expected, $this->subject->addData($input));
+    }
+
+    /**
+     * @test
+     */
+    public function addDataThrowsExceptionIfForeignSelectorDoesNotPointToGroupOrSelectField()
+    {
+        $input = [
+            'command' => 'new',
+            'tableName' => 'aTable',
+            'databaseRow' => [],
+            'inlineChildChildUid' => 42,
+            'inlineParentConfig' => [
+                'foreign_selector' => 'theForeignSelectorField',
+            ],
+            'vanillaTableTca' => [
+                'columns' => [
+                    'theForeignSelectorField' => [
+                        'config' => [
+                            'type' => 'input',
+                        ],
+                    ],
+                ],
+            ],
+        ];
+        $this->setExpectedException(\UnexpectedValueException::class, $this->anything(), 1444434104);
+        $this->subject->addData($input);
+    }
+
+    /**
+     * @test
+     */
     public function addDataSetsPidToVanillaUid()
     {
         $input = [