From: Michael Wallner Date: Mon, 18 Aug 2014 11:56:45 +0000 (+0200) Subject: initial checkin X-Git-Tag: v1.0.0~1 X-Git-Url: https://git.m6w6.name/?p=m6w6%2Fhikke;a=commitdiff_plain;h=188eaa726471888577373bbbcad74b01e5bb734b initial checkin --- 188eaa726471888577373bbbcad74b01e5bb734b diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aab7eee --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +nbproject +vendor diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d1c113c --- /dev/null +++ b/.travis.yml @@ -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 index 0000000..0ec221c --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2014, Michael Wallner . +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 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 +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 +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 index 0000000..756f70d --- /dev/null +++ b/composer.json @@ -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 index 0000000..50c2c82 --- /dev/null +++ b/lib/hikke/Event.php @@ -0,0 +1,121 @@ + + */ +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 index 0000000..3c34a53 --- /dev/null +++ b/lib/hikke/Event/Bucket.php @@ -0,0 +1,70 @@ + + */ +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 index 0000000..dc4dac7 --- /dev/null +++ b/lib/hikke/Event/Prioritized.php @@ -0,0 +1,22 @@ + + */ +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 index 0000000..b9402d3 --- /dev/null +++ b/lib/hikke/Event/Priority.php @@ -0,0 +1,44 @@ + + */ +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 index 0000000..f04c0bb --- /dev/null +++ b/lib/hikke/Event/Proxy.php @@ -0,0 +1,195 @@ + + */ +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 index 0000000..33a94c3 --- /dev/null +++ b/lib/hikke/Event/Storage.php @@ -0,0 +1,100 @@ + + */ +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 index 0000000..34a4b17 --- /dev/null +++ b/tests/hikke/EventTest.php @@ -0,0 +1,56 @@ +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 index 0000000..b93bb9e --- /dev/null +++ b/tests/hikke/ProxyTest.php @@ -0,0 +1,119 @@ +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 index 0000000..461f590 --- /dev/null +++ b/tests/hikke/TestObserver.php @@ -0,0 +1,34 @@ +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"; + } +}