Commit 4ab765f2 authored by Alexander Schnitzler's avatar Alexander Schnitzler Committed by Susanne Moog
Browse files

[TASK] Deprecate switchable controller actions

The usage of switchable controller actions, both via
flexforms and typoscript, is deprecated and will be
removed in one the next major versions of TYPO3,
probably version 11.0 or 12.0.

Switchable controller actions allowed to override the
php plugin configuration and to create god plugins, i.e.
plugins that can be set into multiple different modes
and therefore take care of all possible use cases.

Every plugin should serve a single purpose, therefore
the usage of switchable controller actions is an anti
pattern which will be removed.

The switchable controller action mechanic will be
removed without replacement which means, that there
is no migration path to a similar feature.

Instead, extension authors need to create multiple,
dedicated plugins for different use cases.

Releases: master
Resolves: #89463
Change-Id: I41afac9303205f97f390f208803908177e00cda5
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61511

Tested-by: Richard Haeser's avatarRichard Haeser <richard@maxserv.com>
Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
Reviewed-by: Richard Haeser's avatarRichard Haeser <richard@maxserv.com>
Reviewed-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Susanne Moog's avatarSusanne Moog <look@susi.dev>
parent 98c58a00
.. include:: ../../Includes.txt
===============================================================
Deprecation: #89463 - Deprecate `switchable controller actions`
===============================================================
See :issue:`89463`
Description
===========
`Switchable controller actions` have been marked as deprecated and will be removed in one of the next major versions
of TYPO3, probably version 11.0 or 12.0.
`Switchable controller actions` are used to override the allowed set of controllers and actions via typoscript or plugin
flexforms. While this is convenient for reusing the same plugin for a lot of different use cases, it's also very
problematic as it completely overrides the original configuration defined via
:php:`\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin`.
`Switchable controller actions` therefore have bad implications that rectify their removal.
First of all, `switchable controller actions` override the original configuration of plugins at runtime and possibly
depending on conditions which contradicts the idea of :php:`\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin`
being the authoritative way to define configuration.
Using the same plugin as an entry point for many different functionalities contradicts the idea of a plugin serving one
specific purpose. `Switchable controller actions` allow for creating one central plugin that takes care of everything.
Impact
======
All plugins that are using `switchable controller actions` need to be split into multiple different plugins. Usually, one
would create a new plugin for each possible `switchable controller actions` configuration entry.
Affected Installations
======================
All installations that make use of `switchable controller actions`, either via flexform configuration of plugins or via
typoscript configuration.
Migration
=========
Unfortunately an automatic migration is not possible. As `switchable controller actions` allowed to override the whole
configuration of allowed controllers and actions, the only way to migrate is to create dedicated plugins for each former
`switchable controller actions` configuration entry.
Example:
.. code-block:: xml
<switchableControllerActions>
<TCEforms>
<label>switchable controller actions</label>
<config>
<renderType>selectSingle</renderType>
<items>
<numIndex index="1">
<numIndex index="0">List</numIndex>
<numIndex index="1">Product->list</numIndex>
</numIndex>
<numIndex index="2">
<numIndex index="0">Show</numIndex>
<numIndex index="1">Product->show</numIndex>
</numIndex>
</items>
</config>
</TCEforms>
</switchableControllerActions>
This configuration would lead to the creation configuration of two different plugins like this:
.. code-block: php
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
'extension',
'list',
[
'Product' => 'list'
]
);
\TYPO3\CMS\Extbase\Utility\ExtensionUtility::configurePlugin(
'extension',
'show',
[
'Product' => 'show'
]
);
.. index:: FlexForm, PHP-API, TypoScript, NotScanned, ext:extbase
......@@ -241,9 +241,19 @@ abstract class AbstractConfigurationManager implements \TYPO3\CMS\Core\Singleton
*
* @param array &$frameworkConfiguration
* @param array $switchableControllerActions
* @deprecated since TYPO3 v10, will be removed in one of the next major versions of TYPO3, probably version 11.0 or 12.0.
*/
protected function overrideControllerConfigurationWithSwitchableControllerActions(array &$frameworkConfiguration, array $switchableControllerActions): void
{
trigger_error(
sprintf(
'Plugin "%s" of extension "%s" uses switchable controller actions which has been marked as deprecated as of version TYPO3 10 and will be removed in one of the next major versions of TYPO3, probably version 11.0 or 12.0',
$frameworkConfiguration['pluginName'],
$frameworkConfiguration['extensionName']
),
E_USER_DEPRECATED
);
$controllerAliasToClass = [];
foreach ($frameworkConfiguration['controllerConfiguration'] as $controllerClass => $controllerConfiguration) {
$controllerAliasToClass[$controllerConfiguration['alias']] = $controllerClass;
......
......@@ -230,6 +230,7 @@ class FrontendConfigurationManager extends \TYPO3\CMS\Extbase\Configuration\Abst
* @param array $flexFormConfiguration The full flexForm configuration
* @throws Exception\ParseErrorException
* @return array the modified framework configuration, if needed
* @deprecated since TYPO3 v10, will be removed in one of the next major versions of TYPO3, probably version 11.0 or 12.0.
*/
protected function overrideControllerConfigurationWithSwitchableControllerActionsFromFlexForm(array $frameworkConfiguration, array $flexFormConfiguration): array
{
......
......@@ -18,6 +18,7 @@ use TYPO3\CMS\Core\DataHandling\DataHandler;
/**
* @internal this is not part of TYPO3 Core API as it is a concrete hook implementation.
* @deprecated since TYPO3 v10, will be removed when support for switchable controller actions is removed
*/
class CheckFlexFormValue
{
......
......@@ -102,21 +102,6 @@ class AbstractConfigurationManagerTest extends UnitTestCase
]
];
/**
* @var array
*/
protected $testSwitchableControllerActions = [
'MyExtension\\Controller\\Controller1' => [
'alias' => 'Controller1',
'actions' => ['action1', 'action2', 'action3']
],
'MyExtension\\Controller\\Controller2' => [
'alias' => 'Controller2',
'actions' => ['action4', 'action5', 'action6'],
'nonCacheableActions' => ['action4', 'action6']
]
];
/**
* Sets up this testcase
*/
......@@ -387,301 +372,6 @@ class AbstractConfigurationManagerTest extends UnitTestCase
$this->abstractConfigurationManager->getConfiguration('SomeOtherExtensionName', 'SomeOtherCurrentPluginName');
}
/**
* switchableControllerActions *
*/
/**
* @test
*/
public function switchableControllerActionsAreNotOverriddenIfPluginNameIsSpecified(): void
{
/** @var AbstractConfigurationManager|\PHPUnit\Framework\MockObject\MockObject|AccessibleObjectInterface $abstractConfigurationManager */
$abstractConfigurationManager = $this->getAccessibleMock(
AbstractConfigurationManager::class,
[
'overrideControllerConfigurationWithSwitchableControllerActions',
'getContextSpecificFrameworkConfiguration',
'getTypoScriptSetup',
'getPluginConfiguration',
'getControllerConfiguration',
'getRecursiveStoragePids'
],
[],
'',
false
);
$abstractConfigurationManager->_set('typoScriptService', $this->mockTypoScriptService);
$abstractConfigurationManager->setConfiguration(['switchableControllerActions' => ['overriddenSwitchableControllerActions']]);
$abstractConfigurationManager->expects(self::any())->method('getPluginConfiguration')->willReturn([]);
$abstractConfigurationManager->expects(self::never())->method('overrideControllerConfigurationWithSwitchableControllerActions');
$abstractConfigurationManager->getConfiguration('SomeExtensionName', 'SomePluginName');
}
/**
* @test
*/
public function switchableControllerActionsAreOverriddenIfSpecifiedPluginIsTheCurrentPlugin(): void
{
/** @var AbstractConfigurationManager|\PHPUnit\Framework\MockObject\MockObject|AccessibleObjectInterface $abstractConfigurationManager */
$configuration = [
'extensionName' => 'CurrentExtensionName',
'pluginName' => 'CurrentPluginName',
'switchableControllerActions' => ['overriddenSwitchableControllerActions']
];
$abstractConfigurationManager = $this->getAccessibleMock(
AbstractConfigurationManager::class,
[
'overrideControllerConfigurationWithSwitchableControllerActions',
'getContextSpecificFrameworkConfiguration',
'getTypoScriptSetup',
'getPluginConfiguration',
'getControllerConfiguration',
'getRecursiveStoragePids'
],
[],
'',
false
);
$this->mockTypoScriptService->expects(self::any())->method('convertTypoScriptArrayToPlainArray')->with($configuration)->willReturn($configuration);
$abstractConfigurationManager->_set('typoScriptService', $this->mockTypoScriptService);
$abstractConfigurationManager->setConfiguration($configuration);
$abstractConfigurationManager->expects(self::any())->method('getPluginConfiguration')->willReturn([]);
$abstractConfigurationManager->expects(self::once())->method('overrideControllerConfigurationWithSwitchableControllerActions');
$abstractConfigurationManager->getConfiguration('CurrentExtensionName', 'CurrentPluginName');
}
/**
* @test
*/
public function switchableControllerActionsAreOverriddenIfPluginNameIsNotSpecified(): void
{
/** @var AbstractConfigurationManager|\PHPUnit\Framework\MockObject\MockObject|AccessibleObjectInterface $abstractConfigurationManager */
$configuration = ['switchableControllerActions' => ['overriddenSwitchableControllerActions']];
$abstractConfigurationManager = $this->getAccessibleMock(
AbstractConfigurationManager::class,
[
'overrideControllerConfigurationWithSwitchableControllerActions',
'getContextSpecificFrameworkConfiguration',
'getTypoScriptSetup',
'getPluginConfiguration',
'getControllerConfiguration',
'getRecursiveStoragePids'
],
[],
'',
false
);
$this->mockTypoScriptService->expects(self::any())->method('convertTypoScriptArrayToPlainArray')->with($configuration)->willReturn($configuration);
$abstractConfigurationManager->_set('typoScriptService', $this->mockTypoScriptService);
$abstractConfigurationManager->setConfiguration($configuration);
$abstractConfigurationManager->expects(self::any())->method('getPluginConfiguration')->willReturn([]);
$abstractConfigurationManager->expects(self::once())->method('overrideControllerConfigurationWithSwitchableControllerActions');
$abstractConfigurationManager->getConfiguration();
}
/**
* @test
*/
public function orderOfActionsCanBeOverriddenForCurrentPlugin(): void
{
$configuration = [
'extensionName' => 'CurrentExtensionName',
'pluginName' => 'CurrentPluginName',
'switchableControllerActions' => [
'Controller1' => ['action2', 'action1', 'action3']
]
];
$this->mockTypoScriptService->expects(self::any())->method('convertTypoScriptArrayToPlainArray')->with($configuration)->willReturn($configuration);
$this->abstractConfigurationManager->setConfiguration($configuration);
$this->abstractConfigurationManager->expects(self::once())->method('getPluginConfiguration')->with(
'CurrentExtensionName',
'CurrentPluginName'
)->willReturn($this->testPluginConfiguration);
$this->abstractConfigurationManager->expects(self::once())->method('getControllerConfiguration')->with(
'CurrentExtensionName',
'CurrentPluginName'
)->willReturn($this->testSwitchableControllerActions);
$this->abstractConfigurationManager->expects(self::once())->method('getContextSpecificFrameworkConfiguration')->willReturnCallback(function (
$a
) {
return $a;
});
$mergedConfiguration = $this->abstractConfigurationManager->getConfiguration();
$expectedResult = [
'MyExtension\\Controller\\Controller1' => [
'className' => 'MyExtension\\Controller\\Controller1',
'alias' => 'Controller1',
'actions' => ['action2', 'action1', 'action3']
]
];
$actualResult = $mergedConfiguration['controllerConfiguration'];
self::assertEquals($expectedResult, $actualResult);
}
/**
* @test
*/
public function controllerOfSwitchableControllerActionsCanBeAFullyQualifiedClassName(): void
{
$configuration = [
'extensionName' => 'CurrentExtensionName',
'pluginName' => 'CurrentPluginName',
'switchableControllerActions' => [
'MyExtension\\Controller\\Controller1' => ['action2', 'action1', 'action3'],
'\\MyExtension\\Controller\\Controller2' => ['newAction2', 'action4', 'action5']
]
];
$this->mockTypoScriptService->expects(self::any())->method('convertTypoScriptArrayToPlainArray')->with($configuration)->willReturn($configuration);
$this->abstractConfigurationManager->setConfiguration($configuration);
$this->abstractConfigurationManager->expects(self::once())->method('getPluginConfiguration')->with(
'CurrentExtensionName',
'CurrentPluginName'
)->willReturn($this->testPluginConfiguration);
$this->abstractConfigurationManager->expects(self::once())->method('getControllerConfiguration')->with(
'CurrentExtensionName',
'CurrentPluginName'
)->willReturn($this->testSwitchableControllerActions);
$this->abstractConfigurationManager->expects(self::once())->method('getContextSpecificFrameworkConfiguration')->willReturnCallback(function (
$a
) {
return $a;
});
$mergedConfiguration = $this->abstractConfigurationManager->getConfiguration();
$expectedResult = [
'MyExtension\\Controller\\Controller1' => [
'className' => 'MyExtension\\Controller\\Controller1',
'alias' => 'Controller1',
'actions' => ['action2', 'action1', 'action3']
],
'MyExtension\\Controller\\Controller2' => [
'className' => 'MyExtension\\Controller\\Controller2',
'alias' => 'Controller2',
'actions' => ['newAction2', 'action4', 'action5'],
'nonCacheableActions' => ['action4']
]
];
$actualResult = $mergedConfiguration['controllerConfiguration'];
self::assertEquals($expectedResult, $actualResult);
}
/**
* @test
*/
public function newActionsCanBeAddedForCurrentPlugin(): void
{
$configuration = [
'extensionName' => 'CurrentExtensionName',
'pluginName' => 'CurrentPluginName',
'switchableControllerActions' => [
'Controller1' => ['action2', 'action1', 'action3', 'newAction']
]
];
$this->mockTypoScriptService->expects(self::any())->method('convertTypoScriptArrayToPlainArray')->with($configuration)->willReturn($configuration);
$this->abstractConfigurationManager->setConfiguration($configuration);
$this->abstractConfigurationManager->expects(self::once())->method('getPluginConfiguration')->with(
'CurrentExtensionName',
'CurrentPluginName'
)->willReturn($this->testPluginConfiguration);
$this->abstractConfigurationManager->expects(self::once())->method('getControllerConfiguration')->with(
'CurrentExtensionName',
'CurrentPluginName'
)->willReturn($this->testSwitchableControllerActions);
$this->abstractConfigurationManager->expects(self::once())->method('getContextSpecificFrameworkConfiguration')->willReturnCallback(function (
$a
) {
return $a;
});
$mergedConfiguration = $this->abstractConfigurationManager->getConfiguration();
$expectedResult = [
'MyExtension\\Controller\\Controller1' => [
'className' => 'MyExtension\\Controller\\Controller1',
'alias' => 'Controller1',
'actions' => ['action2', 'action1', 'action3', 'newAction']
]
];
$actualResult = $mergedConfiguration['controllerConfiguration'];
self::assertEquals($expectedResult, $actualResult);
}
/**
* @test
*/
public function controllersCanNotBeOverridden(): void
{
$configuration = [
'extensionName' => 'CurrentExtensionName',
'pluginName' => 'CurrentPluginName',
'switchableControllerActions' => [
'NewController' => ['action1', 'action2']
]
];
$this->mockTypoScriptService->expects(self::any())->method('convertTypoScriptArrayToPlainArray')->with($configuration)->willReturn($configuration);
$this->abstractConfigurationManager->setConfiguration($configuration);
$this->abstractConfigurationManager->expects(self::once())->method('getPluginConfiguration')->with(
'CurrentExtensionName',
'CurrentPluginName'
)->willReturn($this->testPluginConfiguration);
$this->abstractConfigurationManager->expects(self::once())->method('getControllerConfiguration')->with(
'CurrentExtensionName',
'CurrentPluginName'
)->willReturn($this->testSwitchableControllerActions);
$this->abstractConfigurationManager->expects(self::once())->method('getContextSpecificFrameworkConfiguration')->willReturnCallback(function (
$a
) {
return $a;
});
$mergedConfiguration = $this->abstractConfigurationManager->getConfiguration();
$expectedResult = [];
$actualResult = $mergedConfiguration['controllerConfiguration'];
self::assertEquals($expectedResult, $actualResult);
}
/**
* @test
*/
public function cachingOfActionsCanNotBeChanged(): void
{
$configuration = [
'extensionName' => 'CurrentExtensionName',
'pluginName' => 'CurrentPluginName',
'switchableControllerActions' => [
'Controller1' => ['newAction', 'action1'],
'Controller2' => ['newAction2', 'action4', 'action5']
]
];
$this->mockTypoScriptService->expects(self::any())->method('convertTypoScriptArrayToPlainArray')->with($configuration)->willReturn($configuration);
$this->abstractConfigurationManager->setConfiguration($configuration);
$this->abstractConfigurationManager->expects(self::once())->method('getPluginConfiguration')->with(
'CurrentExtensionName',
'CurrentPluginName'
)->willReturn($this->testPluginConfiguration);
$this->abstractConfigurationManager->expects(self::once())->method('getControllerConfiguration')->with(
'CurrentExtensionName',
'CurrentPluginName'
)->willReturn($this->testSwitchableControllerActions);
$this->abstractConfigurationManager->expects(self::once())->method('getContextSpecificFrameworkConfiguration')->willReturnCallback(function (
$a
) {
return $a;
});
$mergedConfiguration = $this->abstractConfigurationManager->getConfiguration();
$expectedResult = [
'MyExtension\\Controller\\Controller1' => [
'className' => 'MyExtension\\Controller\\Controller1',
'alias' => 'Controller1',
'actions' => ['newAction', 'action1']
],
'MyExtension\\Controller\\Controller2' => [
'className' => 'MyExtension\\Controller\\Controller2',
'alias' => 'Controller2',
'actions' => ['newAction2', 'action4', 'action5'],
'nonCacheableActions' => ['action4']
]
];
$actualResult = $mergedConfiguration['controllerConfiguration'];
self::assertEquals($expectedResult, $actualResult);
}
/**
* @test
*/
......
......@@ -259,7 +259,7 @@ class BackendConfigurationManagerTest extends UnitTestCase
$abstractConfigurationManager = $this->getAccessibleMock(
\TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager::class,
['overrideControllerConfigurationWithSwitchableControllerActions', 'getContextSpecificFrameworkConfiguration', 'getTypoScriptSetup', 'getPluginConfiguration', 'getControllerConfiguration'],
['getContextSpecificFrameworkConfiguration', 'getTypoScriptSetup', 'getPluginConfiguration', 'getControllerConfiguration'],
[],
'',
false
......@@ -290,7 +290,7 @@ class BackendConfigurationManagerTest extends UnitTestCase
$abstractConfigurationManager = $this->getAccessibleMock(
\TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager::class,
['overrideControllerConfigurationWithSwitchableControllerActions', 'getContextSpecificFrameworkConfiguration', 'getTypoScriptSetup', 'getPluginConfiguration', 'getControllerConfiguration', 'getQueryGenerator'],
['getContextSpecificFrameworkConfiguration', 'getTypoScriptSetup', 'getPluginConfiguration', 'getControllerConfiguration', 'getQueryGenerator'],
[],
'',
false
......@@ -315,7 +315,7 @@ class BackendConfigurationManagerTest extends UnitTestCase
$abstractConfigurationManager = $this->getAccessibleMock(
\TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager::class,
['overrideControllerConfigurationWithSwitchableControllerActions', 'getContextSpecificFrameworkConfiguration', 'getTypoScriptSetup', 'getPluginConfiguration', 'getControllerConfiguration'],
['getContextSpecificFrameworkConfiguration', 'getTypoScriptSetup', 'getPluginConfiguration', 'getControllerConfiguration'],
[],
'',
false
......@@ -336,7 +336,7 @@ class BackendConfigurationManagerTest extends UnitTestCase
$abstractConfigurationManager = $this->getAccessibleMock(
\TYPO3\CMS\Extbase\Configuration\BackendConfigurationManager::class,
['overrideControllerConfigurationWithSwitchableControllerActions', 'getContextSpecificFrameworkConfiguration', 'getTypoScriptSetup', 'getPluginConfiguration', 'getControllerConfiguration'],
['getContextSpecificFrameworkConfiguration', 'getTypoScriptSetup', 'getPluginConfiguration', 'getControllerConfiguration'],
[],
'',
false
......
......@@ -18,7 +18,6 @@ namespace TYPO3\CMS\Extbase\Tests\Unit\Configuration;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Extbase\Configuration\Exception\ParseErrorException;
use TYPO3\CMS\Extbase\Configuration\FrontendConfigurationManager;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\TestingFramework\Core\AccessibleObjectInterface;
......@@ -267,113 +266,6 @@ class FrontendConfigurationManagerTest extends UnitTestCase
self::assertEquals($expectedResult, $actualResult);
}
/**
* @test
*/
public function overrideControllerConfigurationWithSwitchableControllerActionsFromFlexFormReturnsUnchangedFrameworkConfigurationIfNoFlexFormConfigurationIsFound(
): void {
$frameworkConfiguration = [
'pluginName' => 'Pi1',
'extensionName' => 'SomeExtension',
'controllerConfiguration' => [
'Controller1' => [
'controller' => 'Controller1',
'actions' => 'action1 , action2'
],
'Controller2' => [
'controller' => 'Controller2',
'actions' => 'action2 , action1,action3',
'nonCacheableActions' => 'action2, action3'
]
]
];
$flexFormConfiguration = [];
$actualResult = $this->frontendConfigurationManager->_call(
'overrideControllerConfigurationWithSwitchableControllerActionsFromFlexForm',
$frameworkConfiguration,
$flexFormConfiguration
);
self::assertSame($frameworkConfiguration, $actualResult);
}
/**
* @test
*/
public function overrideControllerConfigurationWithSwitchableControllerActionsFromFlexFormMergesNonCacheableActions(): void
{
$frameworkConfiguration = [
'pluginName' => 'Pi1',
'extensionName' => 'SomeExtension',
'controllerConfiguration' => [
'MyExtension\\Controller\\Controller1' => [
'alias' => 'Controller1',
'actions' => ['action1 , action2']
],
'MyExtension\\Controller\\Controller2' => [
'alias' => 'Controller2',
'actions' => ['action2', 'action1', 'action3'],
'nonCacheableActions' => ['action2', 'action3']
]
]
];
$flexFormConfiguration = [
'switchableControllerActions' => 'Controller1 -> action2;\\MyExtension\\Controller\\Controller2->action3; Controller2->action1'
];
$expectedResult = [
'pluginName' => 'Pi1',
'extensionName' => 'SomeExtension',
'controllerConfiguration' => [
'MyExtension\\Controller\\Controller1' => [
'className' => 'MyExtension\\Controller\\Controller1',
'alias' => 'Controller1',