[BUGFIX] Restrict parent calculation for inline ajax 41/43941/2
authorChristian Kuhn <lolli@schwarzbu.ch>
Fri, 9 Oct 2015 15:31:48 +0000 (17:31 +0200)
committerMorton Jonuschat <m.jonuschat@mojocode.de>
Fri, 9 Oct 2015 16:12:42 +0000 (18:12 +0200)
If a child is expanded, added or synchronized in inline ajax call,
mostly only the inline tca configuration of the parent record is
needed, but not the whole thing.

The patch adds a dedicated InlineParentRecord data group that only
calls needed data providers to achieve this. This has the huge benefit
that the record is no longer fetched from db and only one single
field is prepared. This improves performance of these ajax calls.

Additionally, the patch solves an exception adding new image records
to a new tt_content record without persisting the tt_content record
before.

Resolves: #70549
Releases: master
Change-Id: I1312d00b77446eb89f5da3b898cdff975f057eae
Reviewed-on: http://review.typo3.org/43941
Reviewed-by: Morton Jonuschat <m.jonuschat@mojocode.de>
Tested-by: Morton Jonuschat <m.jonuschat@mojocode.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/FormDataCompiler.php
typo3/sysext/backend/Classes/Form/FormDataGroup/InlineParentRecord.php [new file with mode: 0644]
typo3/sysext/backend/Classes/Form/FormDataProvider/InitializeProcessedTca.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaColumnsProcessShowitem.php
typo3/sysext/backend/Tests/Unit/Form/FormDataGroup/InlineParentRecordTest.php [new file with mode: 0644]
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/InitializeProcessedTcaTest.php
typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaColumnsProcessShowitemTest.php
typo3/sysext/core/Configuration/DefaultConfiguration.php

index 4e4c57a..ba3be60 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Backend\Controller;
 use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use TYPO3\CMS\Backend\Form\FormDataCompiler;
+use TYPO3\CMS\Backend\Form\FormDataGroup\InlineParentRecord;
 use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
 use TYPO3\CMS\Backend\Form\InlineStackProcessor;
 use TYPO3\CMS\Backend\Form\NodeFactory;
@@ -62,25 +63,31 @@ class FormInlineAjaxController
         if (MathUtility::canBeInterpretedAsInteger($parent['uid'])) {
             $command = 'edit';
             $vanillaUid = (int)$parent['uid'];
+            $databaseRow = [
+                // TcaInlineExpandCollapseState needs the record uid
+                'uid' => (int)$parent['uid'],
+            ];
         } else {
             $command = 'new';
+            $databaseRow = [];
             $vanillaUid = (int)$inlineFirstPid;
         }
         $formDataCompilerInputForParent = [
             'vanillaUid' => $vanillaUid,
             'command' => $command,
             'tableName' => $parent['table'],
+            'databaseRow' => $databaseRow,
             'inlineFirstPid' => $inlineFirstPid,
+            'columnsToProcess' => [
+                $parentFieldName
+            ],
             // @todo: still needed?
             'inlineStructure' => $inlineStackProcessor->getStructure(),
             // Do not resolve existing children, we don't need them now
             'inlineResolveExistingChildren' => false,
         ];
-        // @todo: It would be enough to restrict parsing of parent to "inlineConfiguration" of according inline field only
-        // @todo: maybe, not even the database row is required?? We only need overruleTypesArray and sanitized configuration?
-        // @todo: Improving this area would significantly speed up this parsing!
         /** @var TcaDatabaseRecord $formDataGroup */
-        $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
+        $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
         /** @var FormDataCompiler $formDataCompiler */
         $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
         $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
@@ -275,17 +282,21 @@ class FormInlineAjaxController
             'vanillaUid' => (int)$parent['uid'],
             'command' => 'edit',
             'tableName' => $parent['table'],
+            'databaseRow' => [
+                // TcaInlineExpandCollapseState needs this
+                'uid' => (int)$parent['uid'],
+            ],
             'inlineFirstPid' => $inlineFirstPid,
+            'columnsToProcess' => [
+                $parentFieldName
+            ],
             // @todo: still needed?
             'inlineStructure' => $inlineStackProcessor->getStructure(),
             // Do not resolve existing children, we don't need them now
             'inlineResolveExistingChildren' => false,
         ];
-        // @todo: It would be enough to restrict parsing of parent to "inlineConfiguration" of according inline field only
-        // @todo: maybe, not even the database row is required?? We only need overruleTypesArray and sanitized configuration?
-        // @todo: Improving this area would significantly speed up this parsing!
         /** @var TcaDatabaseRecord $formDataGroup */
-        $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
+        $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
         /** @var FormDataCompiler $formDataCompiler */
         $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
         $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
@@ -371,17 +382,21 @@ class FormInlineAjaxController
                 'vanillaUid' => (int)$parent['uid'],
                 'command' => 'edit',
                 'tableName' => $parent['table'],
+                'databaseRow' => [
+                    // TcaInlineExpandCollapseState needs this
+                    'uid' => (int)$parent['uid'],
+                ],
                 'inlineFirstPid' => $inlineFirstPid,
+                'columnsToProcess' => [
+                    $parentFieldName
+                ],
                 // @todo: still needed?
                 'inlineStructure' => $inlineStackProcessor->getStructure(),
                 // Do not compile existing children, we don't need them now
                 'inlineCompileExistingChildren' => false,
             ];
-            // @todo: It would be enough to restrict parsing of parent to "inlineConfiguration" of according inline field only
-            // @todo: maybe, not even the database row is required?? We only need overruleTypesArray and sanitized configuration?
-            // @todo: Improving this area would significantly speed up this parsing!
             /** @var TcaDatabaseRecord $formDataGroup */
-            $formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
+            $formDataGroup = GeneralUtility::makeInstance(InlineParentRecord::class);
             /** @var FormDataCompiler $formDataCompiler */
             $formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
             $parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
@@ -514,7 +529,7 @@ class FormInlineAjaxController
     /**
      * Compile a full child record
      *
-     * @param array $result Result array of parent
+     * @param array $parentData Result array of parent
      * @param string $parentFieldName Name of parent field
      * @param int $childUid Uid of child to compile
      * @return array Full result array
@@ -523,9 +538,9 @@ class FormInlineAjaxController
      * @todo: Find something around that, eg. some option to force TcaInline provider to calculate a
      * @todo: specific forced-open element only :)
      */
-    protected function compileChild(array $result, $parentFieldName, $childUid)
+    protected function compileChild(array $parentData, $parentFieldName, $childUid)
     {
-        $parentConfig = $result['processedTca']['columns'][$parentFieldName]['config'];
+        $parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
         $childTableName = $parentConfig['foreign_table'];
         $overruleTypesArray = [];
         if (isset($parentConfig['foreign_types'])) {
@@ -539,7 +554,7 @@ class FormInlineAjaxController
             'command' => 'edit',
             'tableName' => $childTableName,
             'vanillaUid' => (int)$childUid,
-            'inlineFirstPid' => $result['inlineFirstPid'],
+            'inlineFirstPid' => $parentData['inlineFirstPid'],
             'overruleTypesArray' => $overruleTypesArray,
         ];
         // For foreign_selector with useCombination $mainChild is the mm record
index 2b817a8..710d9d0 100644 (file)
@@ -177,6 +177,7 @@ class FormDataCompiler
             // 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(),
             // 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.
diff --git a/typo3/sysext/backend/Classes/Form/FormDataGroup/InlineParentRecord.php b/typo3/sysext/backend/Classes/Form/FormDataGroup/InlineParentRecord.php
new file mode 100644 (file)
index 0000000..c8c2509
--- /dev/null
@@ -0,0 +1,57 @@
+<?php
+namespace TYPO3\CMS\Backend\Form\FormDataGroup;
+
+/*
+ * 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\Backend\Form\FormDataGroupInterface;
+use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * A data provider group for inline relations. This one calculates a slim "parent"
+ * record with only the inline config.
+ */
+class InlineParentRecord implements FormDataGroupInterface
+{
+    /**
+     * Compile form data
+     *
+     * @param array $result Initialized result array
+     * @return array Result filled with data
+     * @throws \UnexpectedValueException
+     */
+    public function compile(array $result)
+    {
+        $dataProvider = $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['inlineParentRecord'];
+        $orderingService = GeneralUtility::makeInstance(DependencyOrderingService::class);
+        $orderedDataProvider = $orderingService->orderByDependencies($dataProvider, 'before', 'depends');
+
+        foreach ($orderedDataProvider as $providerClassName => $_) {
+            /** @var FormDataProviderInterface $provider */
+            $provider = GeneralUtility::makeInstance($providerClassName);
+
+            if (!$provider instanceof FormDataProviderInterface) {
+                throw new \UnexpectedValueException(
+                    'Data provider ' . $providerClassName . ' must implement FormDataProviderInterface',
+                    1444398947
+                );
+            }
+
+            $result = $provider->addData($result);
+        }
+
+        return $result;
+    }
+}
index 487d9d2..76a4ec6 100644 (file)
@@ -46,13 +46,6 @@ class InitializeProcessedTca implements FormDataProviderInterface
             );
         }
 
-        if (!isset($result['vanillaTableTca']['types'][$result['recordTypeValue']]['showitem'])) {
-            throw new \UnexpectedValueException(
-                'No showitem definition in TCA table ' . $result['tableName'] . ' for type ' . $result['recordTypeValue'],
-                1438614542
-            );
-        }
-
         /**
          * @todo: This does not work for "default" fields like "hidden", those don't have a type set - fix in bootstrap??
         foreach ($result['vanillaTableTca']['columns'] as $fieldName => $fieldConfig) {
index d53ce93..4a9e283 100644 (file)
@@ -32,11 +32,18 @@ class TcaColumnsProcessShowitem implements FormDataProviderInterface
     public function addData(array $result)
     {
         $recordTypeValue = $result['recordTypeValue'];
-        if (empty($result['processedTca']['types'][$recordTypeValue]['showitem'])
+
+        if (!isset($result['processedTca']['types'][$recordTypeValue]['showitem'])
             || !is_string($result['processedTca']['types'][$recordTypeValue]['showitem'])
-            || empty($result['processedTca']['columns'])
-            || !is_array($result['processedTca']['columns'])
         ) {
+            throw new \UnexpectedValueException(
+                'No or invalid showitem definition in TCA table ' . $result['tableName'] . ' for type ' . $recordTypeValue,
+                1438614542
+            );
+        }
+
+        if (empty($result['processedTca']['columns'])) {
+            // We are sure this is an array by InitializeProcessedTca data provider
             return $result;
         }
 
diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataGroup/InlineParentRecordTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataGroup/InlineParentRecordTest.php
new file mode 100644 (file)
index 0000000..e9cdc62
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+namespace TYPO3\CMS\Backend\Tests\Unit\Form\FormDataGroup;
+
+/*
+ * 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 Prophecy\Argument;
+use Prophecy\Prophecy\ObjectProphecy;
+use TYPO3\CMS\Backend\Form\FormDataProviderInterface;
+use TYPO3\CMS\Backend\Form\FormDataGroup\InlineParentRecord;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Service\DependencyOrderingService;
+use TYPO3\CMS\Core\Tests\UnitTestCase;
+
+/**
+ * Test case
+ */
+class InlineParentRecordTest extends UnitTestCase
+{
+    /**
+     * @var InlineParentRecord
+     */
+    protected $subject;
+
+    protected function setUp()
+    {
+        $this->subject = new InlineParentRecord();
+    }
+
+    /**
+     * @test
+     */
+    public function compileReturnsIncomingData()
+    {
+        /** @var DependencyOrderingService|ObjectProphecy $orderingServiceProphecy */
+        $orderingServiceProphecy = $this->prophesize(DependencyOrderingService::class);
+        GeneralUtility::addInstance(DependencyOrderingService::class, $orderingServiceProphecy->reveal());
+        $orderingServiceProphecy->orderByDependencies(Argument::cetera())->willReturnArgument(0);
+
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['inlineParentRecord'] = array();
+
+        $input = array('foo');
+
+        $this->assertEquals($input, $this->subject->compile($input));
+    }
+
+    /**
+     * @test
+     */
+    public function compileReturnsResultChangedByDataProvider()
+    {
+        /** @var DependencyOrderingService|ObjectProphecy $orderingServiceProphecy */
+        $orderingServiceProphecy = $this->prophesize(DependencyOrderingService::class);
+        GeneralUtility::addInstance(DependencyOrderingService::class, $orderingServiceProphecy->reveal());
+        $orderingServiceProphecy->orderByDependencies(Argument::cetera())->willReturnArgument(0);
+
+        /** @var FormDataProviderInterface|ObjectProphecy $formDataProviderProphecy */
+        $formDataProviderProphecy = $this->prophesize(FormDataProviderInterface::class);
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['inlineParentRecord'] = array(
+            FormDataProviderInterface::class => array(),
+        );
+        GeneralUtility::addInstance(FormDataProviderInterface::class, $formDataProviderProphecy->reveal());
+        $providerResult = array('foo');
+        $formDataProviderProphecy->addData(Argument::cetera())->shouldBeCalled()->willReturn($providerResult);
+
+        $this->assertEquals($providerResult, $this->subject->compile([]));
+    }
+
+    /**
+     * @test
+     */
+    public function compileThrowsExceptionIfDataProviderDoesNotImplementInterface()
+    {
+        /** @var DependencyOrderingService|ObjectProphecy $orderingServiceProphecy */
+        $orderingServiceProphecy = $this->prophesize(DependencyOrderingService::class);
+        GeneralUtility::addInstance(DependencyOrderingService::class, $orderingServiceProphecy->reveal());
+        $orderingServiceProphecy->orderByDependencies(Argument::cetera())->willReturnArgument(0);
+
+        /** @var FormDataProviderInterface|ObjectProphecy $formDataProviderProphecy */
+        $formDataProviderProphecy = $this->prophesize(\stdClass::class);
+        $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['formDataGroup']['inlineParentRecord'] = array(
+            \stdClass::class => array(),
+        );
+        GeneralUtility::addInstance(\stdClass::class, $formDataProviderProphecy->reveal());
+
+        $this->setExpectedException(\UnexpectedValueException::class, $this->anything(), 1444398947);
+
+        $this->subject->compile([]);
+    }
+}
index 5943af0..f5db524 100644 (file)
@@ -81,28 +81,6 @@ class InitializeProcessedTcaTest extends UnitTestCase
     /**
      * @test
      */
-    public function addDataThrowsExceptionIfTypesHasNoShowitem()
-    {
-        $input = [
-            'recordTypeValue' => 'aType',
-            'vanillaTableTca' => [
-                'columns' => [
-                    'aField' => [
-                        'type' => 'aType',
-                    ],
-                ],
-                'types' => [
-                    'aType' => [],
-                ],
-            ],
-        ];
-        $this->setExpectedException(\UnexpectedValueException::class, $this->anything(), 1438614542);
-        $this->subject->addData($input);
-    }
-
-    /**
-     * @test
-     */
     public function addDataThrowsExceptionIfTcaColumnsHasNoTypeSet()
     {
         $this->markTestIncomplete('skipped for now, this is not save');
index 201901e..bf54e3f 100644 (file)
@@ -35,6 +35,28 @@ class TcaColumnsProcessShowitemTest extends UnitTestCase
     /**
      * @test
      */
+    public function addDataThrowsExceptionIfTypesHasNoShowitem()
+    {
+        $input = [
+            'recordTypeValue' => 'aType',
+            'vanillaTableTca' => [
+                'columns' => [
+                    'aField' => [
+                        'type' => 'aType',
+                    ],
+                ],
+                'types' => [
+                    'aType' => [],
+                ],
+            ],
+        ];
+        $this->setExpectedException(\UnexpectedValueException::class, $this->anything(), 1438614542);
+        $this->subject->addData($input);
+    }
+
+    /**
+     * @test
+     */
     public function addDataRegistersColumnsFieldReferencedInShowitems()
     {
         $input = [
index 56852f7..91c655e 100644 (file)
@@ -623,6 +623,29 @@ return array(
                         ),
                     ),
                 ),
+                'inlineParentRecord' => array(
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TableTca::class => array(),
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class => array(
+                        'depends' => array(
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\TableTca::class,
+                        ),
+                    ),
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsRemoveUnused::class => array(
+                        'depends' => array(
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\InitializeProcessedTca::class,
+                        ),
+                    ),
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class => array(
+                        'depends' => array(
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaColumnsRemoveUnused::class,
+                        ),
+                    ),
+                    \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineConfiguration::class => array(
+                        'depends' => array(
+                            \TYPO3\CMS\Backend\Form\FormDataProvider\TcaInlineExpandCollapseState::class,
+                        ),
+                    ),
+                ),
             ),
         ),
     ),