From 0e464ae4c3f8ce2989535568c24b58025da4499a Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Wed, 25 Mar 2020 18:00:48 +0100
Subject: [PATCH] [TASK] Streamline BackendLayout API class
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This change moves the ConditionMatching back to the BackendLayoutView.

The main goal is to make BackendLayout more of an entity class again,
whereas BackendLayoutView should be the process to build the BackendLayout
objects, and configuration and return them, and act more as a
BackendLayoutResolver - this could / should be renamed in the future.

This change thus reactivates runtime-caching for fetching
a backend layout configuration again.

In addition, the new method "getBackendLayoutForPage()" now
actually returns a "ready-to-use" Backend Layout instance,
which could also used for the Nested Content Grid in the future.

The previous "configurationArray" is now defined as "stucture".

A short-hand function "getUsedColumns()" is added.

Resolves: #90839
Releases: master
Change-Id: Ic6e65317a5faa6d70a181fc3f24b3716a1137c39
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63918
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Frank Nägler <frank.naegler@typo3.org>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Frank Nägler <frank.naegler@typo3.org>
Reviewed-by: Benni Mack <benni@typo3.org>
---
 .../Page/LocalizationController.php           |   8 +-
 .../Controller/PageLayoutController.php       |  25 +---
 .../View/BackendLayout/BackendLayout.php      |  94 ++++----------
 .../View/BackendLayout/Grid/GridColumn.php    |   7 +-
 .../Classes/View/BackendLayoutView.php        | 122 ++++++++++++++----
 5 files changed, 137 insertions(+), 119 deletions(-)

diff --git a/typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php b/typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php
index 574384b1d4d8..9b91cf1bc0dc 100644
--- a/typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php
+++ b/typo3/sysext/backend/Classes/Controller/Page/LocalizationController.php
@@ -260,15 +260,15 @@ class LocalizationController
     {
         $columns = [];
         $backendLayoutView = GeneralUtility::makeInstance(BackendLayoutView::class);
-        $backendLayouts = $backendLayoutView->getSelectedBackendLayout($pageId);
+        $backendLayout = $backendLayoutView->getBackendLayoutForPage($pageId);
 
-        foreach ($backendLayouts['__items'] as $backendLayout) {
-            $columns[(int)$backendLayout[1]] = $backendLayout[0];
+        foreach ($backendLayout->getUsedColumns() as $columnPos => $columnLabel) {
+            $columns[$columnPos] = $GLOBALS['LANG']->sL($columnLabel);
         }
 
         return [
             'columns' => $columns,
-            'columnList' => array_values($backendLayouts['__colPosList']),
+            'columnList' => array_values($backendLayout->getColumnPositionNumbers()),
         ];
     }
 }
diff --git a/typo3/sysext/backend/Classes/Controller/PageLayoutController.php b/typo3/sysext/backend/Classes/Controller/PageLayoutController.php
index 8049f783625b..62f1d6ba6bdb 100644
--- a/typo3/sysext/backend/Classes/Controller/PageLayoutController.php
+++ b/typo3/sysext/backend/Classes/Controller/PageLayoutController.php
@@ -546,9 +546,9 @@ class PageLayoutController
             ');
 
             // Find backend layout / columns
-            $backendLayout = $this->backendLayouts->getSelectedBackendLayout($this->id);
-            if (!empty($backendLayout['__colPosList'])) {
-                $this->colPosList = implode(',', $backendLayout['__colPosList']);
+            $backendLayout = $this->backendLayouts->getBackendLayoutForPage($this->id);
+            if (!empty($backendLayout->getColumnPositionNumbers())) {
+                $this->colPosList = implode(',', $backendLayout->getColumnPositionNumbers());
             }
             // Removing duplicates, if any
             $this->colPosList = array_unique(GeneralUtility::intExplode(',', $this->colPosList));
@@ -612,24 +612,7 @@ class PageLayoutController
         $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
 
         if (GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('fluidBasedPageModule')) {
-            $selectedCombinedIdentifier = $this->backendLayouts->getSelectedCombinedIdentifier($this->id);
-            // If no backend layout is selected, use default
-            if (empty($selectedCombinedIdentifier)) {
-                $selectedCombinedIdentifier = 'default';
-            }
-
-            $backendLayout = $this->backendLayouts->getDataProviderCollection()->getBackendLayout(
-                $selectedCombinedIdentifier,
-                $this->id
-            );
-
-            // If backend layout is not found available anymore, use default
-            if ($backendLayout === null) {
-                $backendLayout = $this->backendLayouts->getDataProviderCollection()->getBackendLayout(
-                    'default',
-                    $this->id
-                );
-            }
+            $backendLayout = $this->backendLayouts->getBackendLayoutForPage((int)$this->id);
 
             $configuration = $backendLayout->getDrawingConfiguration();
             $configuration->setPageId($this->id);
diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php b/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php
index e0c467149685..383264246c23 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php
@@ -19,10 +19,9 @@ use TYPO3\CMS\Backend\View\BackendLayout\Grid\Grid;
 use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumn;
 use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridRow;
 use TYPO3\CMS\Backend\View\BackendLayout\Grid\LanguageColumn;
+use TYPO3\CMS\Backend\View\BackendLayoutView;
 use TYPO3\CMS\Backend\View\Drawing\BackendLayoutRenderer;
 use TYPO3\CMS\Backend\View\Drawing\DrawingConfiguration;
-use TYPO3\CMS\Core\Localization\LanguageService;
-use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
 /**
@@ -56,9 +55,11 @@ class BackendLayout
     protected $configuration;
 
     /**
+     * The structured data of the configuration represented as array.
+     *
      * @var array
      */
-    protected $configurationArray;
+    protected $structure = [];
 
     /**
      * @var array
@@ -114,7 +115,8 @@ class BackendLayout
         $this->setIdentifier($identifier);
         $this->setTitle($title);
         if (is_array($configuration)) {
-            $this->setConfigurationArray($configuration);
+            $this->structure = $configuration;
+            $this->configuration = $configuration['config'] ?? '';
         } else {
             $this->setConfiguration($configuration);
         }
@@ -201,61 +203,24 @@ class BackendLayout
     }
 
     /**
-     * @param array $configurationArray
+     * @param string $configuration
      */
-    public function setConfigurationArray(array $configurationArray): void
+    public function setConfiguration($configuration)
     {
-        if (!isset($configurationArray['__colPosList'], $configurationArray['__items'])) {
-            // Backend layout configuration is unprocessed, process it now to extract counts and column item lists
-            $colPosList = [];
-            $items = [];
-            $rowIndex = 0;
-            foreach ($configurationArray['backend_layout.']['rows.'] as $row) {
-                $index = 0;
-                $colCount = 0;
-                $columns = [];
-                foreach ($row['columns.'] as $column) {
-                    if (!isset($column['colPos'])) {
-                        continue;
-                    }
-                    $colPos = $column['colPos'];
-                    $colPos = (int)$colPos;
-                    $colPosList[$colPos] = $colPos;
-                    $key = ($index + 1) . '.';
-                    $columns[$key] = $column;
-                    $items[$colPos] = [
-                        (string)$this->getLanguageService()->sL($column['name']),
-                        $colPos,
-                        $column['icon']
-                    ];
-                    $colCount += $column['colspan'] ? $column['colspan'] : 1;
-                    ++ $index;
-                }
-                ++ $rowIndex;
-            }
-
-            $configurationArray['__config'] = $configurationArray;
-            $configurationArray['__colPosList'] = $colPosList;
-            $configurationArray['__items'] = $items;
-        }
-        $this->configurationArray = $configurationArray;
+        $this->configuration = $configuration;
+        GeneralUtility::makeInstance(BackendLayoutView::class)->parseStructure($this);
     }
 
     /**
+     * Returns the columns registered for this layout as $key => $value pair where the key is the colPos
+     * and the value is the title.
+     * "1" => "Left" etc.
+     * Please note that the title can contain LLL references ready for translation.
      * @return array
      */
-    public function getConfigurationArray(): array
+    public function getUsedColumns(): array
     {
-        return $this->configurationArray;
-    }
-
-    /**
-     * @param string $configuration
-     */
-    public function setConfiguration($configuration)
-    {
-        $this->configuration = $configuration;
-        $this->parseConfigurationStringAndSetConfigurationArray($configuration);
+        return $this->structure['usedColumns'] ?? [];
     }
 
     /**
@@ -274,6 +239,16 @@ class BackendLayout
         $this->data = $data;
     }
 
+    public function setStructure(array $structure)
+    {
+        $this->structure = $structure;
+    }
+
+    public function getStructure(): array
+    {
+        return $this->structure;
+    }
+
     /**
      * @return LanguageColumn[]
      */
@@ -295,7 +270,7 @@ class BackendLayout
     public function getGrid(): Grid
     {
         $grid = GeneralUtility::makeInstance(Grid::class, $this);
-        foreach ($this->getConfigurationArray()['__config']['backend_layout.']['rows.'] as $row) {
+        foreach ($this->structure['__config']['backend_layout.']['rows.'] ?? [] as $row) {
             $rowObject = GeneralUtility::makeInstance(GridRow::class, $this);
             foreach ($row['columns.'] as $column) {
                 $columnObject = GeneralUtility::makeInstance(GridColumn::class, $this, $column);
@@ -313,7 +288,7 @@ class BackendLayout
 
     public function getColumnPositionNumbers(): array
     {
-        return $this->getConfigurationArray()['__colPosList'];
+        return $this->structure['__colPosList'];
     }
 
     public function getContentFetcher(): ContentFetcher
@@ -349,22 +324,9 @@ class BackendLayout
         return $translationData['mode'] ?? '';
     }
 
-    protected function parseConfigurationStringAndSetConfigurationArray(string $configuration): void
-    {
-        $parser = GeneralUtility::makeInstance(TypoScriptParser::class);
-        $conditionMatcher = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher::class);
-        $parser->parse(TypoScriptParser::checkIncludeLines($configuration), $conditionMatcher);
-        $this->setConfigurationArray($parser->setup);
-    }
-
     public function __clone()
     {
         $this->drawingConfiguration = clone $this->drawingConfiguration;
         $this->contentFetcher->setBackendLayout($this);
     }
-
-    protected function getLanguageService(): LanguageService
-    {
-        return $GLOBALS['LANG'];
-    }
 }
diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumn.php b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumn.php
index f9f6687df914..374627c085c1 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumn.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayout/Grid/GridColumn.php
@@ -188,10 +188,9 @@ class GridColumn extends AbstractGridObject
     {
         $columnNumber = $this->getColumnNumber();
         $colTitle = (string)BackendUtility::getProcessedValue('tt_content', 'colPos', $columnNumber);
-        $tcaItems = $this->backendLayout->getConfigurationArray()['__items'];
-        foreach ($tcaItems as $item) {
-            if ($item[1] === $columnNumber) {
-                $colTitle = (string)$this->getLanguageService()->sL($item[0]);
+        foreach ($this->backendLayout->getUsedColumns() as $colPos => $title) {
+            if ($colPos === $columnNumber) {
+                $colTitle = (string)$this->getLanguageService()->sL($title);
             }
         }
         return $colTitle;
diff --git a/typo3/sysext/backend/Classes/View/BackendLayoutView.php b/typo3/sysext/backend/Classes/View/BackendLayoutView.php
index b238d77725b0..3895d9457ab7 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayoutView.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayoutView.php
@@ -14,8 +14,15 @@ namespace TYPO3\CMS\Backend\View;
  * The TYPO3 project - inspiring people to share!
  */
 
+use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
 use TYPO3\CMS\Backend\Utility\BackendUtility;
+use TYPO3\CMS\Backend\View\BackendLayout\BackendLayout;
+use TYPO3\CMS\Backend\View\BackendLayout\DataProviderCollection;
+use TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext;
+use TYPO3\CMS\Backend\View\BackendLayout\DefaultDataProvider;
 use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Localization\LanguageService;
+use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
 use TYPO3\CMS\Core\Utility\ArrayUtility;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 
@@ -26,7 +33,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class BackendLayoutView implements \TYPO3\CMS\Core\SingletonInterface
 {
     /**
-     * @var BackendLayout\DataProviderCollection
+     * @var DataProviderCollection
      */
     protected $dataProviderCollection;
 
@@ -53,15 +60,8 @@ class BackendLayoutView implements \TYPO3\CMS\Core\SingletonInterface
      */
     protected function initializeDataProviderCollection()
     {
-        /** @var BackendLayout\DataProviderCollection $dataProviderCollection */
-        $dataProviderCollection = GeneralUtility::makeInstance(
-            BackendLayout\DataProviderCollection::class
-        );
-
-        $dataProviderCollection->add(
-            'default',
-            \TYPO3\CMS\Backend\View\BackendLayout\DefaultDataProvider::class
-        );
+        $dataProviderCollection = GeneralUtility::makeInstance(DataProviderCollection::class);
+        $dataProviderCollection->add('default', DefaultDataProvider::class);
 
         if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'])) {
             $dataProviders = (array)$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'];
@@ -74,15 +74,15 @@ class BackendLayoutView implements \TYPO3\CMS\Core\SingletonInterface
     }
 
     /**
-     * @param BackendLayout\DataProviderCollection $dataProviderCollection
+     * @param DataProviderCollection $dataProviderCollection
      */
-    public function setDataProviderCollection(BackendLayout\DataProviderCollection $dataProviderCollection)
+    public function setDataProviderCollection(DataProviderCollection $dataProviderCollection)
     {
         $this->dataProviderCollection = $dataProviderCollection;
     }
 
     /**
-     * @return BackendLayout\DataProviderCollection
+     * @return DataProviderCollection
      */
     public function getDataProviderCollection()
     {
@@ -327,32 +327,89 @@ class BackendLayoutView implements \TYPO3\CMS\Core\SingletonInterface
     }
 
     /**
-     * Gets the selected backend layout
+     * Gets the selected backend layout structure as an array
      *
      * @param int $pageId
      * @return array|null $backendLayout
      */
-    public function getSelectedBackendLayout($pageId)
+    public function getSelectedBackendLayout($pageId): ?array
+    {
+        $layout = $this->getBackendLayoutForPage((int)$pageId);
+        if ($layout instanceof BackendLayout) {
+            return $layout->getStructure();
+        }
+        return null;
+    }
+
+    /**
+     * Get the BackendLayout object and parse the structure based on the UserTSconfig
+     * @param int $pageId
+     * @return BackendLayout
+     */
+    public function getBackendLayoutForPage(int $pageId): ?BackendLayout
     {
         if (isset($this->selectedBackendLayout[$pageId])) {
             return $this->selectedBackendLayout[$pageId];
         }
-        $backendLayoutData = null;
-
         $selectedCombinedIdentifier = $this->getSelectedCombinedIdentifier($pageId);
         // If no backend layout is selected, use default
         if (empty($selectedCombinedIdentifier)) {
             $selectedCombinedIdentifier = 'default';
         }
-
         $backendLayout = $this->getDataProviderCollection()->getBackendLayout($selectedCombinedIdentifier, $pageId);
         // If backend layout is not found available anymore, use default
         if ($backendLayout === null) {
-            $selectedCombinedIdentifier = 'default';
-            $backendLayout = $this->getDataProviderCollection()->getBackendLayout($selectedCombinedIdentifier, $pageId);
+            $backendLayout = $this->getDataProviderCollection()->getBackendLayout('default', $pageId);
+        }
+
+        $structure = null;
+        if ($backendLayout instanceof BackendLayout) {
+            $structure = $this->parseStructure($backendLayout);
+            // Parse the configuration and inject it back in the backend layout object
+            $backendLayout->setStructure($structure);
+            $this->selectedBackendLayout[$pageId] = $backendLayout;
         }
+        return $backendLayout;
+    }
 
-        return $backendLayout->getConfigurationArray();
+    /**
+     * @param BackendLayout $backendLayout
+     * @return array
+     * @internal
+     */
+    public function parseStructure(BackendLayout $backendLayout): array
+    {
+        $parser = GeneralUtility::makeInstance(TypoScriptParser::class);
+        $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
+        $parser->parse(TypoScriptParser::checkIncludeLines($backendLayout->getConfiguration()), $conditionMatcher);
+
+        $backendLayoutData = [];
+        $backendLayoutData['config'] = $backendLayout->getConfiguration();
+        $backendLayoutData['__config'] = $parser->setup;
+        $backendLayoutData['__items'] = [];
+        $backendLayoutData['__colPosList'] = [];
+        $backendLayoutData['usedColumns'] = [];
+
+        // create items and colPosList
+        if (!empty($backendLayoutData['__config']['backend_layout.']['rows.'])) {
+            foreach ($backendLayoutData['__config']['backend_layout.']['rows.'] as $row) {
+                if (!empty($row['columns.'])) {
+                    foreach ($row['columns.'] as $column) {
+                        if (!isset($column['colPos'])) {
+                            continue;
+                        }
+                        $backendLayoutData['__items'][] = [
+                            $this->getColumnName($column),
+                            $column['colPos'],
+                            null
+                        ];
+                        $backendLayoutData['__colPosList'][] = $column['colPos'];
+                        $backendLayoutData['usedColumns'][(int)$column['colPos']] = $column['name'];
+                    }
+                }
+            }
+        }
+        return $backendLayoutData;
     }
 
     /**
@@ -421,18 +478,35 @@ class BackendLayoutView implements \TYPO3\CMS\Core\SingletonInterface
     }
 
     /**
-     * @return BackendLayout\DataProviderContext
+     * @return DataProviderContext
      */
     protected function createDataProviderContext()
     {
-        return GeneralUtility::makeInstance(BackendLayout\DataProviderContext::class);
+        return GeneralUtility::makeInstance(DataProviderContext::class);
     }
 
     /**
-     * @return \TYPO3\CMS\Core\Localization\LanguageService
+     * @return LanguageService
      */
     protected function getLanguageService()
     {
         return $GLOBALS['LANG'];
     }
+
+    /**
+     * Get column name from colPos item structure
+     *
+     * @param array $column
+     * @return string
+     */
+    protected function getColumnName($column)
+    {
+        $columnName = $column['name'];
+
+        if (GeneralUtility::isFirstPartOfStr($columnName, 'LLL:')) {
+            $columnName = $this->getLanguageService()->sL($columnName);
+        }
+
+        return $columnName;
+    }
 }
-- 
GitLab