Feature: #86003 - Composition based API for the Adminpanel

See forge#86003

Description

A new API to extend the adminpanel for extension authors has been introduced. Enabling future enhancements for the adminpanel without having to make breaking changes for existing module providers is a key ingredient for providing a future proof extensible solution. Using single big interfaces that need to change on updates break backwards compatibility and do not provide sufficient feature encapsulation.

The adminpanel APIs have been refactored to use a composition pattern to allow modules more flexibility. Modules can now only implement the interfaces they need instead of implementing all functionality. For example an adminpanel module that only provides page related settings does no longer have to implement the getContent() method.

Small interfaces have been provided as building blocks for modules with rich functionality. Easy addition of new interfaces that _can_ (not must) be implemented allow future improvements.

Additionally the API has been modified to allow a more object-oriented approach using simple DTOs instead of associative arrays for better type-hinting and a better developer experience. Storing and rendering data have been separated in two steps allowing to completely disconnect the rendered adminpanel from the current page. This can be used as a base for building a complete standalone adminpanel without API changes.

General Request Flow and the Adminpanel

To better understand how the adminpanel stores and renders data, let's take a short look at how the adminpanel is initialized and rendered.

Since TYPO3 v9 TYPO3 uses PSR-15 middlewares. The adminpanel brings three that are relevant to its rendering process:

  • AdminPanelInitiator - Called early in the request stack to allow initialisation of modules to catch most of the request data (for example log entries)
  • AdminPanelDataPersister - Called at nearly the end of a frontend request to store the collected data (this is where module data gets saved)
  • AdminPanelRenderer - Called as one of the last steps in the rendering process, currently replacing the closing body tag with its own code (this is where module content gets rendered)

When building own modules keep in mind at which step your modules methods get called. In the last step for example (the rendering), you should not depend on any data outside of that provided to the module directly (for example do not rely on :php:$GLOBALS` to be filled).

Current Design Considerations

While the API of the adminpanel is very flexible in combining interfaces, the UI has a fixed structure and therefor a few things to consider when implementing own modules.

  • The bottom bar of the adminpanel will only be rendered for modules that have submodules and implement the SubmoduleProviderInterface
  • ShortInfo (see below) is only displayed for "TopLevel" modules
  • Content is only rendered for submodules

How-To add own modules

Adding custom adminpanel modules always follows these steps:

  1. Create a class implementing the basic ModuleInterface
  2. Register the class in ext_localconf.php
  3. Implement further interfaces for additional capabilities
  4. Create module class

To create your own admin panel module, create a new PHP class implementing \TYPO3\CMS\Adminpanel\ModuleApi\ModuleInterface . The interface denotes your class as an adminpanel module and requires the implementation of getIdentifier() and getLabel() as a minimum of methods for a module.

2. Register your module

Displayed as a top level module:

Register your module by adding the following in your ext_localconf.php:

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']['mynamespace_modulename'] => [
    'module' => \Your\Namespace\Adminpanel\Modules\YourModule::class,
    'before' => ['cache'],
];
Copied!

via before or after you can influence where your module will be displayed in the module menu by referencing the identifier / array key of other modules.

Displayed as a sub module:

Register your module by adding the following in your ext_localconf.php:

$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']['info']['submodules']['mynamespace_modulename'] => [
    'module' => \Your\Namespace\Adminpanel\Modules\YourModule::class
];
Copied!

Note the submodules key in the array allowing you to introduce hierarchical configuration.

3. Add additional interfaces

Your module is currently registered but is not doing anything yet, as it has no additional capabilities. The adminpanel provides additional separate interfaces (see list below). By implementing multiple interfaces you have fine-grained control over how your module behaves, which data it stores and how it gets rendered.

Adminpanel Interfaces

ModuleInterface

Purpose

Base interface all adminpanel modules share, defines common methods.

Methods

  • getIdentifier() - Returns string identifier of a module (for example mynamespace_modulename)
  • getLabel() - Returns speaking label of a module (for example My Module)

ConfigurableInterface

Purpose

Used to indicate that an adminpanel module can be enabled or disabled via configuration

Methods

  • isEnabled - Returns bool depending on whether the module is enabled.

Example implementation

\TYPO3\CMS\Adminpanel\ModuleApi\AbstractModule::isEnabled():

/**
 * Returns true if the module is
 * -> either enabled via TSConfig admPanel.enable
 * -> or any setting is overridden
 * override is a way to use functionality of the admin panel without displaying the panel to users
 * for example: hidden records or pages can be displayed by default
 *
 * @return bool
 */
public function isEnabled(): bool
{
    $identifier = $this->getIdentifier();
    $result = $this->isEnabledViaTsConfig();
    if ($this->mainConfiguration['override.'][$identifier] ?? false) {
        $result = (bool)$this->mainConfiguration['override.'][$identifier];
    }
    return $result;
}


Copied!

ContentProviderInterface

Purpose

Adminpanel interface to denote that a module has content to be rendered

Methods

  • getContent(ModuleData $data) - Return content as HTML. For modules implementing the DataProviderInterface the "ModuleData" object is automatically filled with the stored data - if no data is given a "fresh" ModuleData object is injected.

Example implementation

\TYPO3\CMS\Adminpanel\Modules\Debug\QueryInformation::getContent:

public function getContent(ModuleData $data): string
{
    $view = new StandaloneView();
    $view->setTemplatePathAndFilename(
        'typo3/sysext/adminpanel/Resources/Private/Templates/Modules/Debug/QueryInformation.html'
    );
    $this->getLanguageService()->includeLLFile('EXT:adminpanel/Resources/Private/Language/locallang_debug.xlf');
    $view->assignMultiple($data->getArrayCopy());
    return $view->render();
}

Copied!

DataProviderInterface

Purpose

Adminpanel interface to denote that a module provides data to be stored for the current request.

Adminpanel modules can save data to the adminpanel request cache and access this data in the rendering process. Data necessary for rendering the module content has to be returned via this interface implementation, as this allows for separate data collection and rendering and is a pre-requisite for a standalone debug tool.

Methods

  • getDataToStore(ServerRequestInterface $request): ModuleData - Return a ModuleData instance with the data to store

Example implementation

\TYPO3\CMS\Adminpanel\Modules\Info\RequestInformation::getDataToStore:

public function getDataToStore(ServerRequestInterface $request): ModuleData
{
    return new ModuleData(
        [
            'post' => $_POST,
            'get' => $_GET,
            'cookie' => $_COOKIE,
            'session' => $_SESSION,
            'server' => $_SERVER,
        ]
    );
}
Copied!

InitializableInterface

Purpose

Adminpanel interface to denote that a module has tasks to perform on initialization of the request.

Modules that need to set data / options early in the rendering process to be able to collect data, should implement this interface - for example the log module uses the initialization to register the adminpanel log collection early in the rendering process.

Initialize is called in the PSR-15 middleware stack through adminpanel initialisation via the AdminPanel MainController.

Methods

  • initializeModule(ServerRequestInterface $request) - Called on adminpanel initialization

Example implementation

\TYPO3\CMS\Adminpanel\Modules\CacheModule::initializeModule:

public function initializeModule(ServerRequestInterface $request): void
{
    if ($this->configurationService->getConfigurationOption('cache', 'noCache')) {
        $this->getTypoScriptFrontendController()->set_no_cache('Admin Panel: No Caching', true);
    }
}

Copied!

ModuleSettingsProviderInterface

Purpose

Adminpanel module settings interface denotes that a module has own settings.

The adminpanel knows two types of settings:

  • ModuleSettings are relevant for the module itself and its representation (for example the log module provides settings where displayed log level and grouping of the module can be configured)
  • PageSettings are relevant for rendering the page (for example the preview module provides settings showing or hiding hidden content elements or simulating a specific rendering time)

If a module provides settings relevant to its own content, use this interface.

Methods

  • getSettings(): string - Return settings as rendered HTML form elements

Example implementation

\TYPO3\CMS\Adminpanel\Modules\TsDebug\TypoScriptWaterfall::getSettings:

public function getSettings(): string
{
    $view = GeneralUtility::makeInstance(StandaloneView::class);
    $templateNameAndPath = 'EXT:adminpanel/Resources/Private/Templates/Modules/TsDebug/TypoScriptSettings.html';
    $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($templateNameAndPath));
    $view->setPartialRootPaths(['EXT:adminpanel/Resources/Private/Partials']);

    $view->assignMultiple(
        [
            'tree' => (int)$this->getConfigurationOption('tree'),
            ...
        ]
    );

    return $view->render();
}

Copied!

OnSubmitActorInterface

Purpose

Adminpanel interface for modules that need to react on changed configuration (for example if fluid debug settings change, the frontend cache should be cleared).

OnSubmitActors are currently called upon persisting new configuration _before_ the page is reloaded.

Methods

  • onSubmit(array $configurationToSave, ServerRequestInterface $request) - Can act when configuration gets saved. Configuration form vars are provided in $configurationToSave as an array.

Example implementation

\TYPO3\CMS\Adminpanel\Modules\PreviewModule::onSubmit:

/**
 * Clear page cache if fluid debug output setting is changed
 *
 * @param array $input
 * @param ServerRequestInterface $request
 * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
 */
public function onSubmit(array $input, ServerRequestInterface $request): void
{
    $activeConfiguration = (int)$this->getConfigOptionForModule('showFluidDebug');
    if (isset($input['preview_showFluidDebug']) && (int)$input['preview_showFluidDebug'] !== $activeConfiguration) {
        $pageId = (int)$request->getParsedBody()['TSFE_ADMIN_PANEL']['preview_clearCacheId'];
        $cacheManager = GeneralUtility::makeInstance(CacheManager::class);
        $cacheManager->getCache('cache_pages')->flushByTag('pageId_' . $pageId);
        $cacheManager->getCache('fluid_template')->flush();
    }
}


Copied!

PageSettingsProviderInterface

Purpose

Adminpanel page settings interface denotes that a module has settings regarding the page rendering.

The adminpanel knows two types of settings:

  • ModuleSettings are relevant for the module itself and its representation (for example the log module provides settings where displayed log level and grouping of the module can be configured)
  • PageSettings are relevant for rendering the page (for example the preview module provides settings showing or hiding hidden content elements or simulating a specific rendering time)

If a module provides settings changing the rendering of the main page request, use this interface.

Methods

  • getSettings(): string - Return HTML form elements for settings

Example implementation

\TYPO3\CMS\Adminpanel\Modules\EditModule::getPageSettings:

public function getPageSettings(): string
{
    $editToolbarService = GeneralUtility::makeInstance(EditToolbarService::class);
    $toolbar = $editToolbarService->createToolbar();
    $view = GeneralUtility::makeInstance(StandaloneView::class);
    $templateNameAndPath = 'EXT:adminpanel/Resources/Private/Templates/Modules/Settings/Edit.html';
    $view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($templateNameAndPath));
    $view->setPartialRootPaths(['EXT:adminpanel/Resources/Private/Partials']);
    $view->assignMultiple(
        [
            'feEdit' => ExtensionManagementUtility::isLoaded('feedit'),
            ...
        ]
    );
    return $view->render();

Copied!

PageSettingsProviderInterface

Purpose

Adminpanel interface to denote that a module has own resource files.

An adminpanel module implementing this interface may deliver custom JavaScript and Css files to provide additional styling and JavaScript functionality

Methods

  • getJavaScriptFiles(): array - Returns a string array with javascript files that will be rendered after the module
  • getCssFiles(): array - Returns a string array with CSS files that will be rendered after the module

Example implementation

\TYPO3\CMS\Adminpanel\Modules\TsDebugModule :

public function getJavaScriptFiles(): array
{
    return ['EXT:adminpanel/Resources/Public/JavaScript/Modules/TsDebug.js'];
}

public function getCssFiles(): array
{
    return [];
}

Copied!

ShortInfoProviderInterface

Purpose

Adminpanel shortinfo provider interface can be used to add the module to the short info bar of the adminpanel.

Modules providing shortinfo will be displayed in the bottom bar of the adminpanel and may provide "at a glance" info about the current state (for example the log module provides the number of warnings and errors directly).

Be aware that modules with submodules at the moment can only render one short info (the one of the "parent" module). This will likely change in v10.

Methods

  • getShortInfo(): string - Info string (no HTML) that should be rendered
  • getIconIdentifier(): string - An icon for this info line, needs to be registered in IconRegistry

Example implementation

\TYPO3\CMS\Adminpanel\Modules\InfoModule :

public function getShortInfo(): string
{
    $parseTime = $this->getTimeTracker()->getParseTime();
    return sprintf($this->getLanguageService()->sL(
        'LLL:EXT:adminpanel/Resources/Private/Language/locallang_info.xlf:module.shortinfo'
    ), $parseTime);
}

public function getIconIdentifier(): string
{
    return 'actions-document-info';
}


Copied!

SubmoduleProviderInterface

Purpose

Adminpanel interface providing hierarchical functionality for modules.

A module implementing this interface may have submodules. Be aware that the current implementation of the adminpanel renders a maximum level of 2 for modules. If you need to render more levels, write your own module and implement multi-hierarchical rendering in the getContent method.

Methods

  • setSubModules(array $subModules) - Sets array of module instances (instances of ModuleInterface) as submodules
  • getSubModules(): array - Returns an array of module instances
  • hasSubmoduleSettings(): bool - Return true if any of the submodules has settings to be rendered (can be used to render settings in a central place)

Example implementation

\TYPO3\CMS\Adminpanel\ModuleApi\AbstractModule :

public function setSubModules(array $subModules): void
{
    $this->subModules = $subModules;
}

public function getSubModules(): array
{
    return $this->subModules;
}

public function hasSubmoduleSettings(): bool
{
    $hasSettings = false;
    foreach ($this->subModules as $subModule) {
        if ($subModule instanceof ModuleSettingsProviderInterface) {
            $hasSettings = true;
            break;
        }
        if ($subModule instanceof SubmoduleProviderInterface) {
            $hasSettings = $subModule->hasSubmoduleSettings();
        }
    }
    return $hasSettings;
}

Copied!