Breaking: #96812 - No Frontend TypoScript based template overrides in the backend

See forge#96812

Description

A couple of Core extensions with backend module controllers allowed overriding Fluid templates using frontend TypoScript. The two documented extensions are EXT:dashboard and the backend page module. The Extbase based backend extensions EXT:belog, EXT:beuser and EXT:extensionmanager allowed this implicitly too, but this detail has never been directly documented.

This functionality has been removed: All Core extensions, and in general all extensions that switch to the simplified backend templating no longer use the frontend TypoScript based override approach. This has been superseded by a general override strategy based on TSconfig, as described in this changelog entry.

This change became necessary since configuring backend modules via frontend TypoScript is flawed by design: It on one hand forces backend modules to parse the full frontend TypoScript, which is a general performance penalty in the backend - the backend then scales with the amount of frontend TypoScript. Also, the implementation is based on the Extbase ConfigurationManager, which leads to the situation that casual non-Extbase backend modules have an indirect dependency to lots of Extbase code. But most importantly, frontend TypoScript is always bound to a page record. There is no concept in the frontend for the "root" page zero, since that page can not be rendered in the frontend. In the backend however, we have many modules that are not within page context: In general all modules that do not have a page tree. This gives the "Use frontend TypoScript to configure backend modules" approach some hard headaches: It forces the ConfigurationManager to still select "some" page as frontend TypoScript entry point. In practice, the first non-hidden tree-level-one page that has a sys_template record is selected. This strategy is both ugly and troublesome, and leads to the situation that backend module configuration had to be bound to this first page, which could easily explode when for instance pages are resorted - apart from the fact that this scenario is hard to understand and to debug.

Impact

The combination of performance drawbacks, the tight Extbase coupling, and the "which frontend TypoScript should be parsed for page zero?" problematic leads to the decision to phase out the "frontend TypoScript for backend module configuration" approach that Extbase brought in.

One part of this process is a generic backend approach to override backend templates using TSconfig. This has impact on EXT:dashboard widgets and page module template overrides.

Affected Installations

Instances with extensions that configure own EXT:dashboard widgets or override templates of existing dashboard widgets using Frontend TypoScript are affected, as well as instances that override page module templates as described in this changelog entry.

Migration

Page module template overrides

An instance sets frontend TypoScript like this:

module.tx_backend.view.templateRootPaths.1644483508 = EXT:myext/Resources/Private/Templates/
module.tx_backend.view.partialRootPaths.1644483508 = EXT:myext/Resources/Private/Partials/
Copied!

If extension "myext" now delivered a template file such as Resources/Private/Templates/PageLayout/PageLayout.html, that template file was used for rendering the page module instead of the default template.

As described in this changelog, the new definition is now done using TSconfig. The extension "myext" with Composer name "myvendor/myext" can deliver a Configuration/page.tsconfig file (see changelog) with the below content to substitute the old definition and keep overriding template files at the current position:

# Pattern: templates."composer-name"."something-unique" = "overriding-extension-composer-name":"entry-path"
templates.typo3/cms-backend.1644483508 = myvendor/myext:Resources/Private
Copied!

EXT:dashboard

Required changes regarding existing template overrides of the dashboard extension and the dashboard widget registration itself are a bit broader. Let's look at this in detail:

Templating

An extension delivers this TypoScript:

module.tx_dashboard {
    view {
        templateRootPaths {
            1644485473 = EXT:myext/Resources/Private/Templates/Dashboard/Widgets/
        }
    }
}
Copied!

This instructed the dashboard widget renderer to look up widget templates in this path, too. The new registration for extension "myext" with Composer name "myvendor/myext" using file Configuration/page.tsconfig (see changelog) could look like this:

# Pattern: templates.typo3/cms-dashboard."something-unique" = "overriding-extension-composer-name":"entry-path"
templates.typo3/cms-dashboard.1644485473 = myvendor/myext:Resources/Private
Copied!

A widget template is then put to Resources/Private/Templates/Dashboard/Widgets/MyExtensionWidget.html. Extensions that want to stay compatible with both TYPO3 Core v11 and v12 should simply define both the old way and the new way.

Widget registration using Services.yaml

This part (changing Services.yaml and widgets PHP code) is not strictly needed for extensions that configure and deliver own widgets. Extension that work with TYPO3 v11 just work in v12 as well. However, the registration and PHP code changed a bit, extensions that want to stay deprecation log free with v12 should adapt. The changes outlined below will be mandatory with v13.

The registration of widgets using Services.yaml should be changed a bit. It was previously documented that widgets can inject an instance of StandaloneView. This approach was flawed: The StandaloneView has an internal dependency to the current PSR-7 request. The request is not available via dependency injection since it is a heavily stateful runtime dependency. Injecting a view that depends on request is thus a violation and only worked with EXT:dashboard because StandaloneView hides that dependency internally and creates a new request on the fly, which is a hack in that implementation that should be avoided.

The view based on EXT:core ViewInterface with its factory for backend views based on EXT:backend BackendViewFactory makes the dependency to the request object explicit. As such, a "prepared" view can not be injected using DI anymore.

This has impact on both the PHP implementation of widgets, as well as the widget dependency injection configuration.

Let's say a widget has been registered like this:

# This is defined in EXT:dashboard Services.yaml already, extensions
# must not define this in their Services.yaml files again.
dashboard.views.widget:
  class: 'TYPO3\CMS\Fluid\View\StandaloneView'
  public: true
  factory: ['TYPO3\CMS\Dashboard\Views\Factory', 'widgetTemplate']

# This is your custom widget registration in your extensions Services.yaml
dashboard.widget.sysLogErrors:
  class: 'TYPO3\CMS\Dashboard\Widgets\BarChartWidget'
  arguments:
    $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogErrorsDataProvider'
    $view: '@dashboard.views.widget'
    $buttonProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogButtonProvider'
  tags:
    ...
Copied!

The important line is $view: '@dashboard.views.widget': This instructs the DI to inject an instance of StandaloneView using the EXT:dashboard Factory::widgetTemplate() method for argument $view. The dashboard.views.widget is deprecated since TYPO3 Core v12 and should not be used anymore. It logs a deprecation message upon use during build-time and will be removed in v13 together with the Factory.

The new registration should be adapted to this, simply removing the $view argument:

# This is your custom widget registration in your extensions Services.yaml
dashboard.widget.sysLogErrors:
  class: 'TYPO3\CMS\Dashboard\Widgets\BarChartWidget'
  arguments:
    $dataProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogErrorsDataProvider'
    $buttonProvider: '@TYPO3\CMS\Dashboard\Widgets\Provider\SysLogButtonProvider'
  tags:
    ...
Copied!

Now the PHP implementation. The above example references the BarChartWidget class to take care of rendering. The class looked like this before (shortened):

class BarChartWidget implements WidgetInterface
{
    public function __construct(
        private readonly WidgetConfigurationInterface $configuration,
        private readonly ChartDataProviderInterface $dataProvider,
        private readonly StandaloneView $view,
        private readonly $buttonProvider = null,
        private readonly array $options = []
    ) {
    }

    public function renderWidgetContent(): string
    {
        $this->view->setTemplate('Widget/ChartWidget');
        $this->view->assignMultiple([...]);
        return $this->view->render();
    }
}
Copied!

Since StandaloneView should not be injected anymore, we now inject the BackendViewFactory instead and create a view using the factory in renderWidgetContent(). The factory create() method needs the request object. To get this, widgets should now implement RequestAwareWidgetInterface, the EXT:dashboard framework will then setRequest() the current request to the widget immediately after widget instantiation. The new code thus looks like this:

class BarChartWidget implements WidgetInterface, RequestAwareWidgetInterface
{
    private ServerRequestInterface $request;

    public function __construct(
        private readonly WidgetConfigurationInterface $configuration,
        private readonly ChartDataProviderInterface $dataProvider,
        private readonly BackendViewFactory $backendViewFactory,
        private readonly $buttonProvider = null,
        private readonly array $options = []
    ) {
    }

    public function setRequest(ServerRequestInterface $request): void
    {
        $this->request = $request;
    }

    public function renderWidgetContent(): string
    {
        // The second argument is the Composer 'name' of the extension that adds the widget.
        // It is needed to instruct BackendViewFactory to look up templates in this package
        // next to the default location 'typo3/cms-dashboard', too.
        $view = $this->backendViewFactory->create($this->request, ['typo3/cms-dashboard', 'myVendor/myPackage']);
        $this->view->assignMultiple([...]);
        return $this->view->render('Widget/ChartWidget');
    }
}
Copied!

The actual implementation in TYPO3 v12 is still slightly different to keep compatibility with extensions that re-use Core widgets and need v11 and v12 compatibility at the same time. Those Core classes will be adapted in v13 to the above outline version, though.