From bf6bf9b0fbfcb4ac3c95887d80923a344402ca06 Mon Sep 17 00:00:00 2001
From: Benni Mack <benni@typo3.org>
Date: Fri, 22 Mar 2024 10:08:05 +0100
Subject: [PATCH] [TASK] Centralize Page Layout resolving
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This change centralizes Frontend's "PageLayoutResolver",
used in TypoScript, and BackendLayoutView logic
to find the used page layout, while also modelling
more towards an object within PageLayout which can be
used at a later stage in FE to retrieve more information.

At the same time, some BackendLayoutView code is reduced
now.

Resolves: #103466
Releases: main
Change-Id: I716fe7313894aac92e5519a6b725feefff908270
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/83567
Tested-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Nikita Hovratov <nikita.h@live.de>
Reviewed-by: Benni Mack <benni@typo3.org>
Tested-by: Nikita Hovratov <nikita.h@live.de>
Tested-by: Benni Mack <benni@typo3.org>
Tested-by: core-ci <typo3@b13.com>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
---
 .../Classes/View/BackendLayoutView.php        |  68 +++------
 .../Functional/View/BackendLayoutViewTest.php |  18 +--
 typo3/sysext/core/Classes/Page/PageLayout.php |  49 +++++++
 .../core/Classes/Page/PageLayoutResolver.php  | 134 ++++++++++++++++++
 .../IncludeTreeConditionMatcherVisitor.php    |   4 +-
 typo3/sysext/core/Configuration/Services.yaml |   3 +
 .../Page/PageLayoutResolverTest.php           |  87 ++++++++++++
 .../ContentObject/ContentObjectRenderer.php   |   4 +-
 .../Classes/Page/PageLayoutResolver.php       |  73 ----------
 .../Unit/Page/PageLayoutResolverTest.php      |  87 ------------
 .../Tests/Functional/WebhookExecutionTest.php |   5 +-
 11 files changed, 299 insertions(+), 233 deletions(-)
 create mode 100644 typo3/sysext/core/Classes/Page/PageLayout.php
 create mode 100644 typo3/sysext/core/Classes/Page/PageLayoutResolver.php
 create mode 100644 typo3/sysext/core/Tests/Functional/Page/PageLayoutResolverTest.php
 delete mode 100644 typo3/sysext/frontend/Classes/Page/PageLayoutResolver.php
 delete mode 100644 typo3/sysext/frontend/Tests/Unit/Page/PageLayoutResolverTest.php

diff --git a/typo3/sysext/backend/Classes/View/BackendLayoutView.php b/typo3/sysext/backend/Classes/View/BackendLayoutView.php
index c65a8d470572..3ede3dfb0f30 100644
--- a/typo3/sysext/backend/Classes/View/BackendLayoutView.php
+++ b/typo3/sysext/backend/Classes/View/BackendLayoutView.php
@@ -24,6 +24,7 @@ use TYPO3\CMS\Backend\View\BackendLayout\DataProviderContext;
 use TYPO3\CMS\Backend\View\BackendLayout\DefaultDataProvider;
 use TYPO3\CMS\Core\Database\Connection;
 use TYPO3\CMS\Core\Database\ConnectionPool;
+use TYPO3\CMS\Core\Page\PageLayoutResolver;
 use TYPO3\CMS\Core\SingletonInterface;
 use TYPO3\CMS\Core\TypoScript\TypoScriptStringFactory;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
@@ -43,6 +44,7 @@ class BackendLayoutView implements SingletonInterface
     public function __construct(
         private readonly DataProviderCollection $dataProviderCollection,
         private readonly TypoScriptStringFactory $typoScriptStringFactory,
+        private readonly PageLayoutResolver $pageLayoutResolver,
     ) {
         $this->dataProviderCollection->add('default', DefaultDataProvider::class);
         foreach ((array)($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] ?? []) as $identifier => $className) {
@@ -146,35 +148,26 @@ class BackendLayoutView implements SingletonInterface
      * Returns the backend layout which should be used for this page.
      *
      * @return false|string Identifier of the backend layout to be used, or FALSE if none
+     * @internal only public for testing purposes
      */
-    protected function getSelectedCombinedIdentifier(int $pageId): string|false
+    public 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 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 = BackendUtility::BEgetRootLine($pageId, '', true);
+            // Use first element as current page,
+            $page = reset($rootLine);
+            // and remove last element (root page / pid=0)
+            array_pop($rootLine);
+            $selectedLayout = $this->pageLayoutResolver->getLayoutIdentifierForPage($page, $rootLine);
+            if ($selectedLayout === 'none') {
                 // 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 = BackendUtility::BEgetRootLine($pageId, '', true);
-                // Remove first and last element (current and root page)
-                array_shift($rootLine);
-                array_pop($rootLine);
-                foreach ($rootLine as $rootLinePage) {
-                    $this->selectedCombinedIdentifier[$pageId] = (string)$rootLinePage['backend_layout_next_level'];
-                    if ($this->selectedCombinedIdentifier[$pageId] === '-1') {
-                        // If layout for "next level" is set to "none" - don't use any and stop searching
-                        $this->selectedCombinedIdentifier[$pageId] = false;
-                        break;
-                    }
-                    if ($this->selectedCombinedIdentifier[$pageId] !== '' && $this->selectedCombinedIdentifier[$pageId] !== '0') {
-                        // Stop searching if a layout for "next level" is set
-                        break;
-                    }
-                }
+                $selectedLayout = false;
+            } elseif ($selectedLayout === 'default') {
+                $selectedLayout = '0';
             }
+            $this->selectedCombinedIdentifier[$pageId] = $selectedLayout;
         }
         // If it is set to a positive value use this
         return $this->selectedCombinedIdentifier[$pageId];
@@ -316,31 +309,4 @@ class BackendLayoutView implements SingletonInterface
 		}
 		';
     }
-
-    /**
-     * Gets a page record.
-     */
-    protected function getPage(int $pageId): ?array
-    {
-        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
-            ->getQueryBuilderForTable('pages');
-        $queryBuilder->getRestrictions()
-            ->removeAll();
-        $page = $queryBuilder
-            ->select('uid', 'pid', 'backend_layout')
-            ->from('pages')
-            ->where(
-                $queryBuilder->expr()->eq(
-                    'uid',
-                    $queryBuilder->createNamedParameter($pageId, Connection::PARAM_INT)
-                )
-            )
-            ->executeQuery()
-            ->fetchAssociative();
-        if (is_array($page)) {
-            BackendUtility::workspaceOL('pages', $page);
-        }
-
-        return is_array($page) ? $page : null;
-    }
 }
diff --git a/typo3/sysext/backend/Tests/Functional/View/BackendLayoutViewTest.php b/typo3/sysext/backend/Tests/Functional/View/BackendLayoutViewTest.php
index a5904c937724..55b00a3b02a6 100644
--- a/typo3/sysext/backend/Tests/Functional/View/BackendLayoutViewTest.php
+++ b/typo3/sysext/backend/Tests/Functional/View/BackendLayoutViewTest.php
@@ -19,11 +19,9 @@ 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\Functional\FunctionalTestCase;
 
 final class BackendLayoutViewTest extends FunctionalTestCase
@@ -31,19 +29,13 @@ final class BackendLayoutViewTest extends FunctionalTestCase
     private const RUNTIME_CACHE_ENTRY = 'backendUtilityBeGetRootLine';
 
     private FrontendInterface $runtimeCache;
-    private BackendLayoutView&MockObject&AccessibleObjectInterface $backendLayoutView;
+    private BackendLayoutView $subject;
 
     protected function setUp(): void
     {
         parent::setUp();
         $this->runtimeCache = $this->get(CacheManager::class)->getCache('runtime');
-        $this->backendLayoutView = $this->getAccessibleMock(
-            BackendLayoutView::class,
-            ['getPage'],
-            [],
-            '',
-            false
-        );
+        $this->subject = $this->get(BackendLayoutView::class);
     }
 
     protected function tearDown(): void
@@ -61,11 +53,7 @@ final class BackendLayoutViewTest extends FunctionalTestCase
             $this->mockRootLine((int)$pageId, $rootLine);
         }
 
-        $this->backendLayoutView->expects(self::once())
-            ->method('getPage')->with(self::equalTo($pageId))
-            ->willReturn($page);
-
-        $selectedCombinedIdentifier = $this->backendLayoutView->_call('getSelectedCombinedIdentifier', $pageId);
+        $selectedCombinedIdentifier = $this->subject->getSelectedCombinedIdentifier($pageId);
         self::assertEquals($expected, $selectedCombinedIdentifier);
     }
 
diff --git a/typo3/sysext/core/Classes/Page/PageLayout.php b/typo3/sysext/core/Classes/Page/PageLayout.php
new file mode 100644
index 000000000000..61b49254a034
--- /dev/null
+++ b/typo3/sysext/core/Classes/Page/PageLayout.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Core\Page;
+
+/**
+ * Contains information about the layout of a page,
+ * mainly which content areas (colPos=0, colPos=1, ...) are used and filled.
+ *
+ * @internal This is not part of TYPO3 Core API.
+ */
+class PageLayout
+{
+    public function __construct(
+        protected string $identifier,
+        protected string $title,
+        protected array $contentAreas,
+        protected array $fullConfiguration
+    ) {}
+
+    public function getIdentifier(): string
+    {
+        return $this->identifier;
+    }
+
+    public function getTitle(): string
+    {
+        return $this->title;
+    }
+
+    public function getContentAreas(): array
+    {
+        return $this->contentAreas;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Page/PageLayoutResolver.php b/typo3/sysext/core/Classes/Page/PageLayoutResolver.php
new file mode 100644
index 000000000000..df7c330eb4a3
--- /dev/null
+++ b/typo3/sysext/core/Classes/Page/PageLayoutResolver.php
@@ -0,0 +1,134 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Core\Page;
+
+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\Site\SiteFinder;
+use TYPO3\CMS\Core\TypoScript\PageTsConfigFactory;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+
+/**
+ * Finds the proper layout for a page, using the database fields "backend_layout"
+ * and "backend_layout_next_level".
+ *
+ * The most crucial part is that "backend_layout" is only applied for the CURRENT level,
+ * whereas backend_layout_next_level.
+ *
+ * Used in TypoScript as "getData:pagelayout".
+ *
+ * Currently, there is a hard dependency on EXT:backend however, all DataProvider logic should be migrated
+ * towards EXT:core.
+ *
+ * @internal This is not part of TYPO3 Core API.
+ */
+class PageLayoutResolver
+{
+    public function __construct(
+        protected readonly DataProviderCollection $dataProviderCollection,
+        protected readonly SiteFinder $siteFinder,
+        protected readonly PageTsConfigFactory $pageTsConfigFactory
+    ) {
+        $this->dataProviderCollection->add('default', DefaultDataProvider::class);
+        foreach ((array)($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['BackendLayoutDataProvider'] ?? []) as $identifier => $className) {
+            $this->dataProviderCollection->add($identifier, $className);
+        }
+    }
+
+    public function getLayoutForPage(array $pageRecord, array $rootLine): ?PageLayout
+    {
+        $pageId = (int)$pageRecord['uid'];
+        $site = $this->siteFinder->getSiteByPageId($pageId, $rootLine);
+        $pageTsConfig = $this->pageTsConfigFactory->create($rootLine, $site);
+
+        $dataProviderContext = GeneralUtility::makeInstance(DataProviderContext::class);
+        $dataProviderContext
+            ->setPageId($pageId)
+            ->setData($pageRecord)
+            ->setTableName('pages')
+            ->setFieldName('backend_layout')
+            ->setPageTsConfig($pageTsConfig->getPageTsConfigArray());
+
+        $selectedPageLayout = $this->getLayoutIdentifierForPage($pageRecord, $rootLine);
+        $layout = $this->dataProviderCollection->getBackendLayout($selectedPageLayout, $pageId);
+
+        if ($layout === null) {
+            return null;
+        }
+
+        $fullStructure = $layout->getStructure()['__config'];
+        $contentAreas = [];
+        // find all arrays recursively from , where one of the columns within the array is called "colPos"
+        $findColPos = function (array $structure) use (&$findColPos, &$contentAreas) {
+            if (isset($structure['colPos'])) {
+                unset($structure['colspan'], $structure['rowspan']);
+                $contentAreas[] = $structure;
+            }
+            foreach ($structure as $value) {
+                if (is_array($value)) {
+                    $findColPos($value);
+                }
+            }
+        };
+        $findColPos($fullStructure);
+
+        return new PageLayout($layout->getIdentifier(), $layout->getTitle(), $contentAreas, $layout->getStructure());
+    }
+
+    /**
+     * Check if the current page has a value in the DB field "backend_layout"
+     * if empty, check the root line for "backend_layout_next_level"
+     * Same as TypoScript:
+     *   field = backend_layout
+     *   ifEmpty.data = levelfield:-2, backend_layout_next_level, slide
+     *   ifEmpty.ifEmpty = default
+     */
+    public function getLayoutIdentifierForPage(array $page, array $rootLine): string
+    {
+        $selectedLayout = $page['backend_layout'] ?? '';
+
+        // If it is set to "none" - don't use any
+        if ($selectedLayout === '-1') {
+            return 'none';
+        }
+
+        if ($selectedLayout === '' || $selectedLayout === '0') {
+            // If it not set check the root-line for a layout on next level and use this
+            // Remove first element, which is the current page
+            // See also \TYPO3\CMS\Backend\View\BackendLayoutView::getSelectedCombinedIdentifier()
+            array_shift($rootLine);
+            foreach ($rootLine as $rootLinePage) {
+                $selectedLayout = (string)($rootLinePage['backend_layout_next_level'] ?? '');
+                // If layout for "next level" is set to "none" - don't use any and stop searching
+                if ($selectedLayout === '-1') {
+                    $selectedLayout = 'none';
+                    break;
+                }
+                if ($selectedLayout !== '' && $selectedLayout !== '0') {
+                    // Stop searching if a layout for "next level" is set
+                    break;
+                }
+            }
+        }
+        if ($selectedLayout === '0' || $selectedLayout === '') {
+            $selectedLayout = 'default';
+        }
+        return $selectedLayout;
+    }
+}
diff --git a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php
index ebf959c667b4..f41520121c7a 100644
--- a/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php
+++ b/typo3/sysext/core/Classes/TypoScript/IncludeTree/Visitor/IncludeTreeConditionMatcherVisitor.php
@@ -26,9 +26,9 @@ use TYPO3\CMS\Core\Context\UserAspect;
 use TYPO3\CMS\Core\Context\WorkspaceAspect;
 use TYPO3\CMS\Core\ExpressionLanguage\RequestWrapper;
 use TYPO3\CMS\Core\ExpressionLanguage\Resolver;
+use TYPO3\CMS\Core\Page\PageLayoutResolver;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeConditionInterface;
 use TYPO3\CMS\Core\TypoScript\IncludeTree\IncludeNode\IncludeInterface;
-use TYPO3\CMS\Frontend\Page\PageLayoutResolver;
 
 /**
  * A visitor that looks at IncludeConditionInterface nodes and
@@ -122,7 +122,7 @@ final class IncludeTreeConditionMatcherVisitor implements IncludeTreeVisitorInte
             // the 'nearest' parent. However, here it is always passed sorted, so it is a top-down rootLine. Hence, this needs to be once
             // again reversed at this point.
             $bottomUpFullRootLine = array_reverse($fullRootLine);
-            $tree->pagelayout = $this->pageLayoutResolver->getLayoutForPage($variables['page'], $bottomUpFullRootLine);
+            $tree->pagelayout = $this->pageLayoutResolver->getLayoutIdentifierForPage($variables['page'], $bottomUpFullRootLine);
             $enrichedVariables['tree'] = $tree;
         }
 
diff --git a/typo3/sysext/core/Configuration/Services.yaml b/typo3/sysext/core/Configuration/Services.yaml
index 7d38fbae5174..138be5a33462 100644
--- a/typo3/sysext/core/Configuration/Services.yaml
+++ b/typo3/sysext/core/Configuration/Services.yaml
@@ -213,6 +213,9 @@ services:
   TYPO3\CMS\Core\Locking\ResourceMutex:
     public: true
 
+  TYPO3\CMS\Core\Page\PageLayoutResolver:
+    public: true
+
   TYPO3\CMS\Core\Page\PageRenderer:
     arguments:
       $assetsCache: '@cache.assets'
diff --git a/typo3/sysext/core/Tests/Functional/Page/PageLayoutResolverTest.php b/typo3/sysext/core/Tests/Functional/Page/PageLayoutResolverTest.php
new file mode 100644
index 000000000000..11021c298aa9
--- /dev/null
+++ b/typo3/sysext/core/Tests/Functional/Page/PageLayoutResolverTest.php
@@ -0,0 +1,87 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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!
+ */
+
+namespace TYPO3\CMS\Core\Tests\Functional\Page;
+
+use PHPUnit\Framework\Attributes\Test;
+use TYPO3\CMS\Core\Page\PageLayoutResolver;
+use TYPO3\TestingFramework\Core\Functional\FunctionalTestCase;
+
+final class PageLayoutResolverTest extends FunctionalTestCase
+{
+    #[Test]
+    public function getLayoutIdentifierForPageFetchesSelectedPageDirectly(): void
+    {
+        $subject = $this->get(PageLayoutResolver::class);
+        $result = $subject->getLayoutIdentifierForPage(['backend_layout' => '1'], ['does-not-matter']);
+        self::assertEquals('1', $result);
+    }
+
+    #[Test]
+    public function getLayoutIdentifierForPageTreatsSpecialMinusOneValueAsNone(): void
+    {
+        $subject = $this->get(PageLayoutResolver::class);
+        $result = $subject->getLayoutIdentifierForPage(['backend_layout' => '-1'], ['does-not-matter']);
+        self::assertEquals('none', $result);
+    }
+
+    #[Test]
+    public function getLayoutIdentifierForPageTreatsSpecialValueZeroOrEmptyAsDefaultWithEmptyRootLine(): void
+    {
+        $subject = $this->get(PageLayoutResolver::class);
+        $parentPages = [['backend_layout' => '']];
+        $page = ['backend_layout' => '0', 'uid' => 123];
+        $result = $subject->getLayoutIdentifierForPage($page, array_merge([$page], $parentPages));
+        self::assertEquals('default', $result);
+        $page = ['backend_layout' => '', 'uid' => 123];
+        $result = $subject->getLayoutIdentifierForPage($page, array_merge([$page], $parentPages));
+        self::assertEquals('default', $result);
+    }
+
+    #[Test]
+    public function getLayoutIdentifierForPageTreatsSpecialValueZeroOrEmptyAsDefaultWhenNothingGivenInRootLine(): void
+    {
+        $subject = $this->get(PageLayoutResolver::class);
+        // No layout specified for current page
+        $page = ['backend_layout' => '', 'uid' => 123];
+        $parentPages = [['uid' => 13, 'backend_layout' => 'does-not-matter'], ['uid' => 1, 'backend_layout_next_level' => '0']];
+        $result = $subject->getLayoutIdentifierForPage($page, array_merge([$page], $parentPages));
+        self::assertEquals('default', $result);
+    }
+
+    #[Test]
+    public function getLayoutIdentifierForPageFetchesRootLinePagesUpUntilSomethingWasFound(): void
+    {
+        $subject = $this->get(PageLayoutResolver::class);
+        // No layout specified for current page
+        $page = ['backend_layout' => '', 'uid' => 123];
+        $parentPages = [['uid' => 13, 'backend_layout' => 'does-not-matter', 'backend_layout_next_level' => ''], ['uid' => 1, 'backend_layout_next_level' => 'regular']];
+        $result = $subject->getLayoutIdentifierForPage($page, array_merge([$page], $parentPages));
+        self::assertEquals('regular', $result);
+    }
+
+    #[Test]
+    public function getLayoutIdentifierForPageFetchesRootLinePagesUpWhenNoneWasSelectedExplicitly(): void
+    {
+        $subject = $this->get(PageLayoutResolver::class);
+        // No layout specified for current page
+        $page = ['backend_layout' => '', 'uid' => 123];
+        $parentPages = [['uid' => 13, 'backend_layout' => 'does-not-matter'], ['uid' => 15, 'backend_layout_next_level' => '-1'], ['uid' => 1, 'backend_layout_next_level' => 'regular']];
+        $result = $subject->getLayoutIdentifierForPage($page, array_merge([$page], $parentPages));
+        self::assertEquals('none', $result);
+    }
+}
diff --git a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
index 19b4dd829a1a..a854c8de238b 100644
--- a/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
+++ b/typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
@@ -47,6 +47,7 @@ use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
 use TYPO3\CMS\Core\Localization\Locales;
 use TYPO3\CMS\Core\Log\LogManager;
 use TYPO3\CMS\Core\Page\DefaultJavaScriptAssetTrait;
+use TYPO3\CMS\Core\Page\PageLayoutResolver;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\Resource\Exception;
 use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
@@ -82,7 +83,6 @@ use TYPO3\CMS\Frontend\ContentObject\Exception\ExceptionHandlerInterface;
 use TYPO3\CMS\Frontend\ContentObject\Exception\ProductionExceptionHandler;
 use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
 use TYPO3\CMS\Frontend\Imaging\GifBuilder;
-use TYPO3\CMS\Frontend\Page\PageLayoutResolver;
 use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
 use TYPO3\CMS\Frontend\Typolink\LinkFactory;
 use TYPO3\CMS\Frontend\Typolink\LinkResult;
@@ -3901,7 +3901,7 @@ class ContentObjectRenderer implements LoggerAwareInterface
                     case 'pagelayout':
                         $pageInformation = $this->getRequest()->getAttribute('frontend.page.information');
                         $pageLayoutResolver = GeneralUtility::makeInstance(PageLayoutResolver::class);
-                        $retVal = $pageLayoutResolver->getLayoutForPage($pageInformation->getPageRecord(), $pageInformation->getRootLine());
+                        $retVal = $pageLayoutResolver->getLayoutIdentifierForPage($pageInformation->getPageRecord(), $pageInformation->getRootLine());
                         break;
                     case 'current':
                         $retVal = $this->data[$this->currentValKey] ?? null;
diff --git a/typo3/sysext/frontend/Classes/Page/PageLayoutResolver.php b/typo3/sysext/frontend/Classes/Page/PageLayoutResolver.php
deleted file mode 100644
index 2d1165300165..000000000000
--- a/typo3/sysext/frontend/Classes/Page/PageLayoutResolver.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * 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!
- */
-
-namespace TYPO3\CMS\Frontend\Page;
-
-/**
- * Finds the proper layout for a page, using the database fields "backend_layout"
- * and "backend_layout_next_level".
- *
- * The most crucial part is that "backend_layout" is only applied for the CURRENT level,
- * whereas backend_layout_next_level.
- *
- * Used in TypoScript as "getData: pagelayout".
- *
- * @internal as this might get moved to EXT:core if usages in TYPO3 Backend are helpful as well.
- */
-class PageLayoutResolver
-{
-    /**
-     * Check if the current page has a value in the DB field "backend_layout"
-     * if empty, check the root line for "backend_layout_next_level"
-     * Same as TypoScript:
-     *   field = backend_layout
-     *   ifEmpty.data = levelfield:-2, backend_layout_next_level, slide
-     *   ifEmpty.ifEmpty = default
-     */
-    public function getLayoutForPage(array $page, array $rootLine): string
-    {
-        $selectedLayout = $page['backend_layout'] ?? '';
-
-        // If it is set to "none" - don't use any
-        if ($selectedLayout === '-1') {
-            return 'none';
-        }
-
-        if ($selectedLayout === '' || $selectedLayout === '0') {
-            // If it not set check the root-line for a layout on next level and use this
-            // Remove first element, which is the current page
-            // See also \TYPO3\CMS\Backend\View\BackendLayoutView::getSelectedCombinedIdentifier()
-            array_shift($rootLine);
-            foreach ($rootLine as $rootLinePage) {
-                $selectedLayout = (string)($rootLinePage['backend_layout_next_level'] ?? '');
-                // If layout for "next level" is set to "none" - don't use any and stop searching
-                if ($selectedLayout === '-1') {
-                    $selectedLayout = 'none';
-                    break;
-                }
-                if ($selectedLayout !== '' && $selectedLayout !== '0') {
-                    // Stop searching if a layout for "next level" is set
-                    break;
-                }
-            }
-        }
-        if ($selectedLayout === '0' || $selectedLayout === '') {
-            $selectedLayout = 'default';
-        }
-        return $selectedLayout;
-    }
-}
diff --git a/typo3/sysext/frontend/Tests/Unit/Page/PageLayoutResolverTest.php b/typo3/sysext/frontend/Tests/Unit/Page/PageLayoutResolverTest.php
deleted file mode 100644
index b34d24079d85..000000000000
--- a/typo3/sysext/frontend/Tests/Unit/Page/PageLayoutResolverTest.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-
-declare(strict_types=1);
-
-/*
- * 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!
- */
-
-namespace TYPO3\CMS\Frontend\Tests\Unit\Page;
-
-use PHPUnit\Framework\Attributes\Test;
-use TYPO3\CMS\Frontend\Page\PageLayoutResolver;
-use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
-
-final class PageLayoutResolverTest extends UnitTestCase
-{
-    #[Test]
-    public function getLayoutForPageFetchesSelectedPageDirectly(): void
-    {
-        $subject = new PageLayoutResolver();
-        $result = $subject->getLayoutForPage(['backend_layout' => '1'], ['does-not-matter']);
-        self::assertEquals('1', $result);
-    }
-
-    #[Test]
-    public function getLayoutForPageTreatsSpecialMinusOneValueAsNone(): void
-    {
-        $subject = new PageLayoutResolver();
-        $result = $subject->getLayoutForPage(['backend_layout' => '-1'], ['does-not-matter']);
-        self::assertEquals('none', $result);
-    }
-
-    #[Test]
-    public function getLayoutForPageTreatsSpecialValueZeroOrEmptyAsDefaultWithEmptyRootLine(): void
-    {
-        $subject = new PageLayoutResolver();
-        $parentPages = [['backend_layout' => '']];
-        $page = ['backend_layout' => '0'];
-        $result = $subject->getLayoutForPage($page, array_merge([$page], $parentPages));
-        self::assertEquals('default', $result);
-        $page = ['backend_layout' => ''];
-        $result = $subject->getLayoutForPage($page, array_merge([$page], $parentPages));
-        self::assertEquals('default', $result);
-    }
-
-    #[Test]
-    public function getLayoutForPageTreatsSpecialValueZeroOrEmptyAsDefaultWhenNothingGivenInRootLine(): void
-    {
-        $subject = new PageLayoutResolver();
-        // No layout specified for current page
-        $page = ['backend_layout' => ''];
-        $parentPages = [['uid' => 13, 'backend_layout' => 'does-not-matter'], ['uid' => 1, 'backend_layout_next_level' => '0']];
-        $result = $subject->getLayoutForPage($page, array_merge([$page], $parentPages));
-        self::assertEquals('default', $result);
-    }
-
-    #[Test]
-    public function getLayoutForPageFetchesRootLinePagesUpUntilSomethingWasFound(): void
-    {
-        $subject = new PageLayoutResolver();
-        // No layout specified for current page
-        $page = ['backend_layout' => ''];
-        $parentPages = [['uid' => 13, 'backend_layout' => 'does-not-matter', 'backend_layout_next_level' => ''], ['uid' => 1, 'backend_layout_next_level' => 'regular']];
-        $result = $subject->getLayoutForPage($page, array_merge([$page], $parentPages));
-        self::assertEquals('regular', $result);
-    }
-
-    #[Test]
-    public function getLayoutForPageFetchesRootLinePagesUpWhenNoneWasSelectedExplicitly(): void
-    {
-        $subject = new PageLayoutResolver();
-        // No layout specified for current page
-        $page = ['backend_layout' => ''];
-        $parentPages = [['uid' => 13, 'backend_layout' => 'does-not-matter'], ['uid' => 15, 'backend_layout_next_level' => '-1'], ['uid' => 1, 'backend_layout_next_level' => 'regular']];
-        $result = $subject->getLayoutForPage($page, array_merge([$page], $parentPages));
-        self::assertEquals('none', $result);
-    }
-}
diff --git a/typo3/sysext/webhooks/Tests/Functional/WebhookExecutionTest.php b/typo3/sysext/webhooks/Tests/Functional/WebhookExecutionTest.php
index 15adafdcad98..4ec5841cd36f 100644
--- a/typo3/sysext/webhooks/Tests/Functional/WebhookExecutionTest.php
+++ b/typo3/sysext/webhooks/Tests/Functional/WebhookExecutionTest.php
@@ -57,9 +57,6 @@ final class WebhookExecutionTest extends FunctionalTestCase
         $this->importCSVDataSet(__DIR__ . '/Fixtures/pages.csv');
         $this->importCSVDataSet(__DIR__ . '/Fixtures/sys_webhooks.csv');
 
-        $backendUser = $this->setUpBackendUser(1);
-        $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser);
-
         $this->writeSiteConfiguration(
             'testing',
             $this->buildSiteConfiguration(1, '/'),
@@ -67,6 +64,8 @@ final class WebhookExecutionTest extends FunctionalTestCase
                 $this->buildDefaultLanguageConfiguration('EN', '/'),
             ]
         );
+        $backendUser = $this->setUpBackendUser(1);
+        $GLOBALS['LANG'] = $this->get(LanguageServiceFactory::class)->createFromUserPreferences($backendUser);
     }
 
     private function registerRequestInspector(callable $inspector): void
-- 
GitLab