Skip to content

Commit

Permalink
[TASK] Centralize Page Layout resolving
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
bmack committed Mar 23, 2024
1 parent 1ebf4eb commit bf6bf9b
Show file tree
Hide file tree
Showing 11 changed files with 299 additions and 233 deletions.
68 changes: 17 additions & 51 deletions typo3/sysext/backend/Classes/View/BackendLayoutView.php
Expand Up @@ -24,6 +24,7 @@
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;
Expand All @@ -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) {
Expand Down Expand Up @@ -146,35 +148,26 @@ protected function determinePageId(string $tableName, array $data): int|false
* 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];
Expand Down Expand Up @@ -316,31 +309,4 @@ public static function getDefaultColumnLayout(): string
}
';
}

/**
* 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;
}
}
Expand Up @@ -19,31 +19,23 @@

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
{
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
Expand All @@ -61,11 +53,7 @@ public function selectedCombinedIdentifierIsDetermined(false|string $expected, a
$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);
}

Expand Down
49 changes: 49 additions & 0 deletions 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;
}
}
134 changes: 134 additions & 0 deletions 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;
}
}
Expand Up @@ -26,9 +26,9 @@
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
Expand Down Expand Up @@ -122,7 +122,7 @@ public function initializeExpressionMatcherWithVariables(array $variables): void
// 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;
}

Expand Down
3 changes: 3 additions & 0 deletions typo3/sysext/core/Configuration/Services.yaml
Expand Up @@ -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'
Expand Down

0 comments on commit bf6bf9b

Please sign in to comment.