Skip to content

Commit

Permalink
[!!!][FEATURE] Override any backend template with TsConfig
Browse files Browse the repository at this point in the history
This patch finishes the migration to the new backend
templating strategy. Affected are the backend page
module and the dashboard extension:

* Bigger refactoring of PageLayoutController
  towards more modern and streamlined code with
  much less class state.
* Transition of fillDefaultsByPackageName() in
  the page module for the "grid" layout towards
  TsConfig based template overrides.
* Refactoring of the dashboard main controller
  to avoid fillDefaultsByPackageName().

The new template override API based on TsConfig
is now documented with a feature ReST file. The
new template override strategy is breaking for
existing overrides based on FE TypoScript and
documented with a breaking ReST file including
a detailed migration path. The ext:dashboard
documentation is adapted accordingly.

As a result, all backend controllers that do not
rely on extbase no longer parse FE TypoScript.
This is especially useful for the central page
module: Clicking around on a v12 bootstrap_package
page tree in the page module without the patch leads
to around 80 to 120 milliseconds parsetime on my
machine - depending on the number and complexity
of content elements and the cache state. This
systematically shrinks to around 50 to 70 milliseconds
with the patch: The system no longer loads tons of
extbase related classes, dependency injection is more
effective, and the entire FE TypoScript parsing is gone.
With cold caches ("Flush all caches" backend button),
the difference is around 100 milliseconds for the
first call on my system - in general, the bigger the
instance, the higher the improvement.

Change-Id: Ia244495db592526633d01e7f504502e297bd2ef9
Resolves: #96812
Related: #96730
Related: #96614
Related: #90348
Releases: main
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/73366
Tested-by: core-ci <typo3@b13.com>
Tested-by: Stefan Bürk <stefan@buerk.tech>
Tested-by: Benni Mack <benni@typo3.org>
Reviewed-by: Stefan Bürk <stefan@buerk.tech>
Reviewed-by: Benni Mack <benni@typo3.org>
  • Loading branch information
lolli42 authored and bmack committed Feb 11, 2022
1 parent 3bf84fb commit ab6a457
Show file tree
Hide file tree
Showing 43 changed files with 1,393 additions and 1,462 deletions.
5 changes: 0 additions & 5 deletions Build/phpstan/phpstan-baseline.neon
Expand Up @@ -420,11 +420,6 @@ parameters:
count: 1
path: ../../typo3/sysext/backend/Classes/View/BackendLayout/Grid/Grid.php

-
message: "#^Call to an undefined method TYPO3Fluid\\\\Fluid\\\\Core\\\\Rendering\\\\RenderingContextInterface\\:\\:setRequest\\(\\)\\.$#"
count: 1
path: ../../typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php

-
message: "#^Left side of && is always true\\.$#"
count: 1
Expand Down
722 changes: 246 additions & 476 deletions typo3/sysext/backend/Classes/Controller/PageLayoutController.php

Large diffs are not rendered by default.

2 changes: 0 additions & 2 deletions typo3/sysext/backend/Classes/View/BackendViewFactory.php
Expand Up @@ -28,8 +28,6 @@

/**
* Creates a View for backend usage. This is a low level factory. Extensions typically use ModuleTemplate instead.
*
* @internal
*/
final class BackendViewFactory
{
Expand Down
184 changes: 90 additions & 94 deletions typo3/sysext/backend/Classes/View/Drawing/BackendLayoutRenderer.php
Expand Up @@ -17,6 +17,7 @@

namespace TYPO3\CMS\Backend\View\Drawing;

use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayout\ContentFetcher;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\Grid;
Expand All @@ -25,14 +26,13 @@
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridRow;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\LanguageColumn;
use TYPO3\CMS\Backend\View\BackendLayout\RecordRememberer;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Backend\View\PageLayoutContext;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Mvc\Request;
use TYPO3\CMS\Fluid\View\TemplateView;

/**
* Backend Layout Renderer
Expand All @@ -45,23 +45,14 @@
*/
class BackendLayoutRenderer
{
protected PageLayoutContext $context;
protected ContentFetcher $contentFetcher;
protected TemplateView $view;

public function __construct(PageLayoutContext $context)
{
$this->context = $context;
$this->contentFetcher = GeneralUtility::makeInstance(ContentFetcher::class, $context);
$this->view = GeneralUtility::makeInstance(TemplateView::class);
$this->view->getRenderingContext()->setRequest(GeneralUtility::makeInstance(Request::class));
$this->view->getRenderingContext()->getTemplatePaths()->fillDefaultsByPackageName('backend');
$this->view->getRenderingContext()->setControllerName('PageLayout');
$this->view->assign('context', $context);
public function __construct(
protected readonly BackendViewFactory $backendViewFactory,
) {
}

public function getGridForPageLayoutContext(PageLayoutContext $context): Grid
{
$contentFetcher = GeneralUtility::makeInstance(ContentFetcher::class, $context);
$grid = GeneralUtility::makeInstance(Grid::class, $context);
$recordRememberer = GeneralUtility::makeInstance(RecordRememberer::class);
if ($context->getDrawingConfiguration()->getLanguageMode()) {
Expand All @@ -77,7 +68,7 @@ public function getGridForPageLayoutContext(PageLayoutContext $context): Grid
$columnObject = GeneralUtility::makeInstance(GridColumn::class, $context, $column);
$rowObject->addColumn($columnObject);
if (isset($column['colPos'])) {
$records = $this->contentFetcher->getContentRecordsPerColumn((int)$column['colPos'], $languageId);
$records = $contentFetcher->getContentRecordsPerColumn((int)$column['colPos'], $languageId);
$recordRememberer->rememberRecords($records);
foreach ($records as $contentRecord) {
$columnItem = GeneralUtility::makeInstance(GridColumnItem::class, $context, $columnObject, $contentRecord);
Expand All @@ -90,11 +81,85 @@ public function getGridForPageLayoutContext(PageLayoutContext $context): Grid
return $grid;
}

/**
* @param bool $renderUnused If true, renders the bottom column with unused records
*/
public function drawContent(ServerRequestInterface $request, PageLayoutContext $pageLayoutContext, bool $renderUnused = true): string
{
$contentFetcher = GeneralUtility::makeInstance(ContentFetcher::class, $pageLayoutContext);

$view = $this->backendViewFactory->create($request, 'typo3/cms-backend');
$view->assignMultiple([
'context' => $pageLayoutContext,
'hideRestrictedColumns' => (bool)(BackendUtility::getPagesTSconfig($pageLayoutContext->getPageId())['mod.']['web_layout.']['hideRestrictedCols'] ?? false),
'newContentTitle' => $this->getLanguageService()->getLL('newContentElement'),
'newContentTitleShort' => $this->getLanguageService()->getLL('content'),
'allowEditContent' => $this->getBackendUser()->check('tables_modify', 'tt_content'),
]);

if ($pageLayoutContext->getDrawingConfiguration()->getLanguageMode()) {
if ($pageLayoutContext->getDrawingConfiguration()->getDefaultLanguageBinding()) {
$view->assign('languageColumns', $this->getLanguageColumnsWithDefLangBindingForPageLayoutContext($pageLayoutContext));
} else {
$view->assign('languageColumns', $this->getLanguageColumnsForPageLayoutContext($pageLayoutContext));
}
} else {
$context = $pageLayoutContext;
// Check if we have to use a localized context for grid creation
if ($pageLayoutContext->getDrawingConfiguration()->getSelectedLanguageId() > 0) {
// In case a localization is selected, clone the context with this language
$localizedContext = $pageLayoutContext->cloneForLanguage(
$pageLayoutContext->getSiteLanguage($pageLayoutContext->getDrawingConfiguration()->getSelectedLanguageId())
);
if ($localizedContext->getLocalizedPageRecord()) {
// In case the localized context contains the corresponding
// localized page record use this context for grid creation.
$context = $localizedContext;
}
}
$view->assign('grid', $this->getGridForPageLayoutContext($context));
}

$rendered = $view->render('PageLayout/PageLayout');
if ($renderUnused) {
$unusedRecords = $contentFetcher->getUnusedRecords();

if (!empty($unusedRecords)) {
$unusedElementsMessage = GeneralUtility::makeInstance(
FlashMessage::class,
$this->getLanguageService()->getLL('staleUnusedElementsWarning'),
$this->getLanguageService()->getLL('staleUnusedElementsWarningTitle'),
FlashMessage::WARNING
);
$service = GeneralUtility::makeInstance(FlashMessageService::class);
$queue = $service->getMessageQueueByIdentifier();
$queue->addMessage($unusedElementsMessage);

$unusedGrid = GeneralUtility::makeInstance(Grid::class, $pageLayoutContext);
$unusedRow = GeneralUtility::makeInstance(GridRow::class, $pageLayoutContext);
$unusedColumn = GeneralUtility::makeInstance(GridColumn::class, $pageLayoutContext, ['name' => 'unused']);

$unusedGrid->addRow($unusedRow);
$unusedRow->addColumn($unusedColumn);

foreach ($unusedRecords as $unusedRecord) {
$item = GeneralUtility::makeInstance(GridColumnItem::class, $pageLayoutContext, $unusedColumn, $unusedRecord);
$unusedColumn->addItem($item);
}

$view->assign('grid', $unusedGrid);
$rendered .= $view->render('PageLayout/UnusedRecords');
}
}
return $rendered;
}

/**
* @return LanguageColumn[]
*/
protected function getLanguageColumnsForPageLayoutContext(PageLayoutContext $context): iterable
{
$contentFetcher = GeneralUtility::makeInstance(ContentFetcher::class, $context);
$languageColumns = [];
foreach ($context->getLanguagesToShow() as $siteLanguage) {
$localizedLanguageId = $siteLanguage->getLanguageId();
Expand All @@ -109,8 +174,8 @@ protected function getLanguageColumnsForPageLayoutContext(PageLayoutContext $con
} else {
$localizedContext = $context;
}
$translationInfo = $this->contentFetcher->getTranslationData(
$this->contentFetcher->getFlatContentRecords($localizedLanguageId),
$translationInfo = $contentFetcher->getTranslationData(
$contentFetcher->getFlatContentRecords($localizedLanguageId),
$localizedContext->getSiteLanguage()->getLanguageId()
);
$languageColumnObject = GeneralUtility::makeInstance(
Expand All @@ -126,11 +191,12 @@ protected function getLanguageColumnsForPageLayoutContext(PageLayoutContext $con

protected function getLanguageColumnsWithDefLangBindingForPageLayoutContext(PageLayoutContext $context): iterable
{
$contentFetcher = GeneralUtility::makeInstance(ContentFetcher::class, $context);
$languageColumns = [];

// default language
$translationInfo = $this->contentFetcher->getTranslationData(
$this->contentFetcher->getFlatContentRecords(0),
$translationInfo = $contentFetcher->getTranslationData(
$contentFetcher->getFlatContentRecords(0),
0
);

Expand All @@ -151,12 +217,12 @@ protected function getLanguageColumnsWithDefLangBindingForPageLayoutContext(Page
continue;
}

$translationInfo = $this->contentFetcher->getTranslationData(
$this->contentFetcher->getFlatContentRecords($localizedLanguageId),
$translationInfo = $contentFetcher->getTranslationData(
$contentFetcher->getFlatContentRecords($localizedLanguageId),
$localizedContext->getSiteLanguage()->getLanguageId()
);

$translatedRows = $this->contentFetcher->getFlatContentRecords($localizedLanguageId);
$translatedRows = $contentFetcher->getFlatContentRecords($localizedLanguageId);

$grid = $defaultLanguageColumnObject->getGrid();
if ($grid === null) {
Expand Down Expand Up @@ -187,77 +253,7 @@ protected function getLanguageColumnsWithDefLangBindingForPageLayoutContext(Page
);
$languageColumns[$localizedLanguageId] = $languageColumnObject;
}
$languageColumns = [$defaultLanguageColumnObject] + $languageColumns;

return $languageColumns;
}

/**
* @param bool $renderUnused If true, renders the bottom column with unused records
* @return string
*/
public function drawContent(bool $renderUnused = true): string
{
$this->view->assign('hideRestrictedColumns', (bool)(BackendUtility::getPagesTSconfig($this->context->getPageId())['mod.']['web_layout.']['hideRestrictedCols'] ?? false));
$this->view->assign('newContentTitle', $this->getLanguageService()->getLL('newContentElement'));
$this->view->assign('newContentTitleShort', $this->getLanguageService()->getLL('content'));
$this->view->assign('allowEditContent', $this->getBackendUser()->check('tables_modify', 'tt_content'));

if ($this->context->getDrawingConfiguration()->getLanguageMode()) {
if ($this->context->getDrawingConfiguration()->getDefaultLanguageBinding()) {
$this->view->assign('languageColumns', $this->getLanguageColumnsWithDefLangBindingForPageLayoutContext($this->context));
} else {
$this->view->assign('languageColumns', $this->getLanguageColumnsForPageLayoutContext($this->context));
}
} else {
$context = $this->context;
// Check if we have to use a localized context for grid creation
if ($this->context->getDrawingConfiguration()->getSelectedLanguageId() > 0) {
// In case a localization is selected, clone the context with this language
$localizedContext = $this->context->cloneForLanguage(
$this->context->getSiteLanguage($this->context->getDrawingConfiguration()->getSelectedLanguageId())
);
if ($localizedContext->getLocalizedPageRecord()) {
// In case the localized context contains the corresponding
// localized page record use this context for grid creation.
$context = $localizedContext;
}
}
$this->view->assign('grid', $this->getGridForPageLayoutContext($context));
}

$rendered = $this->view->render('PageLayout');
if ($renderUnused) {
$unusedRecords = $this->contentFetcher->getUnusedRecords();

if (!empty($unusedRecords)) {
$unusedElementsMessage = GeneralUtility::makeInstance(
FlashMessage::class,
$this->getLanguageService()->getLL('staleUnusedElementsWarning'),
$this->getLanguageService()->getLL('staleUnusedElementsWarningTitle'),
FlashMessage::WARNING
);
$service = GeneralUtility::makeInstance(FlashMessageService::class);
$queue = $service->getMessageQueueByIdentifier();
$queue->addMessage($unusedElementsMessage);

$unusedGrid = GeneralUtility::makeInstance(Grid::class, $this->context);
$unusedRow = GeneralUtility::makeInstance(GridRow::class, $this->context);
$unusedColumn = GeneralUtility::makeInstance(GridColumn::class, $this->context, ['name' => 'unused']);

$unusedGrid->addRow($unusedRow);
$unusedRow->addColumn($unusedColumn);

foreach ($unusedRecords as $unusedRecord) {
$item = GeneralUtility::makeInstance(GridColumnItem::class, $this->context, $unusedColumn, $unusedRecord);
$unusedColumn->addItem($item);
}

$this->view->assign('grid', $unusedGrid);
$rendered .= $this->view->render('UnusedRecords');
}
}
return $rendered;
return [$defaultLanguageColumnObject] + $languageColumns;
}

protected function getBackendUser(): BackendUserAuthentication
Expand Down
@@ -1,5 +1,5 @@
<f:comment>
Styling requires the colpos to be set to the string 'unused'. To preserve type safty in the
Styling requires the colpos to be set to the string 'unused'. To preserve type safety in the
controller, the string is only used in the template by setting the below "colpos" variable.
</f:comment>
<f:variable name="colpos" value="{f:if(condition: column.unused, then: 'unused', else: column.columnNumber)}"/>
Expand Down
@@ -0,0 +1,66 @@
<html
xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true"
>

<f:layout name="Module" />

<f:section name="Content">

<f:be.pageRenderer
includeRequireJsModules="{
0: 'TYPO3/CMS/Recordlist/ClearCache',
1: 'TYPO3/CMS/Backend/NewContentElementWizardButton',
2: 'TYPO3/CMS/Backend/ContextMenu',
3: 'TYPO3/CMS/Backend/Tooltip',
4: 'TYPO3/CMS/Backend/Localization',
5: 'TYPO3/CMS/Backend/LayoutModule/DragDrop',
6: 'TYPO3/CMS/Backend/Modal'
}"
/>

<f:variable name="immediateActionArgs" value="{0: 'web', 1: pageId, 2: 1}" />
<typo3-immediate-action action="TYPO3.Backend.Storage.ModuleStateStorage.updateWithCurrentMount" args="{immediateActionArgs -> f:format.json() -> f:format.htmlspecialchars()}"></typo3-immediate-action>

<f:for each="{infoBoxes}" as="infoBox">
<f:be.infobox title="{infoBox.title}" state="{infoBox.state}">
<f:format.raw>{infoBox.message}</f:format.raw>
</f:be.infobox>
</f:for>

<f:if condition="{isPageEditable}">
<f:then>
<h1 class="t3js-title-inlineedit">{localizedPageTitle}</h1>
</f:then>
<f:else>
<h1>{localizedPageTitle}</h1>
</f:else>
</f:if>

<f:format.raw>{eventContentHtmlTop}</f:format.raw>

<form action="{f:be.uri(route:'web_layout', parameters:'{id: pageId}')}" id="PageLayoutController" method="post">
<f:format.raw>{mainContentHtml}</f:format.raw>
</form>

<f:if condition="{hiddenElementsShowToggle}">
<div class="form-check">
<input
type="checkbox"
id="checkTt_content_showHidden"
class="form-check-input"
name="SET[tt_content_showHidden]"
value="1"
{f:if(condition:'{hiddenElementsState} == 1', then:'checked="checked"')}
/>
<label class="form-check-label" for="checkTt_content_showHidden">
<f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:hiddenCE" /> (<span class="t3js-hidden-counter">{hiddenElementsCount}</span>)
</label>
</div>
</f:if>

<f:format.raw>{eventContentHtmlBottom}</f:format.raw>

</f:section>

</html>
@@ -0,0 +1,21 @@
<html
xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers"
data-namespace-typo3-fluid="true"
>

<f:layout name="Module" />

<f:section name="Content">

<f:variable name="immediateActionArgs" value="{0: 'web', 1: pageId}" />
<typo3-immediate-action action="TYPO3.Backend.Storage.ModuleStateStorage.update" args="{immediateActionArgs -> f:format.json() -> f:format.htmlspecialchars()}"></typo3-immediate-action>

<h1>{siteName}</h1>

<f:be.infobox title="{f:translate(key:'LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:clickAPage_header')}" state="-1">
<f:translate key="LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:clickAPage_content" />
</f:be.infobox>

</f:section>

</html>

0 comments on commit ab6a457

Please sign in to comment.