Commit 9bee4df4 authored by Christian Kuhn's avatar Christian Kuhn Committed by Benni Mack
Browse files

[!!!][TASK] Remove extbase SignalSlot Dispatcher

Resolves: #96204
Related: #90625
Related: #92996
Releases: main
Change-Id: If76ac793f46bedba70fe325d1329d73cbd6a09fa
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/72464

Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 47ec2ee4
......@@ -32,6 +32,9 @@ The following PHP classes that have previously been marked as deprecated for v11
- :php:`\TYPO3\CMS\Extbase\Mvc\View\AbstractView`
- :php:`\TYPO3\CMS\Extbase\Mvc\View\EmptyView`
- :php:`\TYPO3\CMS\Extbase\Service\EnvironmentService`
- :php:`\TYPO3\CMS\Extbase\SignalSlot\Dispatcher`
- :php:`\TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException`
- :php:`\TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException`
- :php:`\TYPO3\CMS\Frontend\ContentObject\EditPanelContentObject`
The following PHP interfaces that have previously been marked as deprecated for v11 and were now removed:
......
......@@ -50,7 +50,6 @@ use TYPO3\CMS\Extbase\Property\PropertyMapper;
use TYPO3\CMS\Extbase\Reflection\ReflectionService;
use TYPO3\CMS\Extbase\Security\Cryptography\HashService;
use TYPO3\CMS\Extbase\Service\ExtensionService;
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
use TYPO3\CMS\Extbase\Validation\Validator\ConjunctionValidator;
use TYPO3\CMS\Extbase\Validation\Validator\ValidatorInterface;
use TYPO3\CMS\Extbase\Validation\ValidatorResolver;
......@@ -140,12 +139,6 @@ abstract class ActionController implements ControllerInterface
*/
protected $request;
/**
* @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
* @internal only to be used within Extbase, not part of TYPO3 Core API.
*/
protected $signalSlotDispatcher;
/**
* @var ObjectManagerInterface
* @internal only to be used within Extbase, not part of TYPO3 Core API.
......@@ -232,16 +225,6 @@ abstract class ActionController implements ControllerInterface
$this->arguments = GeneralUtility::makeInstance(Arguments::class);
}
/**
* @param \TYPO3\CMS\Extbase\SignalSlot\Dispatcher $signalSlotDispatcher
* @internal only to be used within Extbase, not part of TYPO3 Core API.
* @deprecated since v11, will be removed in v12
*/
public function injectSignalSlotDispatcher(Dispatcher $signalSlotDispatcher)
{
$this->signalSlotDispatcher = $signalSlotDispatcher;
}
/**
* @param \TYPO3\CMS\Extbase\Validation\ValidatorResolver $validatorResolver
* @internal only to be used within Extbase, not part of TYPO3 Core API.
......
......@@ -104,11 +104,6 @@ class Backend implements BackendInterface, SingletonInterface
*/
protected $configurationManager;
/**
* @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher
*/
protected $signalSlotDispatcher;
/**
* @var EventDispatcherInterface
*/
......
......@@ -19,7 +19,6 @@ namespace TYPO3\CMS\Extbase;
use Psr\Container\ContainerInterface;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Package\AbstractServiceProvider;
use TYPO3\CMS\Core\Package\Cache\PackageDependentCacheIdentifier;
use TYPO3\CMS\Core\Resource\ResourceFactory;
......@@ -42,8 +41,6 @@ class ServiceProvider extends AbstractServiceProvider
Object\Container\Container::class => [ static::class, 'getObjectContainer' ],
// @deprecated since v11, will be removed in v12
Object\ObjectManager::class => [ static::class, 'getObjectManager' ],
// @deprecated since v11, will be removed in v12
SignalSlot\Dispatcher::class => [ static::class, 'getSignalSlotDispatcher' ],
Configuration\BackendConfigurationManager::class => [ static::class, 'getBackendConfigurationManager' ],
Configuration\ConfigurationManager::class => [ static::class, 'getConfigurationManager' ],
Reflection\ReflectionService::class => [ static::class, 'getReflectionService' ],
......@@ -69,15 +66,6 @@ class ServiceProvider extends AbstractServiceProvider
return self::new($container, Object\ObjectManager::class, [$container, $container->get(Object\Container\Container::class)]);
}
/**
* @deprecated since v11, will be removed in v12
*/
public static function getSignalSlotDispatcher(ContainerInterface $container): SignalSlot\Dispatcher
{
$logger = $container->get(LogManager::class)->getLogger(SignalSlot\Dispatcher::class);
return self::new($container, SignalSlot\Dispatcher::class, [$container->get(Object\ObjectManager::class), $logger]);
}
public static function getBackendConfigurationManager(ContainerInterface $container): Configuration\BackendConfigurationManager
{
return self::new($container, Configuration\BackendConfigurationManager::class, [
......
<?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\Extbase\SignalSlot;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
use TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException;
use TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException;
/**
* A dispatcher which dispatches signals by calling its registered slot methods
* and passing them the method arguments which were originally passed to the
* signal method.
*
* @deprecated will be removed in TYPO3 v12.0. Use PSR-14 based events and EventDispatcherInterface instead.
*/
class Dispatcher implements SingletonInterface
{
/**
* @var ObjectManagerInterface
*/
protected $objectManager;
/**
* Information about all slots connected a certain signal.
* Indexed by [$signalClassName][$signalMethodName] and then numeric with an
* array of information about the slot
*
* @var array
*/
protected $slots = [];
/**
* @var LoggerInterface
*/
protected $logger;
/**
* @param ObjectManagerInterface $objectManager
* @param LoggerInterface $logger
*/
public function __construct(ObjectManagerInterface $objectManager, LoggerInterface $logger)
{
$this->objectManager = $objectManager;
$this->logger = $logger;
}
/**
* Connects a signal with a slot.
* One slot can be connected with multiple signals by calling this method multiple times.
*
* @param string $signalClassName Name of the class containing the signal
* @param string $signalName Name of the signal
* @param mixed $slotClassNameOrObject Name of the class containing the slot or the instantiated class or a Closure object
* @param string $slotMethodName Name of the method to be used as a slot. If $slotClassNameOrObject is a Closure object, this parameter is ignored
* @param bool $passSignalInformation If set to TRUE, the last argument passed to the slot will be information about the signal (EmitterClassName::signalName)
* @throws \InvalidArgumentException
*/
public function connect(string $signalClassName, string $signalName, $slotClassNameOrObject, string $slotMethodName = '', bool $passSignalInformation = true): void
{
$class = null;
$object = null;
if (is_object($slotClassNameOrObject)) {
$object = $slotClassNameOrObject;
$method = $slotClassNameOrObject instanceof \Closure ? '__invoke' : $slotMethodName;
} else {
if ($slotMethodName === '') {
throw new \InvalidArgumentException('The slot method name must not be empty (except for closures).', 1229531659);
}
$class = $slotClassNameOrObject;
$method = $slotMethodName;
}
$slot = [
'class' => $class,
'method' => $method,
'object' => $object,
'passSignalInformation' => $passSignalInformation === true,
];
// The in_array() comparision needs to be strict to avoid potential issues
// with complex objects being registered as slot.
if (!is_array($this->slots[$signalClassName][$signalName] ?? false) || !in_array($slot, $this->slots[$signalClassName][$signalName], true)) {
$this->slots[$signalClassName][$signalName][] = $slot;
}
}
/**
* Dispatches a signal by calling the registered Slot methods
*
* @param string $signalClassName Name of the class containing the signal
* @param string $signalName Name of the signal
* @param array $signalArguments arguments passed to the signal method
* @return mixed
* @throws Exception\InvalidSlotException if the slot is not valid
* @throws Exception\InvalidSlotReturnException if a slot returns invalid arguments (too few or return value is not an array)
*/
public function dispatch(string $signalClassName, string $signalName, array $signalArguments = [])
{
$this->logger->debug('Triggered signal {signal_class} {signal_name} ', [
'signal_class' => $signalClassName,
'signal_name' => $signalName,
'signalArguments' => $signalArguments,
]);
if (!isset($this->slots[$signalClassName][$signalName])) {
return $signalArguments;
}
foreach ($this->slots[$signalClassName][$signalName] as $slotInformation) {
if (isset($slotInformation['object'])) {
$object = $slotInformation['object'];
} else {
if (!class_exists($slotInformation['class'])) {
throw new InvalidSlotException('The given class "' . $slotInformation['class'] . '" is not a registered object.', 1245673367);
}
$object = $this->objectManager->get($slotInformation['class']);
}
$method = (string)($slotInformation['method'] ?? '');
$callable = [$object, $method];
if (!is_object($object) || !is_callable($callable)) {
throw new InvalidSlotException('The slot method ' . get_class($object) . '->' . $method . '() does not exist.', 1245673368);
}
$preparedSlotArguments = $signalArguments;
if ($slotInformation['passSignalInformation'] === true) {
$preparedSlotArguments[] = $signalClassName . '::' . $signalName;
}
$slotReturn = $callable(...$preparedSlotArguments);
if ($slotReturn) {
if (!is_array($slotReturn)) {
throw new InvalidSlotReturnException('The slot method ' . get_class($object) . '->' . $method . '()\'s return value is of an not allowed type ('
. gettype($slotReturn) . ').', 1376683067);
}
if (count($slotReturn) !== count($signalArguments)) {
throw new InvalidSlotReturnException('The slot method ' . get_class($object) . '->' . $method . '() returned a different number ('
. count($slotReturn) . ') of arguments, than it received (' . count($signalArguments) . ').', 1376683066);
}
$signalArguments = $slotReturn;
}
}
return $signalArguments;
}
/**
* Returns all slots which are connected with the given signal
*
* @param string $signalClassName Name of the class containing the signal
* @param string $signalName Name of the signal
* @return array An array of arrays with slot information
*/
public function getSlots(string $signalClassName, string $signalName): array
{
return $this->slots[$signalClassName][$signalName] ?? [];
}
}
<?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\Extbase\SignalSlot\Exception;
use TYPO3\CMS\Extbase\Object\Exception;
/**
* "Invalid Slot" Exception
*
* @deprecated since v11, will be removed in v12. Drop along with ObjectManager / Container.
*/
class InvalidSlotException extends Exception
{
}
<?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\Extbase\SignalSlot\Exception;
use TYPO3\CMS\Extbase\Object\Exception;
/**
* "Invalid Slot Return" Exception
*
* @deprecated since v11, will be removed in v12. Drop along with ObjectManager / Container.
*/
class InvalidSlotReturnException extends Exception
{
}
<?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\Extbase\Tests\UnitDeprecated\SignalSlot;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
use TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotException;
use TYPO3\CMS\Extbase\SignalSlot\Exception\InvalidSlotReturnException;
use TYPO3\CMS\Extbase\Tests\Fixture\SlotFixture;
use TYPO3\CMS\Extbase\Tests\UnitDeprecated\SignalSlot\Fixtures\OnlyClassNameSpecifiedFixture;
use TYPO3\CMS\Extbase\Tests\UnitDeprecated\SignalSlot\Fixtures\SlotMethodDoesNotExistFixture;
use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
/**
* Test case
*/
class DispatcherTest extends UnitTestCase
{
use ProphecyTrait;
/**
* @var bool Reset singletons created by subject
*/
protected bool $resetSingletonInstances = true;
/**
* @var ObjectManagerInterface|ObjectProphecy
*/
protected $objectManagerProphecy;
/**
* @var \TYPO3\CMS\Extbase\SignalSlot\Dispatcher|\PHPUnit\Framework\MockObject\MockObject|\TYPO3\TestingFramework\Core\AccessibleObjectInterface
*/
protected $signalSlotDispatcher;
protected function setUp(): void
{
parent::setUp();
$this->objectManagerProphecy = $this->prophesize(ObjectManagerInterface::class);
$this->signalSlotDispatcher = new Dispatcher(
$this->objectManagerProphecy->reveal(),
$this->prophesize(LoggerInterface::class)->reveal()
);
}
/**
* @test
*/
public function connectAllowsForConnectingASlotWithASignal(): void
{
$mockSignal = $this->getMockBuilder(\stdClass::class)
->addMethods(['emitSomeSignal'])
->getMock();
$mockSlot = $this->getMockBuilder(\stdClass::class)
->addMethods(['someSlotMethod'])
->getMock();
$this->signalSlotDispatcher->connect(get_class($mockSignal), 'emitSomeSignal', get_class($mockSlot), 'someSlotMethod', true);
$expectedSlots = [
['class' => get_class($mockSlot), 'method' => 'someSlotMethod', 'object' => null, 'passSignalInformation' => true],
];
self::assertSame($expectedSlots, $this->signalSlotDispatcher->getSlots(get_class($mockSignal), 'emitSomeSignal'));
}
/**
* @test
*/
public function connectAlsoAcceptsObjectsInPlaceOfTheClassName(): void
{
$mockSignal = $this->getMockBuilder(\stdClass::class)
->addMethods(['emitSomeSignal'])
->getMock();
$mockSlot = $this->getMockBuilder(\stdClass::class)
->addMethods(['someSlotMethod'])
->getMock();
$this->signalSlotDispatcher->connect(get_class($mockSignal), 'emitSomeSignal', $mockSlot, 'someSlotMethod', true);
$expectedSlots = [
['class' => null, 'method' => 'someSlotMethod', 'object' => $mockSlot, 'passSignalInformation' => true],
];
self::assertSame($expectedSlots, $this->signalSlotDispatcher->getSlots(get_class($mockSignal), 'emitSomeSignal'));
}
/**
* @test
*/
public function connectAlsoAcceptsClosuresActingAsASlot(): void
{
$mockSignal = $this->getMockBuilder(\stdClass::class)
->addMethods(['emitSomeSignal'])
->getMock();
$mockSlot = static function () {
};
$this->signalSlotDispatcher->connect(get_class($mockSignal), 'emitSomeSignal', $mockSlot, 'foo', true);
$expectedSlots = [
['class' => null, 'method' => '__invoke', 'object' => $mockSlot, 'passSignalInformation' => true],
];
self::assertSame($expectedSlots, $this->signalSlotDispatcher->getSlots(get_class($mockSignal), 'emitSomeSignal'));
}
/**
* @test
*/
public function dispatchPassesTheSignalArgumentsToTheSlotMethod(): void
{
$arguments = [];
$mockSlot = static function () use (&$arguments) {
$arguments = func_get_args();
};
$this->signalSlotDispatcher->connect('Foo', 'bar', $mockSlot, '', false);
$this->signalSlotDispatcher->dispatch('Foo', 'bar', ['bar', 'quux']);
self::assertSame(['bar', 'quux'], $arguments);
}
/**
* @test
*/
public function dispatchRetrievesSlotInstanceFromTheObjectManagerIfOnlyAClassNameWasSpecified(): void
{
$slotClassName = OnlyClassNameSpecifiedFixture::class;
$mockSlot = new OnlyClassNameSpecifiedFixture();
$this->objectManagerProphecy->get($slotClassName)->willReturn($mockSlot);
$this->signalSlotDispatcher->connect('Foo', 'emitBar', $slotClassName, 'slot', false);
$this->signalSlotDispatcher->dispatch('Foo', 'emitBar', ['bar', 'quux']);
self::assertSame($mockSlot->arguments, ['bar', 'quux']);
}
/**
* @test
*/
public function dispatchHandsOverArgumentsReturnedByAFormerSlot(): void
{
$firstMockSlot = $this->createMock(SlotFixture::class);
$firstMockSlot->expects(self::once())
->method('slot')
->willReturnCallback(
static function ($foo, $baz) {
return ['modified_' . $foo, 'modified_' . $baz];
}
);
$secondMockSlot = $this->createMock(SlotFixture::class);
$secondMockSlot->expects(self::once())
->method('slot')
->with('modified_bar', 'modified_quux');
$this->signalSlotDispatcher->connect('Foo', 'emitBar', $firstMockSlot, 'slot', false);
$this->signalSlotDispatcher->connect('Foo', 'emitBar', $secondMockSlot, 'slot', false);
$this->signalSlotDispatcher->dispatch('Foo', 'emitBar', ['bar', 'quux']);
}
/**
* @test
*/
public function dispatchHandsOverArgumentsReturnedByAFormerSlotWithoutInterferingWithSignalSlotInformation(): void
{
$firstMockSlot = $this->createMock(SlotFixture::class);
$firstMockSlot->expects(self::once())
->method('slot')
->willReturnCallback(
static function ($foo, $baz) {
return ['modified_' . $foo, 'modified_' . $baz];
}
);
$secondMockSlot = $this->createMock(SlotFixture::class);
$secondMockSlot->expects(self::once())
->method('slot')
->with('modified_bar', 'modified_quux');
$this->signalSlotDispatcher->connect('Foo', 'emitBar', $firstMockSlot, 'slot');
$this->signalSlotDispatcher->connect('Foo', 'emitBar', $secondMockSlot, 'slot');
$this->signalSlotDispatcher->dispatch('Foo', 'emitBar', ['bar', 'quux']);
}
/**
* @test
*/
public function dispatchHandsOverFormerArgumentsIfPreviousSlotDoesNotReturnAnything(): void
{
$firstMockSlot = $this->createMock(SlotFixture::class);
$firstMockSlot->expects(self::once())
->method('slot')
->willReturnCallback(
static function ($foo, $baz) {
return ['modified_' . $foo, 'modified_' . $baz];
}
);
$secondMockSlot = $this->createMock(SlotFixture::class);
$secondMockSlot->expects(self::once())
->method('slot');
$thirdMockSlot = $this->createMock(SlotFixture::class);
$thirdMockSlot->expects(self::once())
->method('slot')
->with('modified_bar', 'modified_quux');
$this->signalSlotDispatcher->connect('Foo', 'emitBar', $firstMockSlot, 'slot');
$this->signalSlotDispatcher->connect('Foo', 'emitBar', $secondMockSlot, 'slot');
$this->signalSlotDispatcher->connect('Foo', 'emitBar', $thirdMockSlot, 'slot');
$this->signalSlotDispatcher->dispatch('Foo', 'emitBar', ['bar', 'quux']);
}
/**
* @test
*/
public function dispatchThrowsAnExceptionIfTheSlotReturnsNonArray(): void
{
$this->expectException(InvalidSlotReturnException::class);
$this->expectExceptionCode(1376683067);
$mockSlot = $this->createMock(SlotFixture::class);
$mockSlot->expects(self::once())
->method('slot')
->willReturnCallback(
static function () {
return 'string';
}
);
$this->signalSlotDispatcher->connect('Foo', 'emitBar', $mockSlot, 'slot', false);
$this->signalSlotDispatcher->dispatch('Foo', 'emitBar', ['bar', 'quux']);
}
/**
* @test
*/
public function dispatchThrowsAnExceptionIfTheSlotReturnsDifferentNumberOfItems(): void
{
$this->expectException(InvalidSlotReturnException::class);
$this->expectExceptionCode(1376683066);
$mockSlot = $this->createMock(SlotFixture::class);
$mockSlot->expects(self::once())
->method('slot')
->willReturnCallback(