--- /dev/null
+nbproject
+vendor
--- /dev/null
+language: php
+
+php:
+ - 5.4
+ - 5.5
+ - 5.6
+ - hhvm
+
+before_script: composer install
+
+script: phpunit --coverage-text --colors tests
--- /dev/null
+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.
+
--- /dev/null
+# 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)
+```
--- /dev/null
+{
+ "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"
+ }
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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;
+ }
+}
--- /dev/null
+<?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();
+}
--- /dev/null
+<?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
+ }
+ }
+
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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();
+ }
+}
--- /dev/null
+<?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);
+ }
+}
--- /dev/null
+<?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");
+ }
+}
--- /dev/null
+<?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";
+ }
+}