From 8b00693a0a4e13ea07623f7a96f57d68bffbe416 Mon Sep 17 00:00:00 2001 From: Qiang Xue Date: Wed, 30 Oct 2013 00:12:48 -0400 Subject: [PATCH] Fixes #1025: Implemented support for class-level events. --- framework/yii/base/Component.php | 7 +- framework/yii/base/Event.php | 132 ++++++++++++++++++++++++++++ tests/unit/framework/base/ComponentTest.php | 1 - tests/unit/framework/base/EventTest.php | 93 ++++++++++++++++++++ 4 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 tests/unit/framework/base/EventTest.php diff --git a/framework/yii/base/Component.php b/framework/yii/base/Component.php index 1d26dd7..b67a09f 100644 --- a/framework/yii/base/Component.php +++ b/framework/yii/base/Component.php @@ -358,13 +358,13 @@ class Component extends Object public function hasEventHandlers($name) { $this->ensureBehaviors(); - return !empty($this->_events[$name]); + return !empty($this->_events[$name]) || Event::hasHandlers($this, $name); } /** * Attaches an event handler to an event. * - * An event handler must be a valid PHP callback. The followings are + * The event handler must be a valid PHP callback. The followings are * some examples: * * ~~~ @@ -374,7 +374,7 @@ class Component extends Object * 'handleClick' // global function handleClick() * ~~~ * - * An event handler must be defined with the following signature, + * The event handler must be defined with the following signature, * * ~~~ * function ($event) @@ -455,6 +455,7 @@ class Component extends Object } } } + Event::trigger($this, $name, $event); } /** diff --git a/framework/yii/base/Event.php b/framework/yii/base/Event.php index 5d40736..97ef905 100644 --- a/framework/yii/base/Event.php +++ b/framework/yii/base/Event.php @@ -45,4 +45,136 @@ class Event extends Object * Note that this varies according to which event handler is currently executing. */ public $data; + + private static $_events = []; + + /** + * Attaches an event handler to a class-level event. + * + * When a class-level event is triggered, event handlers attached + * to that class and all parent classes will be invoked. + * + * For example, the following code attaches an event handler to `ActiveRecord`'s + * `afterInsert` event: + * + * ~~~ + * Event::on([ActiveRecord::className, ActiveRecord::EVENT_AFTER_INSERT], function ($event) { + * Yii::trace(get_class($event->sender) . ' is inserted.'); + * }); + * ~~~ + * + * The handler will be invoked for EVERY successful ActiveRecord insertion. + * + * For more details about how to declare an event handler, please refer to [[Component::on()]]. + * + * @param string $class the fully qualified class name to which the event handler needs to attach + * @param string $name the event name + * @param callback $handler the event handler + * @param mixed $data the data to be passed to the event handler when the event is triggered. + * When the event handler is invoked, this data can be accessed via [[Event::data]]. + * @see off() + */ + public static function on($class, $name, $handler, $data = null) + { + self::$_events[$name][ltrim($class, '\\')][] = [$handler, $data]; + } + + /** + * Detaches an event handler from a class-level event. + * + * This method is the opposite of [[on()]]. + * + * @param string $class the fully qualified class name from which the event handler needs to be detached + * @param string $name the event name + * @param callback $handler the event handler to be removed. + * If it is null, all handlers attached to the named event will be removed. + * @return boolean if a handler is found and detached + * @see on() + */ + public static function off($class, $name, $handler = null) + { + $class = ltrim($class, '\\'); + if (empty(self::$_events[$name][$class])) { + return false; + } + if ($handler === null) { + unset(self::$_events[$name][$class]); + return true; + } else { + $removed = false; + foreach (self::$_events[$name][$class] as $i => $event) { + if ($event[0] === $handler) { + unset(self::$_events[$name][$class][$i]); + $removed = true; + } + } + if ($removed) { + self::$_events[$name][$class] = array_values(self::$_events[$name][$class]); + } + return $removed; + } + } + + /** + * Returns a value indicating whether there is any handler attached to the specified class-level event. + * Note that this method will also check all parent classes to see if there is any handler attached + * to the named event. + * @param string|object $class the object or the fully qualified class name specifying the class-level event + * @param string $name the event name + * @return boolean whether there is any handler attached to the event. + */ + public static function hasHandlers($class, $name) + { + if (empty(self::$_events[$name])) { + return false; + } + if (is_object($class)) { + $class = get_class($class); + } else { + $class = ltrim($class, '\\'); + } + do { + if (!empty(self::$_events[$name][$class])) { + return true; + } + } while (($class = get_parent_class($class)) !== false); + return false; + } + + /** + * Triggers a class-level event. + * This method will cause invocation of event handlers that are attached to the named event + * for the specified class and all its parent classes. + * @param string|object $class the object or the fully qualified class name specifying the class-level event + * @param string $name the event name + * @param Event $event the event parameter. If not set, a default [[Event]] object will be created. + */ + public static function trigger($class, $name, $event = null) + { + if (empty(self::$_events[$name])) { + return; + } + if ($event === null) { + $event = new self; + } + $event->handled = false; + $event->name = $name; + + if (is_object($class)) { + $class = get_class($class); + } else { + $class = ltrim($class, '\\'); + } + do { + if (!empty(self::$_events[$name][$class])) { + foreach (self::$_events[$name][$class] as $handler) { + $event->data = $handler[1]; + call_user_func($handler[0], $event); + if ($event instanceof Event && $event->handled) { + return; + } + } + } + } while (($class = get_parent_class($class)) !== false); + } } diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index d1698eb..2cad56d 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -302,7 +302,6 @@ class ComponentTest extends TestCase $component->detachBehaviors(); $this->assertNull($component->getBehavior('a')); $this->assertNull($component->getBehavior('b')); - } } diff --git a/tests/unit/framework/base/EventTest.php b/tests/unit/framework/base/EventTest.php new file mode 100644 index 0000000..6226793 --- /dev/null +++ b/tests/unit/framework/base/EventTest.php @@ -0,0 +1,93 @@ + + * @since 2.0 + */ +class EventTest extends TestCase +{ + public $counter; + + public function setUp() + { + $this->counter = 0; + Event::off(ActiveRecord::className(), 'save'); + Event::off(Post::className(), 'save'); + Event::off(User::className(), 'save'); + } + + public function testOn() + { + Event::on(Post::className(), 'save', function ($event) { + $this->counter += 1; + }); + Event::on(ActiveRecord::className(), 'save', function ($event) { + $this->counter += 3; + }); + $this->assertEquals(0, $this->counter); + $post = new Post; + $post->save(); + $this->assertEquals(4, $this->counter); + $user = new User; + $user->save(); + $this->assertEquals(7, $this->counter); + } + + public function testOff() + { + $handler = function ($event) { + $this->counter ++; + }; + $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); + Event::on(Post::className(), 'save', $handler); + $this->assertTrue(Event::hasHandlers(Post::className(), 'save')); + Event::off(Post::className(), 'save', $handler); + $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); + } + + public function testHasHandlers() + { + $this->assertFalse(Event::hasHandlers(Post::className(), 'save')); + $this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save')); + Event::on(Post::className(), 'save', function ($event) { + $this->counter += 1; + }); + $this->assertTrue(Event::hasHandlers(Post::className(), 'save')); + $this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save')); + + $this->assertFalse(Event::hasHandlers(User::className(), 'save')); + Event::on(ActiveRecord::className(), 'save', function ($event) { + $this->counter += 1; + }); + $this->assertTrue(Event::hasHandlers(User::className(), 'save')); + $this->assertTrue(Event::hasHandlers(ActiveRecord::className(), 'save')); + } +} + +class ActiveRecord extends Component +{ + public function save() + { + $this->trigger('save'); + } +} + +class Post extends ActiveRecord +{ +} + +class User extends ActiveRecord +{ + +}