Commit a421f9f1 by Qiang Xue

refactored component event.

parent 31777c92
...@@ -7,20 +7,20 @@ is triggered (i.e. comment will be added), our custom code will be executed. ...@@ -7,20 +7,20 @@ is triggered (i.e. comment will be added), our custom code will be executed.
An event is identified by a name that should be unique within the class it is defined at. Event names are *case-sensitive*. An event is identified by a name that should be unique within the class it is defined at. Event names are *case-sensitive*.
One or multiple PHP callbacks, called *event handlers*, could be attached to an event. You can call [[trigger()]] to One or multiple PHP callbacks, called *event handlers*, can be attached to an event. You can call [[trigger()]] to
raise an event. When an event is raised, the event handlers will be invoked automatically in the order they were raise an event. When an event is raised, the event handlers will be invoked automatically in the order they were
attached. attached.
To attach an event handler to an event, call [[on()]]: To attach an event handler to an event, call [[on()]]:
~~~ ~~~
$comment->on('add', function($event) { $post->on('update', function($event) {
// send email notification // send email notification
}); });
~~~ ~~~
In the above, we attach an anonymous function to the "add" event of the comment. In the above, an anonymous function is attached to the "update" event of the post. You may attach
Valid event handlers include: the following types of event handlers:
- anonymous function: `function($event) { ... }` - anonymous function: `function($event) { ... }`
- object method: `array($object, 'handleAdd')` - object method: `array($object, 'handleAdd')`
...@@ -35,8 +35,8 @@ function foo($event) ...@@ -35,8 +35,8 @@ function foo($event)
where `$event` is an [[Event]] object which includes parameters associated with the event. where `$event` is an [[Event]] object which includes parameters associated with the event.
You can also attach an event handler to an event when configuring a component with a configuration array. The syntax is You can also attach a handler to an event when configuring a component with a configuration array.
like the following: The syntax is like the following:
~~~ ~~~
array( array(
...@@ -46,15 +46,13 @@ array( ...@@ -46,15 +46,13 @@ array(
where `on add` stands for attaching an event to the `add` event. where `on add` stands for attaching an event to the `add` event.
You can call [[getEventHandlers()]] to retrieve all event handlers that are attached to a specified event. Because this Sometimes, you may want to associate extra data with an event handler when you attach it to an event
method returns a [[Vector]] object, we can manipulate this object to attach/detach event handlers, or adjust their and then access it when the handler is invoked. You may do so by
relative orders.
~~~ ~~~
$handlers = $comment->getEventHandlers('add'); $post->on('update', function($event) {
$handlers->insertAt(0, $callback); // attach a handler as the first one // the data can be accessed via $event->data
$handlers[] = $callback; // attach a handler as the last one }, $data);
unset($handlers[0]); // detach the first handler
~~~ ~~~
......
...@@ -7,6 +7,8 @@ ...@@ -7,6 +7,8 @@
namespace yii\base; namespace yii\base;
use Yii;
/** /**
* Component is the base class that provides the *property*, *event* and *behavior* features. * Component is the base class that provides the *property*, *event* and *behavior* features.
* *
...@@ -17,16 +19,16 @@ namespace yii\base; ...@@ -17,16 +19,16 @@ namespace yii\base;
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class Component extends \yii\base\Object class Component extends Object
{ {
/** /**
* @var Vector[] the attached event handlers (event name => handlers) * @var array the attached event handlers (event name => handlers)
*/ */
private $_e; private $_events;
/** /**
* @var Behavior[] the attached behaviors (behavior name => behavior) * @var Behavior[] the attached behaviors (behavior name => behavior)
*/ */
private $_b; private $_behaviors;
/** /**
* Returns the value of a component property. * Returns the value of a component property.
...@@ -52,7 +54,7 @@ class Component extends \yii\base\Object ...@@ -52,7 +54,7 @@ class Component extends \yii\base\Object
} else { } else {
// behavior property // behavior property
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) { if ($behavior->canGetProperty($name)) {
return $behavior->$name; return $behavior->$name;
} }
...@@ -87,17 +89,16 @@ class Component extends \yii\base\Object ...@@ -87,17 +89,16 @@ class Component extends \yii\base\Object
return; return;
} elseif (strncmp($name, 'on ', 3) === 0) { } elseif (strncmp($name, 'on ', 3) === 0) {
// on event: attach event handler // on event: attach event handler
$name = trim(substr($name, 3)); $this->on(trim(substr($name, 3)), $value);
$this->getEventHandlers($name)->add($value);
return; return;
} elseif (strncmp($name, 'as ', 3) === 0) { } elseif (strncmp($name, 'as ', 3) === 0) {
// as behavior: attach behavior // as behavior: attach behavior
$name = trim(substr($name, 3)); $name = trim(substr($name, 3));
$this->attachBehavior($name, $value instanceof Behavior ? $value : \Yii::createObject($value)); $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value));
} else { } else {
// behavior property // behavior property
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) { if ($behavior->canSetProperty($name)) {
$behavior->$name = $value; $behavior->$name = $value;
return; return;
...@@ -131,7 +132,7 @@ class Component extends \yii\base\Object ...@@ -131,7 +132,7 @@ class Component extends \yii\base\Object
} else { } else {
// behavior property // behavior property
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name)) { if ($behavior->canGetProperty($name)) {
return $behavior->$name !== null; return $behavior->$name !== null;
} }
...@@ -161,7 +162,7 @@ class Component extends \yii\base\Object ...@@ -161,7 +162,7 @@ class Component extends \yii\base\Object
} else { } else {
// behavior property // behavior property
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name)) { if ($behavior->canSetProperty($name)) {
$behavior->$name = null; $behavior->$name = null;
return; return;
...@@ -198,7 +199,7 @@ class Component extends \yii\base\Object ...@@ -198,7 +199,7 @@ class Component extends \yii\base\Object
} }
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $object) { foreach ($this->_behaviors as $object) {
if (method_exists($object, $name)) { if (method_exists($object, $name)) {
return call_user_func_array(array($object, $name), $params); return call_user_func_array(array($object, $name), $params);
} }
...@@ -213,8 +214,8 @@ class Component extends \yii\base\Object ...@@ -213,8 +214,8 @@ class Component extends \yii\base\Object
*/ */
public function __clone() public function __clone()
{ {
$this->_e = null; $this->_events = null;
$this->_b = null; $this->_behaviors = null;
} }
/** /**
...@@ -259,7 +260,7 @@ class Component extends \yii\base\Object ...@@ -259,7 +260,7 @@ class Component extends \yii\base\Object
return true; return true;
} else { } else {
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canGetProperty($name, $checkVar)) { if ($behavior->canGetProperty($name, $checkVar)) {
return true; return true;
} }
...@@ -289,7 +290,7 @@ class Component extends \yii\base\Object ...@@ -289,7 +290,7 @@ class Component extends \yii\base\Object
return true; return true;
} else { } else {
$this->ensureBehaviors(); $this->ensureBehaviors();
foreach ($this->_b as $behavior) { foreach ($this->_behaviors as $behavior) {
if ($behavior->canSetProperty($name, $checkVar)) { if ($behavior->canSetProperty($name, $checkVar)) {
return true; return true;
} }
...@@ -337,44 +338,17 @@ class Component extends \yii\base\Object ...@@ -337,44 +338,17 @@ class Component extends \yii\base\Object
public function hasEventHandlers($name) public function hasEventHandlers($name)
{ {
$this->ensureBehaviors(); $this->ensureBehaviors();
return isset($this->_e[$name]) && $this->_e[$name]->getCount(); return !empty($this->_events[$name]);
}
/**
* Returns the list of attached event handlers for an event.
* You may manipulate the returned [[Vector]] object by adding or removing handlers.
* For example,
*
* ~~~
* $component->getEventHandlers($eventName)->insertAt(0, $eventHandler);
* ~~~
*
* @param string $name the event name
* @return Vector list of attached event handlers for the event
*/
public function getEventHandlers($name)
{
if (!isset($this->_e[$name])) {
$this->_e[$name] = new Vector;
}
$this->ensureBehaviors();
return $this->_e[$name];
} }
/** /**
* Attaches an event handler to an event. * Attaches an event handler to an event.
* *
* This is equivalent to the following code:
*
* ~~~
* $component->getEventHandlers($eventName)->add($eventHandler);
* ~~~
*
* An event handler must be a valid PHP callback. The followings are * An event handler must be a valid PHP callback. The followings are
* some examples: * some examples:
* *
* ~~~ * ~~~
* function($event) { ... } // anonymous function * function ($event) { ... } // anonymous function
* array($object, 'handleClick') // $object->handleClick() * array($object, 'handleClick') // $object->handleClick()
* array('Page', 'handleClick') // Page::handleClick() * array('Page', 'handleClick') // Page::handleClick()
* 'handleClick' // global function handleClick() * 'handleClick' // global function handleClick()
...@@ -383,31 +357,53 @@ class Component extends \yii\base\Object ...@@ -383,31 +357,53 @@ class Component extends \yii\base\Object
* An event handler must be defined with the following signature, * An event handler must be defined with the following signature,
* *
* ~~~ * ~~~
* function handlerName($event) {} * function ($event)
* ~~~ * ~~~
* *
* where `$event` is an [[Event]] object which includes parameters associated with the event. * where `$event` is an [[Event]] object which includes parameters associated with the event.
* *
* @param string $name the event name * @param string $name the event name
* @param string|array|\Closure $handler the event handler * @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() * @see off()
*/ */
public function on($name, $handler) public function on($name, $handler, $data = null)
{ {
$this->getEventHandlers($name)->add($handler); $this->ensureBehaviors();
$this->_events[$name][] = array($handler, $data);
} }
/** /**
* Detaches an existing event handler from this component. * Detaches an existing event handler from this component.
* This method is the opposite of [[on()]]. * This method is the opposite of [[on()]].
* @param string $name event name * @param string $name event name
* @param string|array|\Closure $handler the event handler to be removed * @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 * @return boolean if a handler is found and detached
* @see on() * @see on()
*/ */
public function off($name, $handler) public function off($name, $handler = null)
{ {
return $this->getEventHandlers($name)->remove($handler) !== false; $this->ensureBehaviors();
if (isset($this->_events[$name])) {
if ($handler === null) {
$this->_events[$name] = array();
} else {
$removed = false;
foreach ($this->_events[$name] as $i => $event) {
if ($event[0] === $handler) {
unset($this->_events[$name][$i]);
$removed = true;
}
}
if ($removed) {
$this->_events[$name] = array_values($this->_events[$name]);
}
return $removed;
}
}
return false;
} }
/** /**
...@@ -420,7 +416,7 @@ class Component extends \yii\base\Object ...@@ -420,7 +416,7 @@ class Component extends \yii\base\Object
public function trigger($name, $event = null) public function trigger($name, $event = null)
{ {
$this->ensureBehaviors(); $this->ensureBehaviors();
if (isset($this->_e[$name]) && $this->_e[$name]->getCount()) { if (!empty($this->_events[$name])) {
if ($event === null) { if ($event === null) {
$event = new Event; $event = new Event;
} }
...@@ -429,8 +425,9 @@ class Component extends \yii\base\Object ...@@ -429,8 +425,9 @@ class Component extends \yii\base\Object
} }
$event->handled = false; $event->handled = false;
$event->name = $name; $event->name = $name;
foreach ($this->_e[$name] as $handler) { foreach ($this->_events[$name] as $handler) {
call_user_func($handler, $event); $event->data = $handler[1];
call_user_func($handler[0], $event);
// stop further handling if the event is handled // stop further handling if the event is handled
if ($event instanceof Event && $event->handled) { if ($event instanceof Event && $event->handled) {
return; return;
...@@ -447,7 +444,7 @@ class Component extends \yii\base\Object ...@@ -447,7 +444,7 @@ class Component extends \yii\base\Object
public function getBehavior($name) public function getBehavior($name)
{ {
$this->ensureBehaviors(); $this->ensureBehaviors();
return isset($this->_b[$name]) ? $this->_b[$name] : null; return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null;
} }
/** /**
...@@ -457,20 +454,20 @@ class Component extends \yii\base\Object ...@@ -457,20 +454,20 @@ class Component extends \yii\base\Object
public function getBehaviors() public function getBehaviors()
{ {
$this->ensureBehaviors(); $this->ensureBehaviors();
return $this->_b; return $this->_behaviors;
} }
/** /**
* Attaches a behavior to this component. * Attaches a behavior to this component.
* This method will create the behavior object based on the given * This method will create the behavior object based on the given
* configuration. After that, the behavior object will be attached to * configuration. After that, the behavior object will be attached to
* this component by calling the [[Behavior::attach]] method. * this component by calling the [[Behavior::attach()]] method.
* @param string $name the name of the behavior. * @param string $name the name of the behavior.
* @param string|array|Behavior $behavior the behavior configuration. This can be one of the following: * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following:
* *
* - a [[Behavior]] object * - a [[Behavior]] object
* - a string specifying the behavior class * - a string specifying the behavior class
* - an object configuration array that will be passed to [[\Yii::createObject()]] to create the behavior object. * - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object.
* *
* @return Behavior the behavior object * @return Behavior the behavior object
* @see detachBehavior * @see detachBehavior
...@@ -498,15 +495,15 @@ class Component extends \yii\base\Object ...@@ -498,15 +495,15 @@ class Component extends \yii\base\Object
/** /**
* Detaches a behavior from the component. * Detaches a behavior from the component.
* The behavior's [[Behavior::detach]] method will be invoked. * The behavior's [[Behavior::detach()]] method will be invoked.
* @param string $name the behavior's name. * @param string $name the behavior's name.
* @return Behavior the detached behavior. Null if the behavior does not exist. * @return Behavior the detached behavior. Null if the behavior does not exist.
*/ */
public function detachBehavior($name) public function detachBehavior($name)
{ {
if (isset($this->_b[$name])) { if (isset($this->_behaviors[$name])) {
$behavior = $this->_b[$name]; $behavior = $this->_behaviors[$name];
unset($this->_b[$name]); unset($this->_behaviors[$name]);
$behavior->detach(); $behavior->detach();
return $behavior; return $behavior;
} else { } else {
...@@ -519,12 +516,12 @@ class Component extends \yii\base\Object ...@@ -519,12 +516,12 @@ class Component extends \yii\base\Object
*/ */
public function detachBehaviors() public function detachBehaviors()
{ {
if ($this->_b !== null) { if ($this->_behaviors !== null) {
foreach ($this->_b as $name => $behavior) { foreach ($this->_behaviors as $name => $behavior) {
$this->detachBehavior($name); $this->detachBehavior($name);
} }
} }
$this->_b = array(); $this->_behaviors = array();
} }
/** /**
...@@ -532,8 +529,8 @@ class Component extends \yii\base\Object ...@@ -532,8 +529,8 @@ class Component extends \yii\base\Object
*/ */
public function ensureBehaviors() public function ensureBehaviors()
{ {
if ($this->_b === null) { if ($this->_behaviors === null) {
$this->_b = array(); $this->_behaviors = array();
foreach ($this->behaviors() as $name => $behavior) { foreach ($this->behaviors() as $name => $behavior) {
$this->attachBehaviorInternal($name, $behavior); $this->attachBehaviorInternal($name, $behavior);
} }
...@@ -549,12 +546,12 @@ class Component extends \yii\base\Object ...@@ -549,12 +546,12 @@ class Component extends \yii\base\Object
private function attachBehaviorInternal($name, $behavior) private function attachBehaviorInternal($name, $behavior)
{ {
if (!($behavior instanceof Behavior)) { if (!($behavior instanceof Behavior)) {
$behavior = \Yii::createObject($behavior); $behavior = Yii::createObject($behavior);
} }
if (isset($this->_b[$name])) { if (isset($this->_behaviors[$name])) {
$this->_b[$name]->detach(); $this->_behaviors[$name]->detach();
} }
$behavior->attach($this); $behavior->attach($this);
return $this->_b[$name] = $behavior; return $this->_behaviors[$name] = $behavior;
} }
} }
...@@ -15,12 +15,14 @@ namespace yii\base; ...@@ -15,12 +15,14 @@ namespace yii\base;
* And the [[handled]] property indicates if the event is handled. * And the [[handled]] property indicates if the event is handled.
* If an event handler sets [[handled]] to be true, the rest of the * If an event handler sets [[handled]] to be true, the rest of the
* uninvoked handlers will no longer be called to handle the event. * uninvoked handlers will no longer be called to handle the event.
* Additionally, an event may specify extra parameters via the [[data]] property. *
* Additionally, when attaching an event handler, extra data may be passed
* and be available via the [[data]] property when the event handler is invoked.
* *
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class Event extends \yii\base\Object class Event extends Object
{ {
/** /**
* @var string the event name. This property is set by [[Component::trigger()]]. * @var string the event name. This property is set by [[Component::trigger()]].
...@@ -39,7 +41,8 @@ class Event extends \yii\base\Object ...@@ -39,7 +41,8 @@ class Event extends \yii\base\Object
*/ */
public $handled = false; public $handled = false;
/** /**
* @var mixed extra custom data associated with the event. * @var mixed the data that is passed to [[Component::on()]] when attaching an event handler.
* Note that this varies according to which event handler is currently executing.
*/ */
public $data; public $data;
} }
...@@ -41,12 +41,12 @@ class ComponentTest extends TestCase ...@@ -41,12 +41,12 @@ class ComponentTest extends TestCase
$component->attachBehavior('a', $behavior); $component->attachBehavior('a', $behavior);
$this->assertSame($behavior, $component->getBehavior('a')); $this->assertSame($behavior, $component->getBehavior('a'));
$component->on('test', 'fake'); $component->on('test', 'fake');
$this->assertEquals(1, $component->getEventHandlers('test')->count); $this->assertTrue($component->hasEventHandlers('test'));
$clone = clone $component; $clone = clone $component;
$this->assertNotSame($component, $clone); $this->assertNotSame($component, $clone);
$this->assertNull($clone->getBehavior('a')); $this->assertNull($clone->getBehavior('a'));
$this->assertEquals(0, $clone->getEventHandlers('test')->count); $this->assertFalse($clone->hasEventHandlers('test'));
} }
public function testHasProperty() public function testHasProperty()
...@@ -151,34 +151,32 @@ class ComponentTest extends TestCase ...@@ -151,34 +151,32 @@ class ComponentTest extends TestCase
public function testOn() public function testOn()
{ {
$this->assertEquals(0, $this->component->getEventHandlers('click')->getCount()); $this->assertFalse($this->component->hasEventHandlers('click'));
$this->component->on('click', 'foo'); $this->component->on('click', 'foo');
$this->assertEquals(1, $this->component->getEventHandlers('click')->getCount()); $this->assertTrue($this->component->hasEventHandlers('click'));
$this->component->on('click', 'bar');
$this->assertEquals(2, $this->component->getEventHandlers('click')->getCount());
$p = 'on click';
$this->component->$p = 'foo2';
$this->assertEquals(3, $this->component->getEventHandlers('click')->getCount());
$this->component->getEventHandlers('click')->add('test'); $this->assertFalse($this->component->hasEventHandlers('click2'));
$this->assertEquals(4, $this->component->getEventHandlers('click')->getCount()); $p = 'on click2';
$this->component->$p = 'foo2';
$this->assertTrue($this->component->hasEventHandlers('click2'));
} }
public function testOff() public function testOff()
{ {
$this->assertFalse($this->component->hasEventHandlers('click'));
$this->component->on('click', 'foo'); $this->component->on('click', 'foo');
$this->component->on('click', array($this->component, 'myEventHandler')); $this->assertTrue($this->component->hasEventHandlers('click'));
$this->assertEquals(2, $this->component->getEventHandlers('click')->getCount()); $this->component->off('click', 'foo');
$this->assertFalse($this->component->hasEventHandlers('click'));
$result = $this->component->off('click', 'foo');
$this->assertTrue($result); $this->component->on('click2', 'foo');
$this->assertEquals(1, $this->component->getEventHandlers('click')->getCount()); $this->component->on('click2', 'foo2');
$result = $this->component->off('click', 'foo'); $this->component->on('click2', 'foo3');
$this->assertFalse($result); $this->assertTrue($this->component->hasEventHandlers('click2'));
$this->assertEquals(1, $this->component->getEventHandlers('click')->getCount()); $this->component->off('click2', 'foo3');
$result = $this->component->off('click', array($this->component, 'myEventHandler')); $this->assertTrue($this->component->hasEventHandlers('click2'));
$this->assertTrue($result); $this->component->off('click2');
$this->assertEquals(0, $this->component->getEventHandlers('click')->getCount()); $this->assertFalse($this->component->hasEventHandlers('click2'));
} }
public function testTrigger() public function testTrigger()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment