Commit c78469c0 authored by Oliver Bartsch's avatar Oliver Bartsch Committed by Benni Mack
Browse files

[FEATURE] Introduce PSR-14 events for DatabaseRecordList

Three new PSR-14 based events are introduced in favour
of the existing $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions']
hook, which is now marked as deprecated, along with
its interface "RecordListHookInterface".

Besides the obvious advantage of using PSR-14 events,
the RecordListHookInterface also required to always
implement all hook methods, even if only one of them
was really used.

The new events feature the same functionality, but
improved and extended. The main subject, e.g. the
record actions or the table header columns, is
therefore equipped with extended CRUD methods,
like adding a new action at a certain position.

Resolves: #95105
Releases: master
Change-Id: If3194474eca7c111be4d113fda04992c5bf5f16c
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70887

Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 74ee68f1
......@@ -128,6 +128,10 @@ services:
TYPO3\CMS\Backend\Form\FormDataProvider\SiteDatabaseEditRow:
public: true
TYPO3\CMS\Backend\RecordList\ElementBrowserRecordList:
shared: false
public: true
TYPO3\CMS\Backend\Resource\PublicUrlPrefixer:
public: true
......
.. include:: ../../Includes.txt
==============================================
Deprecation: #95105 - DatabaseRecordList hooks
==============================================
See :issue:`95105`
Description
===========
The TYPO3 Hook :php:`$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/class.db_list_extra.inc']['actions']`
which is used in the :php:`DatabaseRecordList` class for modifying the
behaviour of each table listing, has been deprecated.
Using this hook always required to implement the :php:`RecordListHookInterface`,
which then required the corresponding hook class to implement four different
hook methods, even if only one of them was needed.
Furthermore are those methods no longer sufficient since e.g. the "controls"
and "clip" sections were merged together already. Therefore, also the
accompanied PHP Interface :php:`TYPO3\CMS\Recordlist\RecordList\RecordListHookInterface`
has been marked as deprecated.
Impact
======
If the hook is registered in a TYPO3 installation, a PHP deprecation
message is triggered. The extension scanner also detects any usage
of the deprecated interface as strong, and the definition of the
hook as weak match.
Affected Installations
======================
TYPO3 installations with custom extensions using this hook.
Migration
=========
Migrate to the corresponding RecordList PSR-14 events:
- `ModifyRecordListTableActionsEvent`
- `ModifyRecordListHeaderColumnsEvent`
- `ModifyRecordListRecordActionsEvent`
.. index:: PHP-API, FullyScanned, ext:core
.. include:: ../../Includes.txt
======================================================
Feature: #95105 - New PSR-14 DatabaseRecordList events
======================================================
See :issue:`95105`
Description
===========
A couple of new PSR-14 events for the :php:`DatabaseRecordList` class
have been added to TYPO3 Core. They are mainly a direct replacement for
the hook methods, defined in the :php:`RecordListHookInterface`, while
their functionality is improved and extended.
The new events can be used to modify the behaviour of each table listing,
which means they can be used to either add, change or even remove columns
and actions.
Following events have been added:
- `ModifyRecordListTableActionsEvent`
- `ModifyRecordListHeaderColumnsEvent`
- `ModifyRecordListRecordActionsEvent`
They all behave in the same way. There is always the subject, e.g. the
record actions or the header columns, together with information like the
current table, the current :php:`DatabaseRecordList` instance and the
current record or the record uids. The subject is therefore equipped
with the usual CRUD methods like `set`, `get` or `remove`. This makes
working with those values much more pleasant. See the below code examples
on how those can be used. Some events also feature additional methods
to influence e.g. the table header attributes or the label, which is
being displayed in case no actions are available for the current user.
An example registration of the events in your extensions' `Services.yaml`:
.. code-block:: yaml
MyVendor\MyPackage\RecordList\MyEventListener:
tags:
- name: event.listener
identifier: 'my-package/recordlist/my-event-listener'
method: 'modifyRecordActions'
- name: event.listener
identifier: 'my-package/recordlist/my-event-listener'
method: 'modifyHeaderColumns'
- name: event.listener
identifier: 'my-package/recordlist/my-event-listener'
method: 'modifyTableActions'
The corresponding event listener class:
.. code-block:: php
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Recordlist\Event\ModifyRecordListHeaderColumnsEvent;
use TYPO3\CMS\Recordlist\Event\ModifyRecordListRecordActionsEvent;
use TYPO3\CMS\Recordlist\Event\ModifyRecordListTableActionsEvent;
class MyEventListener {
protected LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function modifyRecordActions(ModifyRecordListRecordActionsEvent $event): void
{
$currentTable = $event->getTable();
// Add a custom action for a custom table in the secondary action bar, before the "move" action
if ($currentTable === 'my_custom_table' && !$event->hasAction('myAction')) {
$event->setAction(
'<button>My Action</button>',
'myAction',
'secondary',
'move'
);
}
// Remove the "viewBig" action in case more than 4 actions exist in the group
if (count($event->getActionGroup('secondary')) > 4 && $event->hasAction('viewBig')) {
$event->removeAction('viewBig');
}
// Move the "delete" action after the "edit" action
$event->setAction('', 'delete', 'primary', '', 'edit');
}
public function modifyHeaderColumns(ModifyRecordListHeaderColumnsEvent $event): void
{
// Change label of "control" column
$event->setColumn('Custom Controls', '_CONTROL_');
// Add a custom class for the table header row
$event->setHeaderAttributes(['class' => 'my-custom-class']);
}
public function modifyTableActions(ModifyRecordListTableActionsEvent $event): void
{
// Remove "edit" action and log, if this failed
$actionRemoved = $event->removeAction('unknown');
if (!$actionRemoved) {
$this->logger->warning('Action "unknown" could not be removed');
}
// Add a custom clipboard action after "copyMarked"
$event->setAction('<button>My action</button>', 'myAction', '', 'copyMarked');
// Set a custom label for the case, no actions are available for the user
$event->setNoActionLabel('No actions available due to missing permissions.');
}
}
Please have a look at the concrete implementation for a list of all
available methods and their functionalities.
Impact
======
The new PSR-14 events can be used to modify various parts within the
RecordList module in an object-oriented way.
.. index:: PHP-API, ext:core
......@@ -522,4 +522,9 @@ return [
'Deprecation-95083-BackendToolbarCacheActionsHook.rst',
],
],
'$GLOBALS[\'TYPO3_CONF_VARS\'][\'SC_OPTIONS\'][\'typo3/class.db_list_extra.inc\'][\'actions\']' => [
'restFiles' => [
'Deprecation-95105-DatabaseRecordListHooks.rst',
],
],
];
......@@ -1809,4 +1809,9 @@ return [
'Deprecation-95083-BackendToolbarCacheActionsHook.rst',
],
],
'TYPO3\CMS\Recordlist\RecordList\RecordListHookInterface' => [
'restFiles' => [
'Deprecation-95105-DatabaseRecordListHooks.rst',
],
],
];
<?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\Recordlist\Event;
use TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList;
/**
* An event to modify the header columns for a table in the RecordList
*/
final class ModifyRecordListHeaderColumnsEvent
{
private array $columns;
private string $table;
/**
* @var int[]
*/
private array $recordIds;
private DatabaseRecordList $recordList;
/**
* Additional header attributes for the table header row
*
* @var string[]
*/
private array $headerAttributes = [];
public function __construct(array $columns, string $table, array $recordIds, DatabaseRecordList $recordList)
{
$this->columns = $columns;
$this->table = $table;
$this->recordIds = $recordIds;
$this->recordList = $recordList;
}
/**
* Add a new column or override an existing one. Latter is only possible,
* in case $columnName is given. Otherwise, the column will be added with
* a numeric index, which is generally not recommended.
*
* Note: Due to the behaviour of DatabaseRecordList, just adding a column
* does not mean that it is also displayed. The internal $fieldArray needs
* to be adjusted as well. This method only adds the column to the data array.
* Therefore, this method should mainly be used to edit existing columns, e.g.
* change their label.
*
* @param string $column
* @param string $columnName
*/
public function setColumn(string $column, string $columnName = ''): void
{
if ($columnName !== '') {
$this->columns[$columnName] = $column;
} else {
$this->columns[] = $column;
}
}
/**
* Whether the column exists
*
* @param string $columnName
* @return bool
*/
public function hasColumn(string $columnName): bool
{
return (bool)($this->columns[$columnName] ?? false);
}
/**
* Get column by its name
*
* @param string $columnName
* @return string|null The column or NULL if the column does not exist
*/
public function getColumn(string $columnName): ?string
{
return $this->columns[$columnName] ?? null;
}
/**
* Remove column by its name
*
* @param string $columnName
* @return bool Whether the column could be removed - Will therefore
* return FALSE if the column to remove does not exist.
*/
public function removeColumn(string $columnName): bool
{
if (!isset($this->columns[$columnName])) {
return false;
}
unset($this->columns[$columnName]);
return true;
}
public function setColumns(array $columns): void
{
$this->columns = $columns;
}
public function getColumns(): array
{
return $this->columns;
}
public function setHeaderAttributes(array $headerAttributes): void
{
$this->headerAttributes = $headerAttributes;
}
public function getHeaderAttributes(): array
{
return $this->headerAttributes;
}
public function getTable(): string
{
return $this->table;
}
public function getRecordIds(): array
{
return $this->recordIds;
}
/**
* Returns the current DatabaseRecordList instance.
*
* @return DatabaseRecordList
* @todo Might be replaced by a DTO in the future
*/
public function getRecordList(): DatabaseRecordList
{
return $this->recordList;
}
}
<?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\Recordlist\Event;
use TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList;
/**
* An event to modify the displayed record actions (e.g.
* "edit", "copy", "delete") for a table in the RecordList.
*/
final class ModifyRecordListRecordActionsEvent
{
private array $actions;
private string $table;
private array $record;
private DatabaseRecordList $recordList;
public function __construct(array $actions, string $table, array $record, DatabaseRecordList $recordList)
{
$this->actions = $actions;
$this->table = $table;
$this->record = $record;
$this->recordList = $recordList;
}
/**
* Add a new action or override an existing one. Latter is only possible,
* in case $columnName is given. Otherwise, the column will be added with
* a numeric index, which is generally not recommended. It's also possible
* to define the position of an action with either the "before" or "after"
* argument, while their value must be an existing action.
*
* Note: In case non or an invalid $group is provided, the new action will
* be added to the secondary group.
*
* @param string $action
* @param string $actionName
* @param string $group
* @param string $before
* @param string $after
*/
public function setAction(
string $action,
string $actionName = '',
string $group = '',
string $before = '',
string $after = ''
): void {
// Only "primary" and "secondary" are valid, default to "secondary" otherwise
$group = in_array($group, ['primary', 'secondary'], true) ? $group : 'secondary';
if ($actionName !== '') {
if ($before !== '' && $this->hasAction($before, $group)) {
$end = array_splice($this->actions[$group], (int)(array_search($before, array_keys($this->actions[$group]), true)));
$this->actions[$group] = array_merge($this->actions[$group], [$actionName => $action], $end);
} elseif ($after !== '' && $this->hasAction($after, $group)) {
$end = array_splice($this->actions[$group], (int)(array_search($after, array_keys($this->actions[$group]), true)) + 1);
$this->actions[$group] = array_merge($this->actions[$group], [$actionName => $action], $end);
} else {
$this->actions[$group][$actionName] = $action;
}
} else {
$this->actions[$group][] = $action;
}
}
/**
* Whether the action exists in the given group. In case non or
* an invalid $group is provided, both groups will be checked.
*
* @param string $actionName
* @param string $group
* @return bool
*/
public function hasAction(string $actionName, string $group = ''): bool
{
if (in_array($group, ['primary', 'secondary'], true)) {
return (bool)($this->actions[$group][$actionName] ?? false);
}
return (bool)($this->actions['primary'][$actionName] ?? $this->actions['secondary'][$actionName] ?? false);
}
/**
* Get action by its name. In case the action exists in both groups
* and non or an invalid $group is provided, the action from the
* "primary" group will be returned.
*
* @param string $actionName
* @param string $group
* @return string|null
*/
public function getAction(string $actionName, string $group = ''): ?string
{
if (in_array($group, ['primary', 'secondary'], true)) {
return $this->actions[$group][$actionName] ?? null;
}
return $this->actions['primary'][$actionName] ?? $this->actions['secondary'][$actionName] ?? null;
}
/**
* Remove action by its name. In case the action exists in both groups
* and non or an invalid $group is provided, the action will be removed
* from both groups.
*
* @param string $actionName
* @param string $group
* @return bool Whether the action could be removed - Will therefore
* return FALSE if the action to remove does not exist.
*/
public function removeAction(string $actionName, string $group = ''): bool
{
if (($this->actions[$group][$actionName] ?? false) && in_array($group, ['primary', 'secondary'], true)) {
unset($this->actions[$group][$actionName]);
return true;
}
$actionRemoved = false;
if ($this->actions['primary'][$actionName] ?? false) {
unset($this->actions['primary'][$actionName]);
$actionRemoved = true;
}
if ($this->actions['secondary'][$actionName] ?? false) {
unset($this->actions['secondary'][$actionName]);
$actionRemoved = true;
}
return $actionRemoved;
}
/**
* Get the actions of a specific group
*
* @param string $group
* @return array|null
*/
public function getActionGroup(string $group): ?array
{
return in_array($group, ['primary', 'secondary'], true) ? $this->actions[$group] : null;
}
public function setActions(array $actions): void
{
$this->actions = $actions;
}
public function getActions(): array
{
return $this->actions;
}
public function getTable(): string
{
return $this->table;
}
public function getRecord(): array
{
return $this->record;
}
/**
* Returns the current DatabaseRecordList instance.
*
* @return DatabaseRecordList
* @todo Might be replaced by a DTO in the future
*/
public function getRecordList(): DatabaseRecordList
{
return $this->recordList;
}
}
<?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\Recordlist\Event;
use TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList;
/**
* An event to modify the multi record selection actions (e.g.
* "edit", "copy to clipboard") for a table in the RecordList.
*/
final class ModifyRecordListTableActionsEvent
{
private array $actions;
private string $table;