Commit 47fe25d4 by Qiang Xue

new event implementation.

parent e281b402
......@@ -19,7 +19,7 @@ namespace yii\base;
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Behavior extends Object
class Behavior extends \yii\base\Object
{
/**
* @var Component the owner component
......@@ -38,17 +38,17 @@ class Behavior extends Object
*
* The callbacks can be any of the followings:
*
* - method in this behavior: `'handleOnClick'`, equivalent to `array($this, 'handleOnClick')`
* - object method: `array($object, 'handleOnClick')`
* - static method: `array('Page', 'handleOnClick')`
* - method in this behavior: `'handleClick'`, equivalent to `array($this, 'handleClick')`
* - object method: `array($object, 'handleClick')`
* - static method: `array('Page', 'handleClick')`
* - anonymous function: `function($event) { ... }`
*
* The following is an example:
*
* ~~~
* array(
* 'onBeforeValidate' => 'myBeforeValidate',
* 'onAfterValidate' => 'myAfterValidate',
* 'beforeValidate' => 'myBeforeValidate',
* 'afterValidate' => 'myAfterValidate',
* )
* ~~~
*
......@@ -70,7 +70,7 @@ class Behavior extends Object
{
$this->_owner = $owner;
foreach ($this->events() as $event => $handler) {
$owner->attachEventHandler($event, is_string($handler) ? array($this, $handler) : $handler);
$owner->on($event, is_string($handler) ? array($this, $handler) : $handler);
}
}
......@@ -84,7 +84,7 @@ class Behavior extends Object
public function detach($owner)
{
foreach ($this->events() as $event => $handler) {
$owner->detachEventHandler($event, is_string($handler) ? array($this, $handler) : $handler);
$owner->off($event, is_string($handler) ? array($this, $handler) : $handler);
}
$this->_owner = null;
}
......
......@@ -12,34 +12,36 @@ namespace yii\base;
/**
* Component is the base class for all component classes in Yii.
*
* Extending from [[Object]], Component implements the *event* and *behavior*
* features in addition to the *property* feature.
* Component provides the *event* and *behavior* features, in addition to
* the *property* feature which is implemented in its parent class [[Object]].
*
* An event is defined by the presence of a method whose name starts with `on`.
* The event name is the method name. For example, the following method defines
* the `onClick` event:
* Event is a way to "inject" custom code into existing code at certain places.
* For example, a button object can trigger a "click" event when the user clicks
* on the button. We can write custom code and attach it to this event so that
* when the event is triggered, our custom code will be executed.
*
* ~~~
* public function onClick($event)
* {
* $this->raiseEvent('onClick', $event);
* }
* ~~~
*
* Event names are case-insensitive.
* An event is identified by a name (unique within the class it is defined).
* Event names are *case-sensitive*.
*
* An event can be attached with one or multiple PHP callbacks, called *event handlers*.
* One can call [[raiseEvent()]] to raise an event. When an event is raised, the attached
* One can call [[trigger()]] to raise an event. When an event is raised, the attached
* event handlers will be invoked automatically in the order they are attached to the event.
*
* To attach an event handler to an event, call [[attachEventHandler]]. Alternatively,
* you can use the assignment syntax: `$component->onClick = $callback;`,
* where `$callback` refers to a valid PHP callback which can be one of the followings:
* To attach an event handler to an event, call [[on()]]. For example,
*
* - global function: `'handleOnClick'`
* ~~~
* $button->on('click', function($event) {
* echo "I'm clicked!";
* });
* ~~~
*
* In the above, we attach an anonymous function to the "click" event of the button.
* Valid event handlers include:
*
* - anonymous function: `function($event) { ... }`
* - object method: `array($object, 'handleOnClick')`
* - static method: `array('Page', 'handleOnClick')`
* - anonymous function: `function($event) { ... }`
* - global function: `'handleOnClick'`
*
* The signature of an event handler should be like the following:
* ~~~
......@@ -48,14 +50,15 @@ namespace yii\base;
*
* where `$event` is an [[Event]] object which includes parameters associated with the event.
*
* Because `$component->onClick` is returned as a [[Vector]] with each item in the vector being
* an attached event handler, one can manipulate this [[Vector]] object to attach/detach event
* handlers, or adjust their relative orders. For example,
* One can call [[getEventHandlers()]] to retrieve all event handlers that are attached
* to a specified event. Because this method returns a [[Vector]] object, we can manipulate
* this object to attach/detach event handlers, or adjust their relative orders.
*
* ~~~
* $component->onClick->insertAt(0, $callback); // attach a handler as the first one
* $component->onClick[] = $callback; // attach a handler as the last one
* unset($component->onClick[0]); // detach the first handler
* $handlers = $button->getEventHandlers('click');
* $handlers->insertAt(0, $callback); // attach a handler as the first one
* $handlers[] = $callback; // attach a handler as the last one
* unset($handlers[0]); // detach the first handler
* ~~~
*
*
......@@ -65,6 +68,7 @@ namespace yii\base;
* those properties and methods.
*
* To attach a behavior to a component, declare it in [[behaviors()]], or explicitly call [[attachBehavior]].
* Behaviors declared in [[behaviors()]] are automatically attached to the corresponding component.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
......@@ -85,7 +89,6 @@ class Component extends \yii\base\Object
* This method will check in the following order and act accordingly:
*
* - a property defined by a getter: return the getter result
* - an event: return a vector containing the attached event handlers
* - a behavior: return the behavior object
* - a property of a behavior: return the behavior property value
*
......@@ -102,12 +105,6 @@ class Component extends \yii\base\Object
$getter = 'get' . $name;
if (method_exists($this, $getter)) { // read property, e.g. getName()
return $this->$getter();
} elseif (method_exists($this, $name) && strncasecmp($name, 'on', 2) === 0) { // event, e.g. onClick()
$name = strtolower($name);
if (!isset($this->_e[$name])) {
$this->_e[$name] = new Vector;
}
return $this->_e[$name];
} else { // behavior property
$this->ensureBehaviors();
foreach ($this->_b as $behavior) {
......@@ -131,7 +128,6 @@ class Component extends \yii\base\Object
* will be implicitly called when executing `$component->property = $value;`.
* @param string $name the property name or the event name
* @param mixed $value the property value
* @return mixed value that was set
* @throws Exception if the property is not defined or read-only.
* @see __get
*/
......@@ -139,18 +135,18 @@ class Component extends \yii\base\Object
{
$setter = 'set' . $name;
if (method_exists($this, $setter)) { // write property
return $this->$setter($value);
} elseif (method_exists($this, $name) && strncasecmp($name, 'on', 2) === 0) { // event
$name = strtolower($name);
if (!isset($this->_e[$name])) {
$this->_e[$name] = new Vector;
}
return $this->_e[$name]->add($value);
$this->$setter($value);
return;
} elseif (strncmp($name, 'on ', 3) === 0) { // on event
$name = substr($name, 3);
$this->getEventHandlers($name)->add($value);
return;
} else { // behavior property
$this->ensureBehaviors();
foreach ($this->_b as $behavior) {
if ($behavior->canSetProperty($name)) {
return $behavior->$name = $value;
$behavior->$name = $value;
return;
}
}
}
......@@ -166,7 +162,6 @@ class Component extends \yii\base\Object
* This method will check in the following order and act accordingly:
*
* - a property defined by a setter: return whether the property value is null
* - an event: return whether the event has any handler attached
* - a property of a behavior: return whether the property value is null
*
* Do not call this method directly as it is a PHP magic method that
......@@ -179,9 +174,6 @@ class Component extends \yii\base\Object
$getter = 'get' . $name;
if (method_exists($this, $getter)) { // property is not null
return $this->$getter() !== null;
} elseif (method_exists($this, $name) && strncasecmp($name, 'on', 2) === 0) { // has event handler
$name = strtolower($name);
return isset($this->_e[$name]) && $this->_e[$name]->getCount();
} else { // behavior property
$this->ensureBehaviors();
foreach ($this->_b as $behavior) {
......@@ -198,7 +190,6 @@ class Component extends \yii\base\Object
* This method will check in the following order and act accordingly:
*
* - a property defined by a setter: set the property value to be null
* - an event: remove all attached event handlers
* - a property of a behavior: set the property value to be null
*
* Do not call this method directly as it is a PHP magic method that
......@@ -212,9 +203,6 @@ class Component extends \yii\base\Object
if (method_exists($this, $setter)) { // write property
$this->$setter(null);
return;
} elseif (method_exists($this, $name) && strncasecmp($name, 'on', 2) === 0) { // event
unset($this->_e[strtolower($name)]);
return;
} else { // behavior property
$this->ensureBehaviors();
foreach ($this->_b as $behavior) {
......@@ -280,7 +268,7 @@ class Component extends \yii\base\Object
*
* Note that a behavior class must extend from [[Behavior]].
*
* Behaviors declared in this method will be attached to the component on demand.
* Behaviors declared in this method will be attached to the component automatically (on demand).
*
* @return array the behavior configurations.
*/
......@@ -290,18 +278,6 @@ class Component extends \yii\base\Object
}
/**
* Returns a value indicating whether an event is defined.
* An event is defined if the class has a method whose name starts with `on` (e.g. `onClick`).
* Note that event name is case-insensitive.
* @param string $name the event name
* @return boolean whether an event is defined
*/
public function hasEvent($name)
{
return method_exists($this, $name) && strncasecmp($name, 'on', 2) === 0;
}
/**
* Returns a value indicating whether there is any handler attached to the named event.
* @param string $name the event name
* @return boolean whether there is any handler attached to the event.
......@@ -309,7 +285,6 @@ class Component extends \yii\base\Object
public function hasEventHandlers($name)
{
$this->ensureBehaviors();
$name = strtolower($name);
return isset($this->_e[$name]) && $this->_e[$name]->getCount();
}
......@@ -328,15 +303,12 @@ class Component extends \yii\base\Object
*/
public function getEventHandlers($name)
{
if ($this->hasEvent($name)) {
$name = strtolower($name);
if (!isset($this->_e[$name])) {
$this->_e[$name] = new Vector;
}
$this->ensureBehaviors();
return $this->_e[$name];
}
throw new Exception('Undefined event: ' . $name);
}
/**
* Attaches an event handler to an event.
......@@ -351,10 +323,10 @@ class Component extends \yii\base\Object
* some examples:
*
* ~~~
* 'handleOnClick' // handleOnClick() is a global function
* array($object, 'handleOnClick') // $object->handleOnClick()
* array('Page', 'handleOnClick') // Page::handleOnClick()
* function($event) { ... } // anonymous function
* array($object, 'handleClick') // $object->handleClick()
* array('Page', 'handleClick') // Page::handleClick()
* 'handleClick' // global function handleClick()
* ~~~
*
* An event handler must be defined with the following signature,
......@@ -367,68 +339,49 @@ class Component extends \yii\base\Object
*
* @param string $name the event name
* @param callback $handler the event handler
* @throws Exception if the event is not defined
* @see detachEventHandler
* @see off
*/
public function attachEventHandler($name, $handler)
public function on($name, $handler)
{
$this->getEventHandlers($name)->add($handler);
}
/**
* Detaches an existing event handler.
* This method is the opposite of [[attachEventHandler]].
* This method is the opposite of [[on]].
* @param string $name event name
* @param callback $handler the event handler to be removed
* @return boolean if the detachment process is successful
* @see attachEventHandler
* @return boolean if a handler is found and detached
* @see on
*/
public function detachEventHandler($name, $handler)
public function off($name, $handler)
{
return $this->getEventHandlers($name)->remove($handler) !== false;
}
/**
* Raises an event.
* Triggers an event.
* This method represents the happening of an event. It invokes
* all attached handlers for the event.
* @param string $name the event name
* @param Event $event the event parameter
* @throws Exception if the event is undefined or an event handler is invalid.
*/
public function raiseEvent($name, $event)
public function trigger($name, $event)
{
$this->ensureBehaviors();
$name = strtolower($name);
if (isset($this->_e[$name])) {
if ($event instanceof Event) {
$event->name = $name;
$event->handled = false;
}
if (isset($this->_e[$name])) {
foreach ($this->_e[$name] as $handler) {
if (is_string($handler) || $handler instanceof \Closure) {
call_user_func($handler, $event);
} elseif (is_callable($handler, true)) {
// an array: 0 - object, 1 - method name
list($object, $method) = $handler;
if (is_string($object)) { // static method call
call_user_func($handler, $event);
} elseif (method_exists($object, $method)) {
$object->$method($event);
} else {
throw new Exception('Event "' . get_class($this) . '.' . $name . '" is attached with an invalid handler.');
}
} else {
throw new Exception('Event "' . get_class($this) . '.' . $name . '" is attached with an invalid handler.');
}
// stop further handling if the event is handled
if ($event instanceof Event && $event->handled) {
return;
}
}
} elseif (!$this->hasEvent($name)) {
throw new Exception('Raising unknown event: ' . get_class($this) . '.' . $name);
}
}
......
......@@ -24,9 +24,8 @@ class ModelBehavior extends Behavior
* Declares event handlers for owner's events.
* The default implementation returns the following event handlers:
*
* - `onAfterInit` event: [[afterInit]]
* - `onBeforeValidate` event: [[beforeValidate]]
* - `onAfterValidate` event: [[afterValidate]]
* - `beforeValidate` event
* - `afterValidate` event
*
* You may override these event handler methods to respond to the corresponding owner events.
* @return array events (array keys) and the corresponding event handler methods (array values).
......@@ -34,22 +33,12 @@ class ModelBehavior extends Behavior
public function events()
{
return array(
'onAfterInit' => 'afterInit',
'onBeforeValidate' => 'beforeValidate',
'onAfterValidate' => 'afterValidate',
'beforeValidate' => 'beforeValidate',
'afterValidate' => 'afterValidate',
);
}
/**
* Responds to [[Model::onAfterInit]] event.
* Override this method if you want to handle the corresponding event of the [[owner]].
* @param Event $event event parameter
*/
public function afterInit($event)
{
}
/**
* Responds to [[Model::onBeforeValidate]] event.
* Override this method if you want to handle the corresponding event of the [[owner]].
* You may set the [[ModelEvent::isValid|isValid]] property of the event parameter
......
......@@ -294,7 +294,7 @@ class Object
* ~~~
*
* @param array $config the object configuration (name-value pairs that will be used to initialize the object)
* @return Object the created object
* @return \yii\base\Object the created object
* @throws Exception if the configuration is invalid.
*/
public static function newInstance($config = array())
......@@ -326,7 +326,7 @@ class Object
$object->$name = $value;
}
if ($object instanceof \yii\base\Initable) {
if ($object instanceof Initable) {
$object->init();
}
......
......@@ -4,17 +4,20 @@ namespace yiiunit\framework\base;
function globalEventHandler($event)
{
$event->sender->eventHandled=true;
$event->sender->eventHandled = true;
}
function globalEventHandler2($event)
{
$event->sender->eventHandled=true;
$event->handled=true;
$event->sender->eventHandled = true;
$event->handled = true;
}
class ComponentTest extends \yiiunit\TestCase
{
/**
* @var NewComponent
*/
protected $component;
public function setUp()
......@@ -50,19 +53,19 @@ class ComponentTest extends \yiiunit\TestCase
public function testGetProperty()
{
$this->assertTrue('default'===$this->component->Text);
$this->assertTrue('default' === $this->component->Text);
$this->setExpectedException('yii\base\Exception');
$value2=$this->component->Caption;
$value2 = $this->component->Caption;
}
public function testSetProperty()
{
$value='new value';
$this->component->Text=$value;
$text=$this->component->Text;
$this->assertTrue($value===$this->component->Text);
$value = 'new value';
$this->component->Text = $value;
$text = $this->component->Text;
$this->assertTrue($value === $this->component->Text);
$this->setExpectedException('yii\base\Exception');
$this->component->NewMember=$value;
$this->component->NewMember = $value;
}
public function testIsset()
......@@ -74,123 +77,89 @@ class ComponentTest extends \yiiunit\TestCase
$this->assertFalse(isset($this->component->Text));
$this->assertFalse(!empty($this->component->Text));
$this->component->Text='';
$this->component->Text = '';
$this->assertTrue(isset($this->component->Text));
$this->assertTrue(empty($this->component->Text));
}
public function testHasEvent()
public function testOn()
{
$this->assertTrue($this->component->hasEvent('OnMyEvent'));
$this->assertTrue($this->component->hasEvent('onmyevent'));
$this->assertFalse($this->component->hasEvent('onYourEvent'));
}
public function testHasEventHandlers()
{
$this->assertFalse($this->component->hasEventHandlers('OnMyEvent'));
$this->component->attachEventHandler('OnMyEvent','foo');
$this->assertTrue($this->component->hasEventHandlers('OnMyEvent'));
}
public function testGetEventHandlers()
{
$list=$this->component->getEventHandlers('OnMyEvent');
$this->assertEquals($list->getCount(),0);
$this->component->attachEventHandler('OnMyEvent','foo');
$this->assertEquals($list->getCount(),1);
$this->setExpectedException('yii\base\Exception');
$list=$this->component->getEventHandlers('YourEvent');
}
$this->assertEquals(0, $this->component->getEventHandlers('click')->getCount());
$this->component->on('click', 'foo');
$this->assertEquals(1, $this->component->getEventHandlers('click')->getCount());
$this->component->on('click', 'bar');
$this->assertEquals(2, $this->component->getEventHandlers('click')->getCount());
public function testAttachEventHandler()
{
$this->component->attachEventHandler('OnMyEvent','foo');
$this->assertTrue($this->component->getEventHandlers('OnMyEvent')->getCount()===1);
$this->setExpectedException('yii\base\Exception');
$this->component->attachEventHandler('YourEvent','foo');
$this->component->getEventHandlers('click')->add('test');
$this->assertEquals(3, $this->component->getEventHandlers('click')->getCount());
}
public function testDettachEventHandler()
public function testOff()
{
$this->component->attachEventHandler('OnMyEvent','foo');
$this->component->attachEventHandler('OnMyEvent',array($this->component,'myEventHandler'));
$this->assertEquals($this->component->getEventHandlers('OnMyEvent')->getCount(),2);
$this->assertTrue($this->component->detachEventHandler('OnMyEvent','foo'));
$this->assertEquals($this->component->getEventHandlers('OnMyEvent')->getCount(),1);
$this->component->on('click', 'foo');
$this->component->on('click', array($this->component, 'myEventHandler'));
$this->assertEquals(2, $this->component->getEventHandlers('click')->getCount());
$this->assertFalse($this->component->detachEventHandler('OnMyEvent','foo'));
$this->assertEquals($this->component->getEventHandlers('OnMyEvent')->getCount(),1);
$this->assertTrue($this->component->detachEventHandler('OnMyEvent',array($this->component,'myEventHandler')));
$this->assertEquals($this->component->getEventHandlers('OnMyEvent')->getCount(),0);
$this->assertFalse($this->component->detachEventHandler('OnMyEvent','foo'));
$result = $this->component->off('click', 'foo');
$this->assertTrue($result);
$this->assertEquals(1, $this->component->getEventHandlers('click')->getCount());
$result = $this->component->off('click', 'foo');
$this->assertFalse($result);
$this->assertEquals(1, $this->component->getEventHandlers('click')->getCount());
$result = $this->component->off('click', array($this->component, 'myEventHandler'));
$this->assertTrue($result);
$this->assertEquals(0, $this->component->getEventHandlers('click')->getCount());
}
public function testRaiseEvent()
public function testTrigger()
{
$this->component->attachEventHandler('OnMyEvent',array($this->component,'myEventHandler'));
$this->component->on('click', array($this->component, 'myEventHandler'));
$this->assertFalse($this->component->eventHandled);
$this->component->raiseEvent('OnMyEvent',new \yii\base\Event($this));
$this->assertNull($this->component->event);
$this->component->raiseEvent();
$this->assertTrue($this->component->eventHandled);
$this->assertEquals('click', $this->component->event->name);
$this->assertEquals($this->component, $this->component->event->sender);
$this->assertFalse($this->component->event->handled);
$this->setExpectedException('yii\base\Exception');
$this->component->raiseEvent('OnUnknown',new \yii\base\Event($this));
$eventRaised = false;
$this->component->on('click', function($event) use (&$eventRaised) {
$eventRaised = true;
});
$this->component->raiseEvent();
$this->assertTrue($eventRaised);
}
public function testEventAccessor()
public function testHasEventHandlers()
{
$component=new NewComponent;
$this->assertEquals($component->onMyEvent->getCount(),0);
$component->onMyEvent='yiiunit\framework\base\globalEventHandler';
$component->onMyEvent=array($this->component,'myEventHandler');
$this->assertEquals($component->onMyEvent->getCount(),2);
$this->assertFalse($component->eventHandled);
$this->assertFalse($this->component->eventHandled);
$component->onMyEvent();
$this->assertTrue($component->eventHandled);
$this->assertTrue($this->component->eventHandled);
$this->assertFalse($this->component->hasEventHandlers('click'));
$this->component->on('click', 'foo');
$this->assertTrue($this->component->hasEventHandlers('click'));
}
public function testStopEvent()
{
$component=new NewComponent;
$component->onMyEvent='yiiunit\framework\base\globalEventHandler2';
$component->onMyEvent=array($this->component,'myEventHandler');
$component->onMyEvent();
$component = new NewComponent;
$component->on('click', 'yiiunit\framework\base\globalEventHandler2');
$component->on('click', array($this->component, 'myEventHandler'));
$component->raiseEvent();
$this->assertTrue($component->eventHandled);
$this->assertFalse($this->component->eventHandled);
}
public function testInvalidHandler1()
{
$this->component->onMyEvent=array(1,2,3);
$this->setExpectedException('yii\base\Exception');
$this->component->onMyEvent();
}
public function testInvalidHandler2()
{
$this->component->onMyEvent=array($this->component,'nullHandler');
$this->setExpectedException('yii\base\Exception');
$this->component->onMyEvent();
}
public function testDetachBehavior()
{
$component=new NewComponent;
$component = new NewComponent;
$behavior = new NewBehavior;
$component->attachBehavior('a',$behavior);
$this->assertSame($behavior,$component->detachBehavior('a'));
$component->attachBehavior('a', $behavior);
$this->assertSame($behavior, $component->detachBehavior('a'));
}
public function testDetachingBehaviors()
{
$component=new NewComponent;
$component = new NewComponent;
$behavior = new NewBehavior;
$component->attachBehavior('a',$behavior);
$component->attachBehavior('a', $behavior);
$component->detachBehaviors();
$this->setExpectedException('yii\base\Exception');
$component->test();
......@@ -198,15 +167,15 @@ class ComponentTest extends \yiiunit\TestCase
public function testAsa()
{
$component=new NewComponent;
$component = new NewComponent;
$behavior = new NewBehavior;
$component->attachBehavior('a',$behavior);
$this->assertSame($behavior,$component->asa('a'));
$component->attachBehavior('a', $behavior);
$this->assertSame($behavior, $component->asa('a'));
}
public function testCreate()
{
$component = NewComponent2::newInstance(array('a'=>3), 1, 2);
$component = NewComponent2::newInstance(array('a' => 3), 1, 2);
$this->assertEquals(1, $component->b);
$this->assertEquals(2, $component->c);
$this->assertEquals(3, $component->a);
......@@ -218,6 +187,7 @@ class NewComponent extends \yii\base\Component
private $_object = null;
private $_text = 'default';
public $eventHandled = false;
public $event;
public $behaviorCalled = false;
public function getText()
......@@ -227,30 +197,27 @@ class NewComponent extends \yii\base\Component
public function setText($value)
{
$this->_text=$value;
$this->_text = $value;
}
public function getObject()
{
if(!$this->_object)
{
$this->_object=new NewComponent;
$this->_object->_text='object text';
if (!$this->_object) {
$this->_object = new NewComponent;
$this->_object->_text = 'object text';
}
return $this->_object;
}
public function onMyEvent()
public function myEventHandler($event)
{
$this->raiseEvent('OnMyEvent',new \yii\base\Event($this));
$this->eventHandled = true;
$this->event = $event;
}
public function myEventHandler($event)
public function raiseEvent()
{
$this->eventHandled=true;
}
public function exprEvaluator($p1,$comp) {
return "Hello $p1";
$this->trigger('click', new \yii\base\Event($this));
}
}
......@@ -258,7 +225,7 @@ class NewBehavior extends \yii\base\Behavior
{
public function test()
{
$this->owner->behaviorCalled=true;
$this->owner->behaviorCalled = true;
return 2;
}
}
......@@ -268,6 +235,7 @@ class NewComponent2 extends \yii\base\Component
public $a;
public $b;
public $c;
public function __construct($b, $c)
{
$this->b = $b;
......
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