initial checkin
authorMichael Wallner <mike@php.net>
Mon, 18 Aug 2014 11:56:45 +0000 (13:56 +0200)
committerMichael Wallner <mike@php.net>
Mon, 18 Aug 2014 11:56:45 +0000 (13:56 +0200)
14 files changed:
.gitignore [new file with mode: 0644]
.travis.yml [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README.md [new file with mode: 0644]
composer.json [new file with mode: 0644]
lib/hikke/Event.php [new file with mode: 0644]
lib/hikke/Event/Bucket.php [new file with mode: 0644]
lib/hikke/Event/Prioritized.php [new file with mode: 0644]
lib/hikke/Event/Priority.php [new file with mode: 0644]
lib/hikke/Event/Proxy.php [new file with mode: 0644]
lib/hikke/Event/Storage.php [new file with mode: 0644]
tests/hikke/EventTest.php [new file with mode: 0644]
tests/hikke/ProxyTest.php [new file with mode: 0644]
tests/hikke/TestObserver.php [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..aab7eee
--- /dev/null
@@ -0,0 +1,2 @@
+nbproject
+vendor
diff --git a/.travis.yml b/.travis.yml
new file mode 100644 (file)
index 0000000..d1c113c
--- /dev/null
@@ -0,0 +1,11 @@
+language: php
+
+php:
+    - 5.4
+    - 5.5
+    - 5.6
+    - hhvm
+
+before_script: composer install
+    
+script: phpunit --coverage-text --colors tests
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..0ec221c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2014, Michael Wallner <mike@php.net>.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without 
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright notice, 
+      this list of conditions and the following disclaimer.
+    * Redistributions in binary form must reproduce the above copyright 
+      notice, this list of conditions and the following disclaimer in the 
+      documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..50674c2
--- /dev/null
+++ b/README.md
@@ -0,0 +1,69 @@
+# hikke\Event
+
+Prioritized event observers. [![Build Status](https://travis-ci.org/m6w6/hikke.svg)](https://travis-ci.org/m6w6/hikke)
+
+Example:
+
+```php
+<?php
+
+use hikke\Event;
+
+class Observer implements \SplObserver {
+       private $name;
+       function __construct($name) {
+               $this->name = $name;
+       }
+       function update(\SplSubject $e) {
+               echo "Observer '{$this->name}' notified by '$e' ({$e->getPriority()})\n";
+       }
+       function proxiedMethodCall($arg) {
+               $this->name .= $arg;
+       }
+}
+
+$event = new Event("my_event");
+$event->attach(new Observer("o1"), 1);
+$event->attach(new Observer("o2"), 2);
+$event->notify();
+
+?>
+```
+
+Output:
+
+```
+Observer 'o1' notified by 'my_event' (0)
+Observer 'o2' notified by 'my_event' (0)
+```
+
+Another example:
+
+```php
+<?php
+
+$proxy = new Event\Proxy;
+$proxy->ev1 = 0;
+$proxy->ev2 = 1;
+$proxy->attach(new Observer("o1"), null, 1);
+$proxy->attach(new Observer("o2"), null, 0);
+$proxy->attach(new Observer("o3"), "ev2");
+$proxy->ev3->attach(new Observer("o2"));
+
+$proxy->proxiedMethodCall("-proxy");
+$proxy->notify();
+?>
+```
+
+Output:
+
+```
+Observer 'o2-proxy' notified by 'default' (0.001)
+Observer 'o1-proxy' notified by 'default' (0.001)
+Observer 'o2-proxy' notified by 'ev1' (0.002)
+Observer 'o1-proxy' notified by 'ev1' (0.002)
+Observer 'o2-proxy' notified by 'ev3' (0.004)
+Observer 'o2-proxy' notified by 'ev2' (1.003)
+Observer 'o3-proxy' notified by 'ev2' (1.003)
+Observer 'o1-proxy' notified by 'ev2' (1.003)
+```
diff --git a/composer.json b/composer.json
new file mode 100644 (file)
index 0000000..756f70d
--- /dev/null
@@ -0,0 +1,19 @@
+{
+    "name": "m6w6/hikke",
+    "type": "library",
+    "description": "Prioritized Observers",
+    "keywords": ["hikke", "prioritized", "priority", "observer", "event", "spl", "hiccup"],
+    "homepage": "http://github.com/m6w6/hikke",
+    "license": "BSD-2-Clause",
+    "authors": [
+        {
+            "name": "Michael Wallner",
+            "email": "mike@php.net"
+        }
+    ],
+    "autoload": {
+        "psr-0": {
+            "hikke\\Event": "lib"
+        }
+    }
+}
diff --git a/lib/hikke/Event.php b/lib/hikke/Event.php
new file mode 100644 (file)
index 0000000..50c2c82
--- /dev/null
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * Hikke
+ * 
+ * @author Michael Wallner <mike@php.net>
+ */
+namespace hikke;
+
+use hikke\Event\Priority;
+use hikke\Event\Storage;
+
+/**
+ * Event
+ * 
+ * @package hikke\Event
+ */
+class Event extends Priority implements \SplSubject, \IteratorAggregate, \Countable
+{
+       /**
+        * @var string
+        */
+       private $name;
+       
+       /**
+        * @var float
+        */
+       private $priority;
+       
+       /**
+        * @var hikke\Event\Storage
+        */
+       private $storage;
+       
+       /**
+        * Create a new event
+        * @param string $name custom event name
+        * @param int|float $priority
+        */
+       public function __construct($name, $priority = 0) {
+               $this->name = $name;
+               $this->priority = $priority;
+               $this->storage = new Storage;
+       }
+
+       /**
+        * Returns the event name
+        * @return string
+        */
+       public function __toString() {
+               return (string) $this->name;
+       }
+       
+       /**
+        * Get priority of this event
+        * @return float
+        */
+       public function getPriority() {
+               return $this->priority;
+       }
+       
+       /**
+        * Get the event name
+        * @return string
+        */
+       public function getName() {
+               return $this->name;
+       }
+       
+       /**
+        * Update priority of this event
+        * @param float $priority
+        */
+       public function setPriority($priority) {
+               $this->priority = $priority;
+       }
+       
+       /**
+        * Attach an event observer
+        * @param \SplObserver $observer
+        * @param float $priority
+        * @return \hikke\Event
+        */
+       public function attach(\SplObserver $observer, $priority = 0) {
+               $this->storage->insert($observer, $priority);
+               return $this;
+       }
+       
+       /**
+        * Detach an already attached event observer
+        * @param \SplObserver $observer
+        * @return bool whether the observer was attached
+        */
+       public function detach(\SplObserver $observer) {
+               return $this->storage->delete($observer);
+       }
+       
+       /**
+        * Notify attached observers
+        * @param \SplSubject $origin
+        */
+       public function notify(\SplSubject $origin = null) {
+               foreach ($this->storage as $observer) {
+                       $observer->update($origin ?: $this, $this);
+               }
+       }
+       
+       /**
+        * @ignore
+        */
+       public function count() {
+               return count($this->storage);
+       }
+       
+       /**
+        * @ignore
+        */
+       public function getIterator() {
+               return $this->storage;
+       }
+}
diff --git a/lib/hikke/Event/Bucket.php b/lib/hikke/Event/Bucket.php
new file mode 100644 (file)
index 0000000..3c34a53
--- /dev/null
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * Hikke
+ * 
+ * @author Michael Wallner <mike@php.net>
+ */
+namespace hikke\Event;
+
+use hikke\Event\Priority;
+
+/**
+ * Bucket
+ * 
+ * @package hikke\Event
+ */
+class Bucket extends Priority
+{
+       /**
+        * @var object
+        */
+       private $object;
+       
+       /**
+        * @var float
+        */
+       private $priority;
+       
+       /**
+        * @var string
+        */
+       private $hash;
+       
+       /**
+        * Create a new storage bucket
+        * @param object $object
+        * @param float $priority
+        */
+       public function __construct($object, $priority) {
+               $this->object = $object;
+               $this->priority = $priority;
+       }
+       
+       /**
+        * Get the priority of the object in the bucket
+        * @return float
+        */
+       public function getPriority() {
+               return $this->priority;
+       }
+       
+       /**
+        * Get the object contained in this bucket
+        * @return object
+        */
+       public function getObject() {
+               return $this->object;
+       }
+       
+       /**
+        * Get the hash of the object contained in this bucket
+        * @return string
+        */
+       public function getHash() {
+               if (!isset($this->hash)) {
+                       $this->hash = spl_object_hash($this->object);
+               }
+               return $this->hash;
+       }
+}
diff --git a/lib/hikke/Event/Prioritized.php b/lib/hikke/Event/Prioritized.php
new file mode 100644 (file)
index 0000000..dc4dac7
--- /dev/null
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * Hikke
+ * 
+ * @author Michael Wallner <mike@php.net>
+ */
+namespace hikke\Event;
+
+/**
+ * Prioritized
+ * 
+ * @package hikke\Event
+ */
+interface Prioritized
+{
+       /**
+        * Get the object's priority, the closer the value to 0 the higher the priority
+        * @return int|float
+        */
+       public function getPriority();
+}
diff --git a/lib/hikke/Event/Priority.php b/lib/hikke/Event/Priority.php
new file mode 100644 (file)
index 0000000..b9402d3
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * Hikke
+ * 
+ * @author Michael Wallner <mike@php.net>
+ */
+namespace hikke\Event;
+
+/**
+ * Priority
+ * 
+ * @package hikke\Event
+ */
+abstract class Priority implements Prioritized
+{
+       /**
+        * Compare an instance of a prioritzed object ot another
+        * @param \hikke\Event\Prioritized $other
+        * @return int
+        */
+       public function compareTo(Prioritized $other) {
+               return static::compare($this, $other);
+       }
+       
+       /**
+        * Comparator
+        * @param \hikke\Event\Prioritized $a
+        * @param \hikke\Event\Prioritized $b
+        * @return int
+        */
+       public static function compare(Prioritized $a, Prioritized $b) {
+               if ($a->getPriority() < $b->getPriority()) {
+                       return -1;
+               } elseif ($b->getPriority() < $a->getPriority()) {
+                       return 1;
+               } else {
+                       // @codeCoverageIgnoreStart
+                       return 0;
+                       // @codeCoverageIgnoreEnd
+               }
+       }
+       
+}
diff --git a/lib/hikke/Event/Proxy.php b/lib/hikke/Event/Proxy.php
new file mode 100644 (file)
index 0000000..f04c0bb
--- /dev/null
@@ -0,0 +1,195 @@
+<?php
+
+/**
+ * Hikke
+ * 
+ * @author Michael Wallner <mike@php.net>
+ */
+namespace hikke\Event;
+
+use hikke\Event;
+use hikke\Event\Storage;
+
+/**
+ * Proxy
+ * 
+ * @package hikke\Event
+ */
+class Proxy implements \SplSubject, \SplObserver, \IteratorAggregate, \Countable
+{
+       /**
+        * @var array
+        */
+       private $events = array();
+       
+       /**
+        * @var \hikke\Event\Storage
+        */
+       private $storage;
+       
+       public function __construct(array $events = ["default"]) {
+               $this->storage = new Storage;
+               foreach ((array) $events as $priority => $event) {
+                       $this->insert($event, $priority);
+               }
+       }
+       
+       /**
+        * Notify all attached observers passing along $origin
+        * @param \SplSubject $origin
+        */
+       public function update(\SplSubject $origin) {
+               $this->notify($origin);
+       }
+       
+       /**
+        * Apply a cvallback ot a specific or all events
+        * @param string $event
+        * @param callable $apply
+        */
+       public function apply($event, callable $apply) {
+               if (strlen($event)) {
+                       $apply($this->events[$event]);
+               } else {
+                       foreach ($this->storage as $ev) {
+                               $apply($ev);
+                       }
+               }
+       }
+       
+       /**
+        * Notify all attached observers to a specific or all events passing alomg $origin
+        * @param object $origin
+        * @param string $event
+        */
+       public function notify($origin = null, $event = null) {
+               $this->apply($event, function($ev) use($origin) {
+                       $ev->notify($origin);
+               });
+       }
+       
+       /**
+        * Attach an observer to a specfiv or all events
+        * @param \SplObserver $observer
+        * @param string $event
+        * @param int|float $priority
+        * @return \hikke\Event\Proxy self
+        */
+       public function attach(\SplObserver $observer, $event = null, $priority = 0) {
+               $this->apply($event, function($ev) use($observer, $priority) {
+                       $ev->attach($observer, $priority);
+               });
+               return $this;
+       }
+       
+       /**
+        * Detach an observer from all or a specific event
+        * @param \SplObserver $observer
+        * @param string $event
+        * @return \hikke\Event\Proxy self
+        */
+       public function detach(\SplObserver $observer, $event = null) {
+               $this->apply($event, function($ev) use($observer) {
+                       $ev->detach($observer);
+               });
+               return $this;
+       }
+       
+       /**
+        * Insert a new event type
+        * @param string $name
+        * @param int|float $priority
+        */
+       private function insert($name, $priority = 0) {
+               if ($priority instanceof Event) {
+                       /* assignement in the form:
+                        * $proxy->foo = new Event("foo", 123);
+                        */
+                       $event = $priority;
+                       $priority = $event->getPriority();
+                       
+                       /* sanity check */
+                       if ($name !== $event->getName()) {
+                               throw new \UnexpectedValueException(
+                                       sprintf("The event names differ: '%s' <> '%s'",
+                                               $name, $event->getName()));
+                       }
+               } elseif (isset($this->events[$name])) {
+                       throw new \UnexpectedValueException(
+                               sprintf("The event name '%s' is already in use", $name));
+               } else {
+                       $event = new Event($name);
+               }
+               
+               $bucket = $this->storage->insert($event, $priority);
+               $event->setPriority($bucket->getPriority());
+               $this->events[$name] = $event;
+       }
+       
+       /**
+        * @ignore
+        */
+       public function __call($method, $args) {
+               $observers = new \SplObjectStorage;
+               $this->apply(null, function($ev) use($observers) {
+                       foreach ($ev as $observer) {
+                               if (!$observers->contains($observer)) {
+                                       $observers->attach($observer);
+                               }
+                       }
+               });
+               foreach ($observers as $observer) {
+                       if (is_callable(array($observer, $method))) {
+                               call_user_func_array(array($observer, $method), $args);
+                       }
+               }
+       }
+       
+       /**
+        * @ignore
+        */
+       public function __get($event) {
+               if (!isset($this->events[$event])) {
+                       $this->insert($event);
+               }
+               return $this->events[$event];
+       }
+       
+       /**
+        * @ignore
+        */
+       public function __set($event, $priority) {
+               $this->insert($event, $priority);
+       }
+       
+       /**
+        * @ignore
+        */
+       public function __isset($event) {
+               return isset($this->events[$event]);
+       }
+       
+       /**
+        * @ignore
+        */
+       public function __unset($event) {
+               if (isset($this->events[$event])) {
+                       $this->storage->delete($this->events[$event]);
+                       unset($this->events[$event]);
+               }
+       }
+       
+       /**
+        * @ignore
+        */
+       public function getIterator() {
+               return $this->storage;
+       }
+       
+       /**
+        * @ignore
+        */
+       public function count() {
+               return count($this->storage);
+       }
+}
diff --git a/lib/hikke/Event/Storage.php b/lib/hikke/Event/Storage.php
new file mode 100644 (file)
index 0000000..33a94c3
--- /dev/null
@@ -0,0 +1,100 @@
+<?php
+
+/**
+ * Hikke
+ * 
+ * @author Michael Wallner <mike@php.net>
+ */
+namespace hikke\Event;
+
+use hikke\Event\Bucket;
+
+/**
+ * Storage
+ * 
+ * @package hikke\Event
+ */
+class Storage implements \Iterator, \Countable
+{
+       /**
+        * @var int
+        */
+       private $sequence = 1;
+       
+       /**
+        * @var array
+        */
+       private $buckets = array();
+       
+       /**
+        * @var array
+        */
+       private $iterator;
+       
+       /**
+        * @param object $object
+        * @param int|float $priority
+        * @return \hikke\Event\Bucket
+        */
+       public function insert($object, $priority = 0) {
+               $priority += $this->sequence++ / 1000;
+               $bucket = new Bucket($object, $priority);
+               $this->buckets[$bucket->getHash()] = $bucket;
+               return $bucket;
+       }
+       
+       /**
+        * @param object $object
+        * @return bool whether the storage container the object
+        */
+       public function delete($object) {
+               $hash = spl_object_hash($object);
+               if (($found = isset($this->buckets[$hash]))) {
+                       unset($this->buckets[$hash]);
+               }
+               return $found;
+       }
+       
+       /**
+        * @ignore
+        */
+       public function count() {
+               return count($this->buckets);
+       }
+       
+       /**
+        * @ignore
+        */
+       public function rewind() {
+               $this->iterator = $this->buckets;
+               uasort($this->iterator, ["hikke\\Event\\Priority", "compare"]);
+       }
+       
+       /**
+        * @ignore
+        */
+       public function valid() {
+               return NULL !== key($this->iterator);
+       }
+       
+       /**
+        * @ignore
+        */
+       public function next() {
+               next($this->iterator);
+       }
+       
+       /**
+        * @ignore
+        */
+       public function key() {
+               return current($this->iterator)->getPriority();
+       }
+       
+       /**
+        * @ignore
+        */
+       public function current() {
+               return current($this->iterator)->getObject();
+       }
+}
diff --git a/tests/hikke/EventTest.php b/tests/hikke/EventTest.php
new file mode 100644 (file)
index 0000000..34a4b17
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+
+namespace hikke;
+
+require_once __DIR__."/../../vendor/autoload.php";
+require_once __DIR__."/TestObserver.php";
+
+class EventTest extends \PHPUnit_Framework_TestCase
+{
+       function setUp() {
+               TestObserver::reset();
+       }
+       
+       function testBasic() {
+               $first = new Event("first", 1);
+               $second = new Event("second", 2);
+               
+               $this->assertEquals(-1, $first->compareTo($second));
+               $this->assertEquals(1, $second->compareTo($first));
+               
+               $this->assertEquals("first", $first->getName());
+               $this->assertEquals("second", $second->getName());
+       }
+       
+       function testAttachDetach() {
+               $event = new Event("event");
+               $observer = new TestObserver("observer");
+               $this->assertEquals($event, $event->attach($observer));
+               $this->assertTrue($event->detach($observer));
+               $this->assertFalse($event->detach($observer));
+       }
+       
+       function testNotify() {
+               $event = new Event("event");
+               $event->attach(new TestObserver("first"));
+               $event->attach(new TestObserver("second"));
+               $event->attach(new TestObserver("third"));
+               $event->notify();
+               $this->assertEquals(
+                       "first: event\n".
+                       "second: event\n".
+                       "third: event\n", TestObserver::$logs);
+       }
+       
+       function testPrioritizedNotify() {
+               $event = new Event("event");
+               $event->attach(new TestObserver("first"),3);
+               $event->attach(new TestObserver("second"),2);
+               $event->attach(new TestObserver("third"),1);
+               $event->notify();
+               $this->assertEquals(
+                       "third: event\n".
+                       "second: event\n".
+                       "first: event\n", TestObserver::$logs);
+       }
+}
diff --git a/tests/hikke/ProxyTest.php b/tests/hikke/ProxyTest.php
new file mode 100644 (file)
index 0000000..b93bb9e
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+
+namespace hikke;
+
+require_once __DIR__."/../../vendor/autoload.php";
+require_once __DIR__."/TestObserver.php";
+
+class ProxyTest extends \PHPUnit_Framework_TestCase
+{
+       function setUp() {
+               TestObserver::reset();
+       }
+       
+       function testBasic() {
+               $proxy = new Event\Proxy(["first","second"]);
+               $this->assertInstanceOf("hikke\\Event", $proxy->first);
+               $this->assertInstanceOf("hikke\\Event", $proxy->second);
+       }
+       
+       function testAutoInsert() {
+               $proxy = new Event\Proxy;
+               $this->assertFalse(isset($proxy->event));
+               $this->assertInstanceOf("hikke\\Event", $proxy->event);
+               $this->assertTrue(isset($proxy->event));
+               unset($proxy->event);
+               $this->assertFalse(isset($proxy->event));
+       }
+       
+       function testExplicitInsert() {
+               $proxy = new Event\Proxy;
+               $this->assertFalse(isset($proxy->event));
+               $proxy->event = 1.23;
+               $this->assertTrue(isset($proxy->event));
+               $this->assertInstanceOf("hikke\Event", $proxy->event);
+               $this->assertEquals(1.23, round($proxy->event->getPriority(),2));
+       }
+       
+       function testProxy() {
+               $proxy = new Event\Proxy(["one", "two", "three"]);
+               $observer = new TestObserver("observer");
+               $proxy->attach($observer);
+               $proxy->notify();
+               $this->assertEquals(
+                       "observer: one\n".
+                       "observer: two\n".
+                       "observer: three\n",
+                       TestObserver::$logs);
+       }
+       
+       function testProxyProxy() {
+               $proxied = new Event\Proxy(["one", "two", "three"]);
+               $observer = new TestObserver("observer");
+               $proxied->attach($observer);
+               $proxy = new Event\Proxy;
+               $proxy->attach($proxied);
+               $proxy->notify();
+               $this->assertEquals(
+                       "observer: default:one\n".
+                       "observer: default:two\n".
+                       "observer: default:three\n",
+                       TestObserver::$logs);
+       }
+       
+       function testProxyCall() {
+               $proxy = new Event\Proxy;
+               $observer = new TestObserver("call");
+               $proxy->attach($observer);
+               $arg = (object) ["data" => null];
+               $proxy->callMe($arg);
+               $this->assertEquals("hikke\\TestObserver::callMe\n", $arg->data);
+       }
+       
+       function testIterator() {
+               $proxy = new Event\Proxy;
+               $proxy->attach(new TestObserver("o1"));
+               $proxy->attach(new TestObserver("o2"));
+               $proxy->attach(new TestObserver("o3"));
+               $string = "";
+               foreach ($proxy as $event) {
+                       $string .= $event;
+                       foreach ($event as $observer) {
+                               $string .= ":$observer";
+                       }
+               }
+               $this->assertEquals("default:o1:o2:o3", $string);
+       }
+       
+       function testCountAndDetach() {
+               $rcount = function($proxy) {
+                       return array_sum(array_map(function($e) {
+                               return count($e);
+                       }, iterator_to_array($proxy)));
+               };
+               $proxy = new Event\Proxy(["e1","e2"]);
+               $observer1 = new TestObserver("o1");
+               $proxy->attach($observer1);
+               $this->assertEquals(2, count($proxy));
+               $observer2 = new TestObserver("o2");
+               $proxy->attach($observer2);
+               $this->assertEquals(4, $rcount($proxy));
+               $proxy->detach($observer1);
+               $this->assertEquals(2, $rcount($proxy));
+               $proxy->detach($observer2, "e2");
+               $this->assertEquals(1, $rcount($proxy));
+       }
+       
+       function testAssignTwice() {
+               $proxy = new Event\Proxy;
+               $proxy->ev0 = new Event("ev0", 0);
+               $this->setExpectedException("UnexpectedValueException", "The event name 'ev0' is already in use");
+               $proxy->ev0 = 0;
+       }
+       
+       function testAssignDifferent() {
+               $proxy = new Event\Proxy;
+               $this->setExpectedException("UnexpectedValueException", "The event names differ: 'ev3' <> 'ev4'");
+               $proxy->ev3 = new Event("ev4");
+       }
+}
diff --git a/tests/hikke/TestObserver.php b/tests/hikke/TestObserver.php
new file mode 100644 (file)
index 0000000..461f590
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+
+namespace hikke;
+
+class TestObserver implements \SplObserver
+{
+       static $logs;
+       
+       private $name;
+       
+       function __construct($name) {
+               $this->name = $name;
+       }
+       
+       function __toString() {
+               return $this->name;
+       }
+       
+       static function reset() {
+               self::$logs = "";
+       }
+       
+       function update(\SplSubject $event, $supp = null) {
+               self::$logs .= "$this->name: $event";
+               if ($supp && $supp != $event) {
+                       self::$logs .= ":$supp";
+               }
+               self::$logs .= "\n";
+       }
+       
+       function callMe(\stdClass $arg) {
+               $arg->data .= __METHOD__."\n";
+       }
+}