From 83de2a61a12f5382a3f131acf237bb6c1b4d63ee Mon Sep 17 00:00:00 2001 From: Chuck Hagenbuch Date: Wed, 5 Jan 2011 09:57:33 -0500 Subject: [PATCH] Simple PubSub framework, for now a very close port of http://weierophinney.net/matthew/archives/199-A-Simple-PHP-Publish-Subscribe-System.html --- framework/PubSub/lib/Horde/PubSub.php | 122 ++++++++++++++++++++ framework/PubSub/lib/Horde/PubSub/Handle.php | 81 +++++++++++++ framework/PubSub/lib/Horde/PubSub/Provider.php | 125 +++++++++++++++++++++ framework/PubSub/test/Horde/PubSub/AllTests.php | 38 +++++++ framework/PubSub/test/Horde/PubSub/HandleTest.php | 48 ++++++++ .../PubSub/test/Horde/PubSub/ProviderTest.php | 85 ++++++++++++++ framework/PubSub/test/Horde/PubSub/PubSubTest.php | 98 ++++++++++++++++ 7 files changed, 597 insertions(+) create mode 100644 framework/PubSub/lib/Horde/PubSub.php create mode 100644 framework/PubSub/lib/Horde/PubSub/Handle.php create mode 100644 framework/PubSub/lib/Horde/PubSub/Provider.php create mode 100644 framework/PubSub/test/Horde/PubSub/AllTests.php create mode 100644 framework/PubSub/test/Horde/PubSub/HandleTest.php create mode 100644 framework/PubSub/test/Horde/PubSub/ProviderTest.php create mode 100644 framework/PubSub/test/Horde/PubSub/PubSubTest.php diff --git a/framework/PubSub/lib/Horde/PubSub.php b/framework/PubSub/lib/Horde/PubSub.php new file mode 100644 index 000000000..d15ab564f --- /dev/null +++ b/framework/PubSub/lib/Horde/PubSub.php @@ -0,0 +1,122 @@ + + * @license New BSD {@link http://www.opensource.org/licenses/bsd-license.php} + */ + +/** + * Publish-Subscribe system + * + * @category Horde + * @package PubSub + */ +class Horde_PubSub +{ + /** + * Subscribed topics and their handles + */ + protected static $_topics = array(); + + /** + * Publish to all handlers for a given topic + * + * @param string $topic + * @param mixed $args All arguments besides the topic are passed as arguments to the handler + * @return void + */ + public static function publish($topic, $args = null) + { + if (empty(self::$_topics[$topic])) { + return; + } + $args = func_get_args(); + array_shift($args); + foreach (self::$_topics[$topic] as $handle) { + $handle->call($args); + } + } + + /** + * Subscribe to a topic + * + * @param string $topic + * @param string|object $context Function name, class name, or object instance + * @param null|string $handler If $context is a class or object, the name of the method to call + * @return Horde_PubSub_Handle Pub-Sub handle (to allow later unsubscribe) + */ + public static function subscribe($topic, $context, $handler = null) + { + if (empty(self::$_topics[$topic])) { + self::$_topics[$topic] = array(); + } + $handle = new Horde_PubSub_Handle($topic, $context, $handler); + if (in_array($handle, self::$_topics[$topic])) { + $index = array_search($handle, self::$_topics[$topic]); + return self::$_topics[$topic][$index]; + } + self::$_topics[$topic][] = $handle; + return $handle; + } + + /** + * Unsubscribe a handler from a topic + * + * @param Horde_PubSub_Handle $handle + * @return bool Returns true if topic and handle found, and unsubscribed; returns false if either topic or handle not found + */ + public static function unsubscribe(Horde_PubSub_Handle $handle) + { + $topic = $handle->getTopic(); + if (empty(self::$_topics[$topic])) { + return false; + } + if (false === ($index = array_search($handle, self::$_topics[$topic]))) { + return false; + } + unset(self::$_topics[$topic][$index]); + return true; + } + + /** + * Retrieve all registered topics + * + * @return array + */ + public static function getTopics() + { + return array_keys(self::$_topics); + } + + /** + * Retrieve all handlers for a given topic + * + * @param string $topic + * @return array Array of Horde_PubSub_Handle objects + */ + public static function getSubscribedHandles($topic) + { + if (empty(self::$_topics[$topic])) { + return array(); + } + return self::$_topics[$topic]; + } + + /** + * Clear all handlers for a given topic + * + * @param string $topic + * @return void + */ + public static function clearHandles($topic) + { + if (!empty(self::$_topics[$topic])) { + unset(self::$_topics[$topic]); + } + } +} diff --git a/framework/PubSub/lib/Horde/PubSub/Handle.php b/framework/PubSub/lib/Horde/PubSub/Handle.php new file mode 100644 index 000000000..e943a0233 --- /dev/null +++ b/framework/PubSub/lib/Horde/PubSub/Handle.php @@ -0,0 +1,81 @@ + + * @license New BSD {@link http://www.opensource.org/licenses/bsd-license.php} + */ + +/** + * Publish-Subscribe handler: unique handle subscribed to a given topic. + * + * @category Horde + * @package PubSub + */ +class Horde_PubSub_Handle +{ + /** + * PHP callback to invoke + * @var string|array + */ + protected $_callback; + + /** + * Topic to which this handle is subscribed + * @var string + */ + protected $_topic; + + /** + * Constructor + * + * @param string $topic Topic to which handle is subscribed + * @param string|object $context Function name, class name, or object instance + * @param string|null $handler Method name, if $context is a class or object + */ + public function __construct($topic, $context, $handler = null) + { + $this->_topic = $topic; + + if (null === $handler) { + $this->_callback = $context; + } else { + $this->_callback = array($context, $handler); + } + } + + /** + * Get topic to which handle is subscribed + * + * @return string + */ + public function getTopic() + { + return $this->_topic; + } + + /** + * Retrieve registered callback + * + * @return string|array + */ + public function getCallback() + { + return $this->_callback; + } + + /** + * Invoke handler + * + * @param array $args Arguments to pass to callback + * @return void + */ + public function call(array $args) + { + call_user_func_array($this->getCallback(), $args); + } +} diff --git a/framework/PubSub/lib/Horde/PubSub/Provider.php b/framework/PubSub/lib/Horde/PubSub/Provider.php new file mode 100644 index 000000000..83f3d0aeb --- /dev/null +++ b/framework/PubSub/lib/Horde/PubSub/Provider.php @@ -0,0 +1,125 @@ + + * @license New BSD {@link http://www.opensource.org/licenses/bsd-license.php} + */ + +/** + * Publish-Subscribe provider + * + * Use Horde_PubSub_Provider when you want to create a per-instance plugin + * system for your objects. + * + * @category Horde + * @package PubSub + */ +class Horde_PubSub_Provider +{ + /** + * Subscribed topics and their handles + */ + protected $_topics = array(); + + /** + * Publish to all handlers for a given topic + * + * @param string $topic + * @param mixed $args All arguments besides the topic are passed as arguments to the handler + * @return void + */ + public function publish($topic, $args = null) + { + if (empty($this->_topics[$topic])) { + return; + } + $args = func_get_args(); + array_shift($args); + foreach ($this->_topics[$topic] as $handle) { + $handle->call($args); + } + } + + /** + * Subscribe to a topic + * + * @param string $topic + * @param string|object $context Function name, class name, or object instance + * @param null|string $handler If $context is a class or object, the name of the method to call + * @return Horde_PubSub_Handle Pub-Sub handle (to allow later unsubscribe) + */ + public function subscribe($topic, $context, $handler = null) + { + if (empty($this->_topics[$topic])) { + $this->_topics[$topic] = array(); + } + $handle = new Horde_PubSub_Handle($topic, $context, $handler); + if (in_array($handle, $this->_topics[$topic])) { + $index = array_search($handle, $this->_topics[$topic]); + return $this->_topics[$topic][$index]; + } + $this->_topics[$topic][] = $handle; + return $handle; + } + + /** + * Unsubscribe a handler from a topic + * + * @param Horde_PubSub_Handle $handle + * @return bool Returns true if topic and handle found, and unsubscribed; returns false if either topic or handle not found + */ + public function unsubscribe(Horde_PubSub_Handle $handle) + { + $topic = $handle->getTopic(); + if (empty($this->_topics[$topic])) { + return false; + } + if (false === ($index = array_search($handle, $this->_topics[$topic]))) { + return false; + } + unset($this->_topics[$topic][$index]); + return true; + } + + /** + * Retrieve all registered topics + * + * @return array + */ + public function getTopics() + { + return array_keys($this->_topics); + } + + /** + * Retrieve all handlers for a given topic + * + * @param string $topic + * @return array Array of Horde_PubSub_Handle objects + */ + public function getSubscribedHandles($topic) + { + if (empty($this->_topics[$topic])) { + return array(); + } + return $this->_topics[$topic]; + } + + /** + * Clear all handlers for a given topic + * + * @param string $topic + * @return void + */ + public function clearHandles($topic) + { + if (!empty($this->_topics[$topic])) { + unset($this->_topics[$topic]); + } + } +} diff --git a/framework/PubSub/test/Horde/PubSub/AllTests.php b/framework/PubSub/test/Horde/PubSub/AllTests.php new file mode 100644 index 000000000..df6f773ee --- /dev/null +++ b/framework/PubSub/test/Horde/PubSub/AllTests.php @@ -0,0 +1,38 @@ + + * @license http://www.fsf.org/copyleft/lgpl.html LGPL + * @link http://pear.horde.org/index.php?package=PubSub + */ + +/** + * Define the main method + */ +if (!defined('PHPUnit_MAIN_METHOD')) { + define('PHPUnit_MAIN_METHOD', 'Horde_PubSub_AllTests::main'); +} + +/** + * Prepare the test setup. + */ +require_once 'Horde/Test/AllTests.php'; + +/** + * @package Horde_PubSub + * @subpackage UnitTests + */ +class Horde_PubSub_AllTests extends Horde_Test_AllTests +{ +} + +Horde_PubSub_AllTests::init('Horde_PubSub', __FILE__); + +if (PHPUnit_MAIN_METHOD == 'Horde_PubSub_AllTests::main') { + Horde_PubSub_AllTests::main(); +} diff --git a/framework/PubSub/test/Horde/PubSub/HandleTest.php b/framework/PubSub/test/Horde/PubSub/HandleTest.php new file mode 100644 index 000000000..2c0a4eb0c --- /dev/null +++ b/framework/PubSub/test/Horde/PubSub/HandleTest.php @@ -0,0 +1,48 @@ +args)) { + unset($this->args); + } + } + + public function testGetTopicShouldReturnTopic() + { + $handle = new Horde_PubSub_Handle('foo', 'bar'); + $this->assertEquals('foo', $handle->getTopic()); + } + + public function testCallbackShouldBeStringIfNoHandlerPassedToConstructor() + { + $handle = new Horde_PubSub_Handle('foo', 'bar'); + $this->assertSame('bar', $handle->getCallback()); + } + + public function testCallbackShouldBeArrayIfHandlerPassedToConstructor() + { + $handle = new Horde_PubSub_Handle('foo', 'bar', 'baz'); + $this->assertSame(array('bar', 'baz'), $handle->getCallback()); + } + + public function testCallShouldInvokeCallbackWithSuppliedArguments() + { + $handle = new Horde_PubSub_Handle('foo', $this, 'handleCall'); + $args = array('foo', 'bar', 'baz'); + $handle->call($args); + $this->assertSame($args, $this->args); + } + + public function handleCall() + { + $this->args = func_get_args(); + } +} diff --git a/framework/PubSub/test/Horde/PubSub/ProviderTest.php b/framework/PubSub/test/Horde/PubSub/ProviderTest.php new file mode 100644 index 000000000..570629bf0 --- /dev/null +++ b/framework/PubSub/test/Horde/PubSub/ProviderTest.php @@ -0,0 +1,85 @@ +message)) { + unset($this->message); + } + $this->provider = new Horde_PubSub_Provider; + } + + public function testSubscribeShouldReturnHandle() + { + $handle = $this->provider->subscribe('test', $this, __METHOD__); + $this->assertTrue($handle instanceof Horde_PubSub_Handle); + } + + public function testSubscribeShouldAddHandleToTopic() + { + $handle = $this->provider->subscribe('test', $this, __METHOD__); + $handles = $this->provider->getSubscribedHandles('test'); + $this->assertEquals(1, count($handles)); + $this->assertContains($handle, $handles); + } + + public function testSubscribeShouldAddTopicIfItDoesNotExist() + { + $topics = $this->provider->getTopics(); + $this->assertTrue(empty($topics), var_export($topics, 1)); + $handle = $this->provider->subscribe('test', $this, __METHOD__); + $topics = $this->provider->getTopics(); + $this->assertFalse(empty($topics)); + $this->assertContains('test', $topics); + } + + public function testUnsubscribeShouldRemoveHandleFromTopic() + { + $handle = $this->provider->subscribe('test', $this, __METHOD__); + $handles = $this->provider->getSubscribedHandles('test'); + $this->assertContains($handle, $handles); + $this->provider->unsubscribe($handle); + $handles = $this->provider->getSubscribedHandles('test'); + $this->assertNotContains($handle, $handles); + } + + public function testUnsubscribeShouldReturnFalseIfTopicDoesNotExist() + { + $handle = $this->provider->subscribe('test', $this, __METHOD__); + $this->provider->clearHandles('test'); + $this->assertFalse($this->provider->unsubscribe($handle)); + } + + public function testUnsubscribeShouldReturnFalseIfHandleDoesNotExist() + { + $handle1 = $this->provider->subscribe('test', $this, __METHOD__); + $this->provider->clearHandles('test'); + $handle2 = $this->provider->subscribe('test', $this, 'handleTestTopic'); + $this->assertFalse($this->provider->unsubscribe($handle1)); + } + + public function testRetrievingSubscribedHandlesShouldReturnEmptyArrayWhenTopicDoesNotExist() + { + $handles = $this->provider->getSubscribedHandles('test'); + $this->assertTrue(empty($handles)); + } + + public function testPublishShouldNotifySubscribedHandlers() + { + $handle = $this->provider->subscribe('test', $this, 'handleTestTopic'); + $this->provider->publish('test', 'test message'); + $this->assertEquals('test message', $this->message); + } + + public function handleTestTopic($message) + { + $this->message = $message; + } +} diff --git a/framework/PubSub/test/Horde/PubSub/PubSubTest.php b/framework/PubSub/test/Horde/PubSub/PubSubTest.php new file mode 100644 index 000000000..1ecc83118 --- /dev/null +++ b/framework/PubSub/test/Horde/PubSub/PubSubTest.php @@ -0,0 +1,98 @@ +message)) { + unset($this->message); + } + $this->clearAllTopics(); + } + + public function tearDown() + { + $this->clearAllTopics(); + } + + public function clearAllTopics() + { + $topics = Horde_PubSub::getTopics(); + foreach ($topics as $topic) { + Horde_PubSub::clearHandles($topic); + } + } + + public function testSubscribeShouldReturnHandle() + { + $handle = Horde_PubSub::subscribe('test', $this, __METHOD__); + $this->assertTrue($handle instanceof Horde_PubSub_Handle); + } + + public function testSubscribeShouldAddHandleToTopic() + { + $handle = Horde_PubSub::subscribe('test', $this, __METHOD__); + $handles = Horde_PubSub::getSubscribedHandles('test'); + $this->assertEquals(1, count($handles)); + $this->assertContains($handle, $handles); + } + + public function testSubscribeShouldAddTopicIfItDoesNotExist() + { + $topics = Horde_PubSub::getTopics(); + $this->assertTrue(empty($topics), var_export($topics, 1)); + $handle = Horde_PubSub::subscribe('test', $this, __METHOD__); + $topics = Horde_PubSub::getTopics(); + $this->assertFalse(empty($topics)); + $this->assertContains('test', $topics); + } + + public function testUnsubscribeShouldRemoveHandleFromTopic() + { + $handle = Horde_PubSub::subscribe('test', $this, __METHOD__); + $handles = Horde_PubSub::getSubscribedHandles('test'); + $this->assertContains($handle, $handles); + Horde_PubSub::unsubscribe($handle); + $handles = Horde_PubSub::getSubscribedHandles('test'); + $this->assertNotContains($handle, $handles); + } + + public function testUnsubscribeShouldReturnFalseIfTopicDoesNotExist() + { + $handle = Horde_PubSub::subscribe('test', $this, __METHOD__); + Horde_PubSub::clearHandles('test'); + $this->assertFalse(Horde_PubSub::unsubscribe($handle)); + } + + public function testUnsubscribeShouldReturnFalseIfHandleDoesNotExist() + { + $handle1 = Horde_PubSub::subscribe('test', $this, __METHOD__); + Horde_PubSub::clearHandles('test'); + $handle2 = Horde_PubSub::subscribe('test', $this, 'handleTestTopic'); + $this->assertFalse(Horde_PubSub::unsubscribe($handle1)); + } + + public function testRetrievingSubscribedHandlesShouldReturnEmptyArrayWhenTopicDoesNotExist() + { + $handles = Horde_PubSub::getSubscribedHandles('test'); + $this->assertTrue(empty($handles)); + } + + public function testPublishShouldNotifySubscribedHandlers() + { + $handle = Horde_PubSub::subscribe('test', $this, 'handleTestTopic'); + Horde_PubSub::publish('test', 'test message'); + $this->assertEquals('test message', $this->message); + } + + public function handleTestTopic($message) + { + $this->message = $message; + } +} -- 2.11.0