[FEATURE] Integrate Signal Slot Handling
authorAndreas Wolf <andreas.wolf@ikt-werk.de>
Wed, 8 Feb 2012 12:39:46 +0000 (13:39 +0100)
committerOliver Hader <oliver@typo3.org>
Wed, 8 Feb 2012 17:28:19 +0000 (18:28 +0100)
Integrate Signal Slot Handling to t3lib_SignalSlot_Dispatcher.

t3lib_SignalSlot_Dispatcher::getInstance()->connect();
t3lib_SignalSlot_Dispatcher::getInstance()->dispatch();
t3lib_SignalSlot_Dispatcher::getInstance()->getSlots();

Change-Id: I4478903503c99e4a556beec31e908efb6f41c7ae
Resolves: #33748
Reviewed-on: http://review.typo3.org/8907
Reviewed-by: Oliver Hader
Tested-by: Oliver Hader
t3lib/core_autoload.php
t3lib/signalslot/Dispatcher.php [new file with mode: 0644]
t3lib/signalslot/InvalidSlotException.php [new file with mode: 0644]
tests/t3lib/signalslot/DispatcherTest.php [new file with mode: 0644]

index 639ba0c..234f83d 100644 (file)
@@ -161,6 +161,8 @@ $t3libClasses = array(
        't3lib_scbase' => PATH_t3lib . 'class.t3lib_scbase.php',
        't3lib_search_livesearch' => PATH_t3lib . 'search/class.t3lib_search_livesearch.php',
        't3lib_search_livesearch_queryparser' => PATH_t3lib . 'search/class.t3lib_search_livesearch_queryParser.php',
+       't3lib_signalslot_dispatcher' => PATH_t3lib . 'signalslot/Dispatcher.php',
+       't3lib_signalslot_invalidslotexception' => PATH_t3lib . 'signalslot/InvalidSlotException.php',
        't3lib_singleton' => PATH_t3lib . 'interfaces/interface.t3lib_singleton.php',
        't3lib_softrefproc' => PATH_t3lib . 'class.t3lib_softrefproc.php',
        't3lib_spritemanager' => PATH_t3lib . 'class.t3lib_spritemanager.php',
diff --git a/t3lib/signalslot/Dispatcher.php b/t3lib/signalslot/Dispatcher.php
new file mode 100644 (file)
index 0000000..4a27a56
--- /dev/null
@@ -0,0 +1,144 @@
+<?php
+/***************************************************************
+ * Copyright notice
+ *
+ * (c) 2011-2012 Andreas Wolf <andreas.wolf@ikt-werk.de>
+ * All rights reserved
+ *
+ * This script is part of the TYPO3 project. The TYPO3 project is
+ * free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * The GNU General Public License can be found at
+ * http://www.gnu.org/copyleft/gpl.html.
+ * A copy is found in the textfile GPL.txt and important notices to the license
+ * from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ * This script is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+
+/**
+ * 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.
+ *
+ * @api
+ */
+class t3lib_SignalSlot_Dispatcher implements t3lib_Singleton {
+
+       /**
+        * 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 = array();
+
+       /**
+        * Gets a singleton instance of this class.
+        *
+        * @return t3lib_SignalSlot_Dispatcher
+        */
+       public static function getInstance() {
+               return t3lib_div::makeInstance('t3lib_SignalSlot_Dispatcher');
+       }
+
+       /**
+        * 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 boolean $passSignalInformation If set to TRUE, the last argument passed to the slot will be information about the signal (EmitterClassName::signalName)
+        * @return void
+        * @api
+        */
+       public function connect($signalClassName, $signalName, $slotClassNameOrObject, $slotMethodName = '', $passSignalInformation = TRUE) {
+               $class = NULL;
+               $object = NULL;
+
+               if (strpos($signalName, 'emit') === 0) {
+                       $possibleSignalName = lcfirst(substr($signalName, strlen('emit')));
+                       throw new \InvalidArgumentException('The signal should not be connected with the method name ("' . $signalName . '"). Try "' . $possibleSignalName . '" for the signal name.', 1314016630);
+               }
+
+               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;
+               }
+
+               $this->slots[$signalClassName][$signalName][] = array(
+                       'class' => $class,
+                       'method' => $method,
+                       'object' => $object,
+                       'passSignalInformation' => ($passSignalInformation === TRUE)
+               );
+       }
+
+       /**
+        * 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 void
+        * @throws t3lib_SignalSlot_InvalidSlotException if the slot is not valid
+        * @api
+        */
+       public function dispatch($signalClassName, $signalName, array $signalArguments = array()) {
+               if (!isset($this->slots[$signalClassName][$signalName])) {
+                       return;
+               }
+
+               foreach ($this->slots[$signalClassName][$signalName] as $slotInformation) {
+                       if (isset($slotInformation['object'])) {
+                               $object = $slotInformation['object'];
+                       } else {
+                               if (!class_exists($slotInformation['class'], TRUE)) {
+                                       throw new t3lib_SignalSlot_InvalidSlotException('The given class "' . $slotInformation['class'] . '" does not exist.', 1319136841);
+                               }
+                               $object = t3lib_div::makeInstance($slotInformation['class']);
+                       }
+                       if ($slotInformation['passSignalInformation'] === TRUE) {
+                               $signalArguments[] = $signalClassName . '::' . $signalName;
+                       }
+                       if (!method_exists($object, $slotInformation['method'])) {
+                               throw new t3lib_SignalSlot_InvalidSlotException('The slot method ' . get_class($object) . '->' . $slotInformation['method'] . '() does not exist.', 1245673368);
+                       }
+                       call_user_func_array(array($object, $slotInformation['method']), $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
+        * @api
+        */
+       public function getSlots($signalClassName, $signalName) {
+               return (isset($this->slots[$signalClassName][$signalName])) ? $this->slots[$signalClassName][$signalName] : array();
+       }
+}
+
+if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/signalslot/Dispatcher.php'])) {
+       include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['t3lib/signalslot/Dispatcher.php']);
+}
+
+?>
\ No newline at end of file
diff --git a/t3lib/signalslot/InvalidSlotException.php b/t3lib/signalslot/InvalidSlotException.php
new file mode 100644 (file)
index 0000000..fffc573
--- /dev/null
@@ -0,0 +1,36 @@
+<?php
+/***************************************************************
+ * Copyright notice
+ *
+ * (c) 2011-2012 Andreas Wolf <andreas.wolf@ikt-werk.de>
+ * All rights reserved
+ *
+ * This script is part of the TYPO3 project. The TYPO3 project is
+ * free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * The GNU General Public License can be found at
+ * http://www.gnu.org/copyleft/gpl.html.
+ * A copy is found in the textfile GPL.txt and important notices to the license
+ * from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ * This script is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+
+/**
+ * Exception to be thrown if a slot to handle a signal is
+ * invalid and thus cannot be called to be processed.
+ */
+class t3lib_SignalSlot_InvalidSlotException extends t3lib_exception {
+
+}
+?>
\ No newline at end of file
diff --git a/tests/t3lib/signalslot/DispatcherTest.php b/tests/t3lib/signalslot/DispatcherTest.php
new file mode 100644 (file)
index 0000000..8e9f66d
--- /dev/null
@@ -0,0 +1,180 @@
+<?php
+/***************************************************************
+ * Copyright notice
+ *
+ * (c) 2011-2012 Andreas Wolf <andreas.wolf@ikt-werk.de>
+ * All rights reserved
+ *
+ * This script is part of the TYPO3 project. The TYPO3 project is
+ * free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * The GNU General Public License can be found at
+ * http://www.gnu.org/copyleft/gpl.html.
+ * A copy is found in the textfile GPL.txt and important notices to the license
+ * from the author is found in LICENSE.txt distributed with these scripts.
+ *
+ *
+ * This script is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * This copyright notice MUST APPEAR in all copies of the script!
+ ***************************************************************/
+
+
+/**
+ * Testcase for the Signal/Slot dispatcher
+ *
+ * @author Andreas Wolf <andreas.wolf@ikt-werk.de>
+ * @package TYPO3
+ * @subpackage t3lib
+ */
+class t3lib_SignalSlot_DispatcherTest extends Tx_Phpunit_TestCase {
+
+       /**
+        * @var t3lib_SignalSlot_Dispatcher
+        */
+       private $fixture;
+
+       /**
+        * Sets up this test case.
+        */
+       public function setUp() {
+               $this->fixture = new t3lib_SignalSlot_Dispatcher();
+       }
+
+       /**
+        * Cleans up this test case.
+        */
+       public function tearDown() {
+               unset($this->fixture);
+       }
+
+       /**
+        * @test
+        */
+       public function connectAllowsForConnectingASlotWithASignal() {
+               $mockSignal = $this->getMock('ClassA', array('emitSomeSignal'));
+               $mockSlot = $this->getMock('ClassB', array('someSlotMethod'));
+
+               $this->fixture->connect(get_class($mockSignal), 'someSignal', get_class($mockSlot), 'someSlotMethod', FALSE);
+
+               $expectedSlots = array(
+                       array('class' => get_class($mockSlot), 'method' => 'someSlotMethod', 'object' => NULL, 'passSignalInformation' => FALSE)
+               );
+               $this->assertSame($expectedSlots, $this->fixture->getSlots(get_class($mockSignal), 'someSignal'));
+       }
+
+       /**
+        * @test
+        */
+       public function connectAlsoAcceptsObjectsInPlaceOfTheClassName() {
+               $mockSignal = $this->getMock('ClassA', array('emitSomeSignal'));
+               $mockSlot = $this->getMock('ClassB', array('someSlotMethod'));
+
+               $this->fixture->connect(get_class($mockSignal), 'someSignal', $mockSlot, 'someSlotMethod', FALSE);
+
+               $expectedSlots = array(
+                       array('class' => NULL, 'method' => 'someSlotMethod', 'object' => $mockSlot, 'passSignalInformation' => FALSE)
+               );
+               $this->assertSame($expectedSlots, $this->fixture->getSlots(get_class($mockSignal), 'someSignal'));
+       }
+
+       /**
+        * @test
+        */
+       public function connectAlsoAcceptsClosuresActingAsASlot() {
+               $mockSignal = $this->getMock('ClassA', array('emitSomeSignal'));
+               $mockSlot = function() { };
+
+               $this->fixture->connect(get_class($mockSignal), 'someSignal', $mockSlot, 'foo', FALSE);
+
+               $expectedSlots = array(
+                       array('class' => NULL, 'method' => '__invoke', 'object' => $mockSlot, 'passSignalInformation' => FALSE)
+               );
+               $this->assertSame($expectedSlots, $this->fixture->getSlots(get_class($mockSignal), 'someSignal'));
+       }
+
+       /**
+        * @test
+        */
+       public function dispatchPassesTheSignalArgumentsToTheSlotMethod() {
+               $arguments = array();
+               $mockSlot = function() use (&$arguments) {
+                       $arguments = func_get_args();
+               };
+
+               $this->fixture->connect('Foo', 'bar', $mockSlot, NULL, FALSE);
+
+               $this->fixture->dispatch('Foo', 'bar', array('foo' => 'bar', 'baz' => 'quux'));
+               $this->assertSame(array('bar', 'quux'), $arguments);
+       }
+
+       /**
+        * @test
+        */
+       public function dispatchCreatesSlotInstanceIfOnlyAClassNameWasSpecified() {
+               $slotClassName = 'Mock_' . md5(uniqid(mt_rand(), TRUE));
+               eval ('class ' . $slotClassName . ' { static $arguments; function slot($foo, $baz) { self::$arguments = array($foo, $baz); } }');
+
+               $this->fixture->connect('Foo', 'bar', $slotClassName, 'slot', FALSE);
+
+               $this->fixture->dispatch('Foo', 'bar', array('foo' => 'bar', 'baz' => 'quux'));
+               $this->assertSame(array('bar', 'quux'), $slotClassName::$arguments);
+       }
+
+       /**
+        * @test
+        * @expectedException t3lib_SignalSlot_InvalidSlotException
+        */
+       public function dispatchThrowsAnExceptionIfTheSpecifiedClassOfASlotIsUnknown() {
+               $this->fixture->connect('Foo', 'bar', 'NonExistingClassName', 'slot', FALSE);
+               $this->fixture->dispatch('Foo', 'bar', array());
+       }
+
+       /**
+        * @test
+        * @expectedException t3lib_SignalSlot_InvalidSlotException
+        */
+       public function dispatchThrowsAnExceptionIfTheSpecifiedSlotMethodDoesNotExist() {
+               $slotClassName = 'Mock_' . md5(uniqid(mt_rand(), TRUE));
+               eval ('class ' . $slotClassName . ' { function slot($foo, $baz) { $this->arguments = array($foo, $baz); } }');
+               $mockSlot = new $slotClassName();
+
+               $this->fixture->connect('Foo', 'bar', $slotClassName, 'unknownMethodName', TRUE);
+
+               $this->fixture->dispatch('Foo', 'bar', array('foo' => 'bar', 'baz' => 'quux'));
+               $this->assertSame($mockSlot->arguments, array('bar', 'quux'));
+       }
+
+       /**
+        * @test
+        */
+       public function dispatchPassesArgumentContainingSlotInformationLastIfTheConnectionStatesSo() {
+               $arguments = array();
+               $mockSlot = function() use (&$arguments) {
+                       $arguments = func_get_args();
+               };
+
+               $this->fixture->connect('SignalClassName', 'methodName', $mockSlot, NULL, TRUE);
+
+               $this->fixture->dispatch('SignalClassName', 'methodName', array('foo' => 'bar', 'baz' => 'quux'));
+               $this->assertSame(array('bar', 'quux', 'SignalClassName::methodName'), $arguments);
+       }
+
+       /**
+        * @test
+        * @expectedException \InvalidArgumentException
+        */
+       public function connectWithSignalNameStartingWithEmitShouldNotBeAllowed() {
+               $mockSignal = $this->getMock('ClassA', array('emitSomeSignal'));
+               $mockSlot = $this->getMock('ClassB', array('someSlotMethod'));
+
+               $this->fixture->connect(get_class($mockSignal), 'emitSomeSignal', get_class($mockSlot), 'someSlotMethod', FALSE);
+       }
+}
+?>
\ No newline at end of file