Simple PubSub framework, for now a very close port of http://weierophinney.net/matthe...
authorChuck Hagenbuch <chuck@horde.org>
Wed, 5 Jan 2011 14:57:33 +0000 (09:57 -0500)
committerChuck Hagenbuch <chuck@horde.org>
Thu, 6 Jan 2011 15:09:41 +0000 (10:09 -0500)
framework/PubSub/lib/Horde/PubSub.php [new file with mode: 0644]
framework/PubSub/lib/Horde/PubSub/Handle.php [new file with mode: 0644]
framework/PubSub/lib/Horde/PubSub/Provider.php [new file with mode: 0644]
framework/PubSub/test/Horde/PubSub/AllTests.php [new file with mode: 0644]
framework/PubSub/test/Horde/PubSub/HandleTest.php [new file with mode: 0644]
framework/PubSub/test/Horde/PubSub/ProviderTest.php [new file with mode: 0644]
framework/PubSub/test/Horde/PubSub/PubSubTest.php [new file with mode: 0644]

diff --git a/framework/PubSub/lib/Horde/PubSub.php b/framework/PubSub/lib/Horde/PubSub.php
new file mode 100644 (file)
index 0000000..d15ab56
--- /dev/null
@@ -0,0 +1,122 @@
+<?php
+/**
+ * Publish-Subscribe system based on Phly_PubSub
+ * (http://weierophinney.net/matthew/archives/199-A-Simple-PHP-Publish-Subscribe-System.html)
+ *
+ * @category  Horde
+ * @package   PubSub
+ * @copyright Copyright (C) 2008 - Present, Matthew Weier O'Phinney
+ * @author    Matthew Weier O'Phinney <mweierophinney@gmail.com>
+ * @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 (file)
index 0000000..e943a02
--- /dev/null
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Publish-Subscribe system based on Phly_PubSub
+ * (http://weierophinney.net/matthew/archives/199-A-Simple-PHP-Publish-Subscribe-System.html)
+ *
+ * @category  Horde
+ * @package   PubSub
+ * @copyright Copyright (C) 2008 - Present, Matthew Weier O'Phinney
+ * @author    Matthew Weier O'Phinney <mweierophinney@gmail.com>
+ * @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 (file)
index 0000000..83f3d0a
--- /dev/null
@@ -0,0 +1,125 @@
+<?php
+/**
+ * Publish-Subscribe system based on Phly_PubSub
+ * (http://weierophinney.net/matthew/archives/199-A-Simple-PHP-Publish-Subscribe-System.html)
+ *
+ * @category  Horde
+ * @package   PubSub
+ * @copyright Copyright (C) 2008 - Present, Matthew Weier O'Phinney
+ * @author    Matthew Weier O'Phinney <mweierophinney@gmail.com>
+ * @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 (file)
index 0000000..df6f773
--- /dev/null
@@ -0,0 +1,38 @@
+<?php
+/**
+ * All tests for the Horde_PubSub:: package.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  PubSub
+ * @author   Gunnar Wrobel <wrobel@pardus.de>
+ * @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 (file)
index 0000000..2c0a4eb
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+/**
+ * @category   Horde
+ * @package    PubSub
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    New BSD {@link http://www.opensource.org/licenses/bsd-license.php}
+ */
+class Horde_PubSub_HandleTest extends Horde_Test_Case
+{
+    public function setUp()
+    {
+        if (isset($this->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 (file)
index 0000000..570629b
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+/**
+ * @category   Horde
+ * @package    PubSub
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    New BSD {@link http://www.opensource.org/licenses/bsd-license.php}
+ */
+class Horde_PubSub_ProviderTest extends Horde_Test_Case
+{
+    public function setUp()
+    {
+        if (isset($this->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 (file)
index 0000000..1ecc831
--- /dev/null
@@ -0,0 +1,98 @@
+<?php
+/**
+ * @category   Horde
+ * @package    PubSub
+ * @subpackage UnitTests
+ * @copyright  Copyright (c) 2005-2008 Zend Technologies USA Inc. (http://www.zend.com)
+ * @license    New BSD {@link http://www.opensource.org/licenses/bsd-license.php}
+ */
+class Horde_PubSub_PubSubTest extends Horde_Test_Case
+{
+    public function setUp()
+    {
+        if (isset($this->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;
+    }
+}