From 5ac249b5ef001ca5f87905f47855c2a068ec4b0a Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Fri, 8 Mar 2024 22:42:22 +0100
Subject: [PATCH] [TASK] Streamline Backend Layout View code

This change adapts some places around Backend Layouts,
which is a pre-patch in order to centralize
previously used code from 2013 to move towards a more
generic concept, which needs to go into EXT:core
as the structure should also be useful and evaluated
in EXT:frontend.

This change now cleans up places which are non-breaking
but hardens PHP code without changing the underlying
logic.

Resolves: #103365
Releases: main
Change-Id: I77382d93342e5c2e45966f96bf485619c79f25f3
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83362
Reviewed-by: Oliver Bartsch <bo@cedev.de>
Tested-by: Andreas Kienast <a.fernandez@scripting-base.de>
Tested-by: Oliver Bartsch <bo@cedev.de>
Reviewed-by: Andreas Kienast <a.fernandez@scripting-base.de>
Tested-by: core-ci <typo3@b13.com>
---
 .../View/BackendLayout/BackendLayout.php      | 116 ++++------------
 .../BackendLayout/DataProviderCollection.php  |  34 ++---
 .../BackendLayout/DataProviderContext.php     |  79 +++--------
 .../BackendLayout/DefaultDataProvider.php     |  34 ++---
 .../PageTsBackendLayoutDataProvider.php       |  22 +--
 .../Classes/View/BackendLayoutView.php        | 130 ++++++------------
 .../View/BackendLayoutViewTest.php            |  44 ++++--
 7 files changed, 136 insertions(+), 323 deletions(-)
 rename typo3/sysext/backend/Tests/{Unit => Functional}/View/BackendLayoutViewTest.php (88%)

diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php b/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php
index dd499041c870..cc85112ac211 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayout/BackendLayout.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 /*
  * This file is part of the TYPO3 CMS project.
  *
@@ -23,50 +25,19 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
  */
 class BackendLayout
 {
-    /**
-     * @var string
-     */
-    protected $identifier;
-
-    /**
-     * @var string
-     */
-    protected $title;
-
-    /**
-     * @var string
-     */
-    protected $description;
-
-    /**
-     * @var string
-     */
-    protected $iconPath;
-
-    /**
-     * @var string
-     */
-    protected $configuration;
+    protected string $identifier;
+    protected string $title;
+    protected string $description = '';
+    protected string $iconPath = '';
+    protected string $configuration = '';
 
     /**
      * The structured data of the configuration represented as array.
-     *
-     * @var array
      */
-    protected $structure = [];
+    protected array $structure = [];
+    protected array $data = [];
 
-    /**
-     * @var array
-     */
-    protected $data;
-
-    /**
-     * @param string $identifier
-     * @param string $title
-     * @param string|array $configuration
-     * @return BackendLayout
-     */
-    public static function create($identifier, $title, $configuration)
+    public static function create(string $identifier, string $title, string|array $configuration): BackendLayout
     {
         return GeneralUtility::makeInstance(
             static::class,
@@ -76,12 +47,7 @@ class BackendLayout
         );
     }
 
-    /**
-     * @param string $identifier
-     * @param string $title
-     * @param string|array $configuration
-     */
-    public function __construct($identifier, $title, $configuration)
+    public function __construct(string $identifier, string $title, string|array $configuration)
     {
         $this->setIdentifier($identifier);
         $this->setTitle($title);
@@ -93,19 +59,12 @@ class BackendLayout
         }
     }
 
-    /**
-     * @return string
-     */
-    public function getIdentifier()
+    public function getIdentifier(): string
     {
         return $this->identifier;
     }
 
-    /**
-     * @param string $identifier
-     * @throws \UnexpectedValueException
-     */
-    public function setIdentifier($identifier)
+    public function setIdentifier(string $identifier): void
     {
         if (str_contains($identifier, '__')) {
             throw new \UnexpectedValueException(
@@ -117,66 +76,42 @@ class BackendLayout
         $this->identifier = $identifier;
     }
 
-    /**
-     * @return string
-     */
-    public function getTitle()
+    public function getTitle(): string
     {
         return $this->title;
     }
 
-    /**
-     * @param string $title
-     */
-    public function setTitle($title)
+    public function setTitle(string $title): void
     {
         $this->title = $title;
     }
 
-    /**
-     * @return string
-     */
-    public function getDescription()
+    public function getDescription(): string
     {
         return $this->description;
     }
 
-    /**
-     * @param string $description
-     */
-    public function setDescription($description)
+    public function setDescription(string $description): void
     {
         $this->description = $description;
     }
 
-    /**
-     * @return string
-     */
-    public function getIconPath()
+    public function getIconPath(): string
     {
         return $this->iconPath;
     }
 
-    /**
-     * @param string $iconPath
-     */
-    public function setIconPath($iconPath)
+    public function setIconPath(string $iconPath): void
     {
         $this->iconPath = $iconPath;
     }
 
-    /**
-     * @return string
-     */
-    public function getConfiguration()
+    public function getConfiguration(): string
     {
         return $this->configuration;
     }
 
-    /**
-     * @param string $configuration
-     */
-    public function setConfiguration($configuration)
+    public function setConfiguration(string $configuration): void
     {
         $this->configuration = $configuration;
         $this->structure = GeneralUtility::makeInstance(BackendLayoutView::class)->parseStructure($this);
@@ -203,20 +138,17 @@ class BackendLayout
         return $this->structure['rowCount'] ?? 0;
     }
 
-    /**
-     * @return array
-     */
-    public function getData()
+    public function getData(): array
     {
         return $this->data;
     }
 
-    public function setData(array $data)
+    public function setData(array $data): void
     {
         $this->data = $data;
     }
 
-    public function setStructure(array $structure)
+    public function setStructure(array $structure): void
     {
         $this->structure = $structure;
     }
diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/DataProviderCollection.php b/typo3/sysext/backend/Classes/View/BackendLayout/DataProviderCollection.php
index 141a5673722e..5bcb0465a14d 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayout/DataProviderCollection.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayout/DataProviderCollection.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 /*
  * This file is part of the TYPO3 CMS project.
  *
@@ -24,24 +26,15 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class DataProviderCollection implements SingletonInterface
 {
     /**
-     * @var array|DataProviderInterface[]
-     */
-    protected $dataProviders = [];
-
-    /**
-     * @var array
+     * @var DataProviderInterface[]
      */
-    protected $results = [];
+    protected array $dataProviders = [];
+    protected array $results = [];
 
     /**
      * Adds a data provider to this collection.
-     *
-     * @param string $identifier
-     * @param string|object $classNameOrObject
-     * @throws \UnexpectedValueException
-     * @throws \LogicException
      */
-    public function add($identifier, $classNameOrObject)
+    public function add(string $identifier, string|object $classNameOrObject): void
     {
         if (str_contains($identifier, '__')) {
             throw new \UnexpectedValueException(
@@ -73,9 +66,9 @@ class DataProviderCollection implements SingletonInterface
      * backend layouts. Each data provider returns its own
      * backend layout collection.
      *
-     * @return array|BackendLayoutCollection[]
+     * @return BackendLayoutCollection[]
      */
-    public function getBackendLayoutCollections(DataProviderContext $dataProviderContext)
+    public function getBackendLayoutCollections(DataProviderContext $dataProviderContext): array
     {
         $result = [];
 
@@ -93,12 +86,8 @@ class DataProviderCollection implements SingletonInterface
      * e.g. "myextension_regular" and "myextension" is the identifier
      * of the accordant data provider and "regular" the identifier of
      * the accordant backend layout.
-     *
-     * @param string $combinedIdentifier
-     * @param int $pageId
-     * @return BackendLayout|null
      */
-    public function getBackendLayout($combinedIdentifier, $pageId)
+    public function getBackendLayout(string $combinedIdentifier, int $pageId): ?BackendLayout
     {
         $backendLayout = null;
 
@@ -118,11 +107,8 @@ class DataProviderCollection implements SingletonInterface
 
     /**
      * Creates a new backend layout collection.
-     *
-     * @param string $identifier
-     * @return BackendLayoutCollection
      */
-    protected function createBackendLayoutCollection($identifier)
+    protected function createBackendLayoutCollection(string $identifier): BackendLayoutCollection
     {
         return GeneralUtility::makeInstance(
             BackendLayoutCollection::class,
diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/DataProviderContext.php b/typo3/sysext/backend/Classes/View/BackendLayout/DataProviderContext.php
index 8d09efaf8568..2b21457c7747 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayout/DataProviderContext.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayout/DataProviderContext.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 /*
  * This file is part of the TYPO3 CMS project.
  *
@@ -22,97 +24,51 @@ use TYPO3\CMS\Core\SingletonInterface;
  */
 class DataProviderContext implements SingletonInterface
 {
-    /**
-     * @var int
-     */
-    protected $pageId;
-
-    /**
-     * @var string
-     */
-    protected $tableName;
-
-    /**
-     * @var string
-     */
-    protected $fieldName;
-
-    /**
-     * @var array
-     */
-    protected $data;
-
-    /**
-     * @var array
-     */
-    protected $pageTsConfig;
+    protected int $pageId = 0;
+    protected string $tableName = '';
+    protected string $fieldName = '';
+    protected array $data = [];
+    protected array $pageTsConfig = [];
 
-    /**
-     * @return int
-     */
-    public function getPageId()
+    public function getPageId(): int
     {
         return $this->pageId;
     }
 
-    /**
-     * @param int $pageId
-     * @return DataProviderContext
-     */
-    public function setPageId($pageId)
+    public function setPageId(int $pageId): self
     {
         $this->pageId = $pageId;
         return $this;
     }
 
-    /**
-     * @return string
-     */
-    public function getTableName()
+    public function getTableName(): string
     {
         return $this->tableName;
     }
 
-    /**
-     * @param string $tableName
-     * @return DataProviderContext
-     */
-    public function setTableName($tableName)
+    public function setTableName(string $tableName): self
     {
         $this->tableName = $tableName;
         return $this;
     }
 
-    /**
-     * @return string
-     */
-    public function getFieldName()
+    public function getFieldName(): string
     {
         return $this->fieldName;
     }
 
-    /**
-     * @param string $fieldName
-     * @return DataProviderContext
-     */
-    public function setFieldName($fieldName)
+    public function setFieldName(string $fieldName): self
     {
         $this->fieldName = $fieldName;
         return $this;
     }
 
-    /**
-     * @return array
-     */
-    public function getData()
+    public function getData(): array
     {
         return $this->data;
     }
 
-    /**
-     * @return DataProviderContext
-     */
-    public function setData(array $data)
+    public function setData(array $data): self
     {
         $this->data = $data;
         return $this;
@@ -123,10 +79,7 @@ class DataProviderContext implements SingletonInterface
         return $this->pageTsConfig;
     }
 
-    /**
-     * @return DataProviderContext
-     */
-    public function setPageTsConfig(array $pageTsConfig)
+    public function setPageTsConfig(array $pageTsConfig): self
     {
         $this->pageTsConfig = $pageTsConfig;
         return $this;
diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/DefaultDataProvider.php b/typo3/sysext/backend/Classes/View/BackendLayout/DefaultDataProvider.php
index 03a2b3001caa..1b183499475d 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayout/DefaultDataProvider.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayout/DefaultDataProvider.php
@@ -1,5 +1,7 @@
 <?php
 
+declare(strict_types=1);
+
 /*
  * This file is part of the TYPO3 CMS project.
  *
@@ -31,10 +33,9 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
 class DefaultDataProvider implements DataProviderInterface
 {
     /**
-     * @var string
      * Table name for backend_layouts
      */
-    protected $tableName = 'backend_layout';
+    protected string $tableName = 'backend_layout';
 
     /**
      * Adds backend layouts to the given backend layout collection.
@@ -44,7 +45,7 @@ class DefaultDataProvider implements DataProviderInterface
     public function addBackendLayouts(
         DataProviderContext $dataProviderContext,
         BackendLayoutCollection $backendLayoutCollection
-    ) {
+    ): void {
         $layoutData = $this->getLayoutData(
             $dataProviderContext->getFieldName(),
             $dataProviderContext->getPageTsConfig(),
@@ -62,9 +63,8 @@ class DefaultDataProvider implements DataProviderInterface
      *
      * @param string|int $identifier
      * @param int $pageId
-     * @return BackendLayout|null
      */
-    public function getBackendLayout($identifier, $pageId)
+    public function getBackendLayout($identifier, $pageId): ?BackendLayout
     {
         $backendLayout = null;
 
@@ -83,10 +83,8 @@ class DefaultDataProvider implements DataProviderInterface
 
     /**
      * Creates a backend layout with the default configuration.
-     *
-     * @return BackendLayout
      */
-    protected function createDefaultBackendLayout()
+    protected function createDefaultBackendLayout(): BackendLayout
     {
         return BackendLayout::create(
             'default',
@@ -97,12 +95,10 @@ class DefaultDataProvider implements DataProviderInterface
 
     /**
      * Creates a new backend layout using the given record data.
-     *
-     * @return BackendLayout
      */
-    protected function createBackendLayout(array $data)
+    protected function createBackendLayout(array $data): BackendLayout
     {
-        $backendLayout = BackendLayout::create($data['uid'], $data['title'], $data['config']);
+        $backendLayout = BackendLayout::create((string)$data['uid'], $data['title'], $data['config']);
         $backendLayout->setIconPath($this->getIconPath($data));
         $backendLayout->setData($data);
         return $backendLayout;
@@ -110,10 +106,8 @@ class DefaultDataProvider implements DataProviderInterface
 
     /**
      * Resolves the icon from the database record
-     *
-     * @return string
      */
-    protected function getIconPath(array $icon)
+    protected function getIconPath(array $icon): string
     {
         $fileRepository = GeneralUtility::makeInstance(FileRepository::class);
         $references = $fileRepository->findByRelation($this->tableName, 'icon', (int)$icon['uid']);
@@ -132,7 +126,7 @@ class DefaultDataProvider implements DataProviderInterface
      * @param int $pageUid the ID of the page wea re getting the layouts for
      * @return array $layouts A collection of layout data of the registered provider
      */
-    protected function getLayoutData($fieldName, array $pageTsConfig, $pageUid)
+    protected function getLayoutData(string $fieldName, array $pageTsConfig, int $pageUid): array
     {
         $storagePid = $this->getStoragePid($pageTsConfig);
         $pageTsConfigId = $this->getPageTSconfigIds($pageTsConfig);
@@ -207,10 +201,8 @@ class DefaultDataProvider implements DataProviderInterface
 
     /**
      * Returns the storage PID from TCEFORM.
-     *
-     * @return int
      */
-    protected function getStoragePid(array $pageTsConfig)
+    protected function getStoragePid(array $pageTsConfig): int
     {
         $storagePid = 0;
 
@@ -223,10 +215,8 @@ class DefaultDataProvider implements DataProviderInterface
 
     /**
      * Returns the page TSconfig from TCEFORM.
-     *
-     * @return array
      */
-    protected function getPageTSconfigIds(array $pageTsConfig)
+    protected function getPageTSconfigIds(array $pageTsConfig): array
     {
         $pageTsConfigIds = [
             'backend_layout' => 0,
diff --git a/typo3/sysext/backend/Classes/View/BackendLayout/PageTsBackendLayoutDataProvider.php b/typo3/sysext/backend/Classes/View/BackendLayout/PageTsBackendLayoutDataProvider.php
index d89ae0c0fb87..390443b81bfb 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayout/PageTsBackendLayoutDataProvider.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayout/PageTsBackendLayoutDataProvider.php
@@ -130,7 +130,7 @@ final class PageTsBackendLayoutDataProvider implements DataProviderInterface
     private function generateBackendLayoutFromTsConfig(string $identifier, array $data): ?array
     {
         $backendLayout = [];
-        if (!empty($data['config.']['backend_layout.']) && is_array($data['config.']['backend_layout.'])) {
+        if (is_array($data['config.']['backend_layout.'] ?? null)) {
             $backendLayout['uid'] = substr($identifier, 0, -1);
             $backendLayout['title'] = $data['title'] ?? $backendLayout['uid'];
             $backendLayout['icon'] = $data['icon'] ?? '';
@@ -148,7 +148,7 @@ final class PageTsBackendLayoutDataProvider implements DataProviderInterface
     /**
      * Attach Backend Layout to internal Stack
      */
-    private function attachBackendLayout(mixed $backendLayout = null)
+    private function attachBackendLayout(mixed $backendLayout = null): void
     {
         if ($backendLayout) {
             $this->backendLayouts[$backendLayout['uid']] = $backendLayout;
@@ -160,23 +160,9 @@ final class PageTsBackendLayoutDataProvider implements DataProviderInterface
      */
     private function createBackendLayout(array $data): BackendLayout
     {
-        $backendLayout = BackendLayout::create($data['uid'], $data['title'], $data['config']);
-        $backendLayout->setIconPath($this->getIconPath($data['icon']));
+        $backendLayout = BackendLayout::create((string)$data['uid'], $data['title'], $data['config']);
+        $backendLayout->setIconPath($data['icon'] ?? '');
         $backendLayout->setData($data);
         return $backendLayout;
     }
-
-    /**
-     * Gets and sanitizes the icon path.
-     *
-     * @param string $icon Name of the icon file
-     */
-    private function getIconPath(string $icon): string
-    {
-        $iconPath = '';
-        if (!empty($icon)) {
-            $iconPath = $icon;
-        }
-        return $iconPath;
-    }
 }
diff --git a/typo3/sysext/backend/Classes/View/BackendLayoutView.php b/typo3/sysext/backend/Classes/View/BackendLayoutView.php
index 9211bbf2a2e3..23a8759376aa 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayoutView.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayoutView.php
@@ -47,11 +47,8 @@ class BackendLayoutView implements SingletonInterface
         private readonly TypoScriptStringFactory $typoScriptStringFactory,
     ) {
         $this->dataProviderCollection->add('default', DefaultDataProvider::class);
-        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'])) {
-            $dataProviders = (array)$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'];
-            foreach ($dataProviders as $identifier => $className) {
-                $this->dataProviderCollection->add($identifier, $className);
-            }
+        foreach ((array)($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] ?? []) as $identifier => $className) {
+            $this->dataProviderCollection->add($identifier, $className);
         }
     }
 
@@ -70,10 +67,11 @@ class BackendLayoutView implements SingletonInterface
     public function addBackendLayoutItems(array &$parameters)
     {
         $pageId = $this->determinePageId($parameters['table'], $parameters['row']) ?: 0;
-        $pageTsConfig = (array)BackendUtility::getPagesTSconfig($pageId);
+        $pageTsConfig = BackendUtility::getPagesTSconfig($pageId);
         $identifiersToBeExcluded = $this->getIdentifiersToBeExcluded($pageTsConfig);
 
-        $dataProviderContext = $this->createDataProviderContext()
+        $dataProviderContext = GeneralUtility::makeInstance(DataProviderContext::class);
+        $dataProviderContext
             ->setPageId($pageId)
             ->setData($parameters['row'])
             ->setTableName($parameters['table'])
@@ -106,12 +104,11 @@ class BackendLayoutView implements SingletonInterface
     /**
      * Determines the page id for a given record of a database table.
      *
-     * @param string $tableName
      * @return int|false Returns page id or false on error
      */
-    protected function determinePageId($tableName, array $data)
+    protected function determinePageId(string $tableName, array $data): int|false
     {
-        if (empty($data)) {
+        if ($data === []) {
             return false;
         }
 
@@ -144,28 +141,26 @@ class BackendLayoutView implements SingletonInterface
             $pageId = $data['pid'];
         }
 
-        return $pageId;
+        return (int)$pageId;
     }
 
     /**
      * Returns the backend layout which should be used for this page.
      *
-     * @param int $pageId
-     * @return bool|string Identifier of the backend layout to be used, or FALSE if none
+     * @return false|string Identifier of the backend layout to be used, or FALSE if none
      */
-    public function getSelectedCombinedIdentifier($pageId)
+    protected function getSelectedCombinedIdentifier(int $pageId): string|false
     {
         if (!isset($this->selectedCombinedIdentifier[$pageId])) {
             $page = $this->getPage($pageId);
             $this->selectedCombinedIdentifier[$pageId] = (string)($page['backend_layout'] ?? null);
-
             if ($this->selectedCombinedIdentifier[$pageId] === '-1') {
                 // If it is set to "none" - don't use any
                 $this->selectedCombinedIdentifier[$pageId] = false;
             } elseif ($this->selectedCombinedIdentifier[$pageId] === '' || $this->selectedCombinedIdentifier[$pageId] === '0') {
                 // If it not set check the root-line for a layout on next level and use this
                 // (root-line starts with current page and has page "0" at the end)
-                $rootLine = $this->getRootLine($pageId);
+                $rootLine = BackendUtility::BEgetRootLine($pageId, '', true);
                 // Remove first and last element (current and root page)
                 array_shift($rootLine);
                 array_pop($rootLine);
@@ -189,10 +184,8 @@ class BackendLayoutView implements SingletonInterface
 
     /**
      * Gets backend layout identifiers to be excluded
-     *
-     * @return array
      */
-    protected function getIdentifiersToBeExcluded(array $pageTSconfig)
+    protected function getIdentifiersToBeExcluded(array $pageTSconfig): array
     {
         $identifiersToBeExcluded = [];
 
@@ -212,7 +205,7 @@ class BackendLayoutView implements SingletonInterface
      * This method is called as "itemsProcFunc" with the accordant context
      * for tt_content.colPos.
      */
-    public function colPosListItemProcFunc(array $parameters)
+    public function colPosListItemProcFunc(array &$parameters): void
     {
         $pageId = $this->determinePageId($parameters['table'], $parameters['row']);
 
@@ -223,12 +216,8 @@ class BackendLayoutView implements SingletonInterface
 
     /**
      * Adds items to a colpos list
-     *
-     * @param int $pageId
-     * @param array $items
-     * @return array
      */
-    protected function addColPosListLayoutItems($pageId, $items)
+    protected function addColPosListLayoutItems(int $pageId, array $items): array
     {
         $layout = $this->getSelectedBackendLayout($pageId);
         if ($layout && !empty($layout['__items'])) {
@@ -239,11 +228,9 @@ class BackendLayoutView implements SingletonInterface
 
     /**
      * Gets the list of available columns for a given page id
-     *
-     * @param int $id
-     * @return array $tcaItems
+     * @todo: will be removed once Page Position Map for content is removed.
      */
-    public function getColPosListItemsParsed($id)
+    public function getColPosListItemsParsed(int $id): array
     {
         $tsConfig = BackendUtility::getPagesTSconfig($id)['TCEFORM.']['tt_content.']['colPos.'] ?? [];
         $tcaConfig = $GLOBALS['TCA']['tt_content']['columns']['colPos']['config'] ?? [];
@@ -275,43 +262,36 @@ class BackendLayoutView implements SingletonInterface
      * @return array The updated $item array
      * @internal
      */
-    protected function addItems($items, $iArray)
+    protected function addItems(array $items, array $iArray): array
     {
         $languageService = $this->getLanguageService();
-        if (is_array($iArray)) {
-            foreach ($iArray as $value => $label) {
-                // if the label is an array (that means it is a subelement
-                // like "34.icon = mylabel.png", skip it (see its usage below)
-                if (is_array($label)) {
-                    continue;
-                }
-                // check if the value "34 = mylabel" also has a "34.icon = myimage.png"
-                if (isset($iArray[$value . '.']) && $iArray[$value . '.']['icon']) {
-                    $icon = $iArray[$value . '.']['icon'];
-                } else {
-                    $icon = '';
-                }
-                $items[] = [$languageService->sL($label), $value, $icon];
+        foreach ($iArray as $value => $label) {
+            // if the label is an array (that means it is a subelement
+            // like "34.icon = mylabel.png", skip it (see its usage below)
+            if (is_array($label)) {
+                continue;
+            }
+            // check if the value "34 = mylabel" also has a "34.icon = myimage.png"
+            if (isset($iArray[$value . '.']) && $iArray[$value . '.']['icon']) {
+                $icon = $iArray[$value . '.']['icon'];
+            } else {
+                $icon = '';
             }
+            $items[] = [$languageService->sL($label), $value, $icon];
         }
         return $items;
     }
 
     /**
      * Gets the selected backend layout structure as an array
-     *
-     * @param int $pageId
-     * @return array|null $backendLayout
      */
-    public function getSelectedBackendLayout($pageId)
+    public function getSelectedBackendLayout(int $pageId): ?array
     {
-        $layout = $this->getBackendLayoutForPage((int)$pageId);
-        return $layout?->getStructure();
+        return $this->getBackendLayoutForPage($pageId)?->getStructure();
     }
 
     /**
      * Get the BackendLayout object and parse the structure based on the UserTSconfig
-     * @return BackendLayout
      */
     public function getBackendLayoutForPage(int $pageId): ?BackendLayout
     {
@@ -329,7 +309,7 @@ class BackendLayoutView implements SingletonInterface
             $backendLayout = $this->dataProviderCollection->getBackendLayout('default', $pageId);
         }
 
-        if ($backendLayout instanceof BackendLayout) {
+        if ($backendLayout !== null) {
             $this->selectedBackendLayout[$pageId] = $backendLayout;
         }
         return $backendLayout;
@@ -376,12 +356,9 @@ class BackendLayoutView implements SingletonInterface
     }
 
     /**
-     * Get default columns layout
-     *
-     * @return string Default four column layout
-     * @static
+     * Get default columns layout (main column)
      */
-    public static function getDefaultColumnLayout()
+    public static function getDefaultColumnLayout(): string
     {
         return '
 		backend_layout {
@@ -403,11 +380,8 @@ class BackendLayoutView implements SingletonInterface
 
     /**
      * Gets a page record.
-     *
-     * @param int $pageId
-     * @return array|false|null
      */
-    protected function getPage($pageId)
+    protected function getPage(int $pageId): ?array
     {
         $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
             ->getQueryBuilderForTable('pages');
@@ -424,28 +398,11 @@ class BackendLayoutView implements SingletonInterface
             )
             ->executeQuery()
             ->fetchAssociative();
-        BackendUtility::workspaceOL('pages', $page);
-
-        return $page;
-    }
-
-    /**
-     * Gets the page root-line.
-     *
-     * @param int $pageId
-     * @return array
-     */
-    protected function getRootLine($pageId)
-    {
-        return BackendUtility::BEgetRootLine($pageId, '', true);
-    }
+        if (is_array($page)) {
+            BackendUtility::workspaceOL('pages', $page);
+        }
 
-    /**
-     * @return DataProviderContext
-     */
-    protected function createDataProviderContext()
-    {
-        return GeneralUtility::makeInstance(DataProviderContext::class);
+        return is_array($page) ? $page : null;
     }
 
     protected function getLanguageService(): LanguageService
@@ -455,14 +412,9 @@ class BackendLayoutView implements SingletonInterface
 
     /**
      * Get column name from colPos item structure
-     *
-     * @param array $column
-     * @return string
      */
-    protected function getColumnName($column)
+    protected function getColumnName(array $column): string
     {
-        $columnName = $column['name'];
-        $columnName = $this->getLanguageService()->sL($columnName);
-        return $columnName;
+        return $this->getLanguageService()->sL($column['name']);
     }
 }
diff --git a/typo3/sysext/backend/Tests/Unit/View/BackendLayoutViewTest.php b/typo3/sysext/backend/Tests/Functional/View/BackendLayoutViewTest.php
similarity index 88%
rename from typo3/sysext/backend/Tests/Unit/View/BackendLayoutViewTest.php
rename to typo3/sysext/backend/Tests/Functional/View/BackendLayoutViewTest.php
index 5e56d0c919c1..a5904c937724 100644
--- a/typo3/sysext/backend/Tests/Unit/View/BackendLayoutViewTest.php
+++ b/typo3/sysext/backend/Tests/Functional/View/BackendLayoutViewTest.php
@@ -15,49 +15,55 @@ declare(strict_types=1);
  * The TYPO3 project - inspiring people to share!
  */
 
-namespace TYPO3\CMS\Backend\Tests\Unit\View;
+namespace TYPO3\CMS\Backend\Tests\Functional\View;
 
 use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\Attributes\Test;
 use PHPUnit\Framework\MockObject\MockObject;
 use TYPO3\CMS\Backend\View\BackendLayoutView;
+use TYPO3\CMS\Core\Cache\CacheManager;
+use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
 use TYPO3\TestingFramework\Core\AccessibleObjectInterface;
-use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
 
-final class BackendLayoutViewTest extends UnitTestCase
+final class BackendLayoutViewTest extends FunctionalTestCase
 {
-    protected BackendLayoutView&MockObject&AccessibleObjectInterface $backendLayoutView;
+    private const RUNTIME_CACHE_ENTRY = 'backendUtilityBeGetRootLine';
+
+    private FrontendInterface $runtimeCache;
+    private BackendLayoutView&MockObject&AccessibleObjectInterface $backendLayoutView;
 
-    /**
-     * Sets up this test case.
-     */
     protected function setUp(): void
     {
         parent::setUp();
+        $this->runtimeCache = $this->get(CacheManager::class)->getCache('runtime');
         $this->backendLayoutView = $this->getAccessibleMock(
             BackendLayoutView::class,
-            ['getPage', 'getRootLine'],
+            ['getPage'],
             [],
             '',
             false
         );
     }
 
-    /**
-     * @param bool|string $expected
-     */
+    protected function tearDown(): void
+    {
+        $this->runtimeCache->remove(self::RUNTIME_CACHE_ENTRY);
+        parent::tearDown();
+    }
+
     #[DataProvider('selectedCombinedIdentifierIsDeterminedDataProvider')]
     #[Test]
-    public function selectedCombinedIdentifierIsDetermined($expected, array $page, array $rootLine): void
+    public function selectedCombinedIdentifierIsDetermined(false|string $expected, array $page, array $rootLine): void
     {
         $pageId = $page['uid'];
+        if ($pageId !== false) {
+            $this->mockRootLine((int)$pageId, $rootLine);
+        }
 
         $this->backendLayoutView->expects(self::once())
             ->method('getPage')->with(self::equalTo($pageId))
             ->willReturn($page);
-        $this->backendLayoutView
-            ->method('getRootLine')->with(self::equalTo($pageId))
-            ->willReturn($rootLine);
 
         $selectedCombinedIdentifier = $this->backendLayoutView->_call('getSelectedCombinedIdentifier', $pageId);
         self::assertEquals($expected, $selectedCombinedIdentifier);
@@ -201,4 +207,12 @@ final class BackendLayoutViewTest extends UnitTestCase
             ],
         ];
     }
+
+    private function mockRootLine(int $pageId, array $rootLine): void
+    {
+        $this->runtimeCache->set(self::RUNTIME_CACHE_ENTRY, [
+            $pageId . '--' => $rootLine, // plain, no overlay
+            $pageId . '--1' => $rootLine, // workspace overlay
+        ]);
+    }
 }
-- 
GitLab