Events

Currently in Yii 1 events are usable both to customize core behavior and to make applications and modules more flexible. Working with event is three steps:

  1. Declare it:

public function onNewComment($event) {

  $this->raiseEvent('onNewComment', $event);

}

  1. Raise it:



function addComment(Comment $comment){

  $event = new CModelEvent($this);


  $this->onNewComment($event);

  return $event->isValid;

}



  1. Attach handler:



$postModel = Post::model()->findByPk(10);

$notifier = new Notifier();

$postModel->onNewComment = array($notifier, 'comment');



The only thing I don’t like in this is the fact that default event handler is empty in most cases. So I guess we can reduce process to the following:

  1. Raise event:



function addComment(Comment $comment){

  $event = new CModelEvent($this);


  $this->raiseEvent('onNewComment', $event);

  return $event->isValid;

}



  1. Attach handler:



$postModel = Post::model()->findByPk(10);

$notifier = new Notifier();

$postModel->onNewComment = array($notifier, 'comment');



The main reason for the declaration are:

  • validation: When you access $postModel->onNewComment, it needs a way to know onNewComment represents an event;

  • documentation: Doc generator needs to know what kind of event parameter is associated with what event.

  • override: provides a way for child classes to override the event. For example, a child event may change the type of event parameter and add some additional data.

Considering the fact that events are not very common, I think explicit declaration (even though repetitive) is better than implicit one. In 5.3, the declaration can be further simplified as:




function onClick($event) {

	$this->raiseEvent(__METHOD__, $event);

}



OK. What if we’ll simplify events system to use a single events registry?

For example, we’ll be able to raise any event from any component and subscribe to any event w/o knowing what raises it and w/o having an object at all?




Yii::app()->raiseEvent('notification', $event);






Yii::app()->registerHandler('notification', function($event){

  // do something

});



This (I name it as global event) could be a complement to the existing component-based event system, but not to replace it.

The current event system allows you to attach event handlers via property configuration, which is not possible via this global event system.

This global event system needs to introduce namespace to avoid name conflict. Also, the documentation will be a problem, although this can be solved by introducing new doc syntax (like @property).

I think we need to see the actual needs before introducing a new feature.

I don’t see a reason why this isn’t possible to configure global events with config:


'events' => array(

  'eventName' => array('MyClass', 'myMethod'),

),



Right. Also it introduces a single array lookup per event raised plus some memory to store handler bindings. Maybe doesn’t worth changing.

Could a global event registry be used to realize notifications between modules/ controllers?

Ben

Yes, that was the idea.

The main problem with the application-wide event distribution system (publisher/subscriber model) you’re proposing, is that objects have to subscribe.

In a dynamic language like PHP, this becomes a problem, because in order to subscribe to an application-wide event, the subscriber has to exist - which means you have to load/create many objects, just so they can sit there and listen.

You don’t which ones, or how many of those, are going to get called into play, but you have to create them all, “just in case”, which is something you want to avoid in PHP, at all possible cost.

The answer to that problem, frequently is to use only static event-handlers; that way, you can subscribe in advance, and the event handler class can late-load when receiving it’s first notification, e.g. setting the event handler to MyHandler::myMethod() rather than $object->method().

I have been working on a publisher/subscriber architecture of this type for quite some time, and it is a radically different beast. I’m not saying we couldn’t or shouldn’t support something like that, but just to let you know, I think you will find there are considerable architectural differences between object-based events such as we have in Yii at the moment, and a publisher/subscriber model such as you’re proposing. The seem similar at a glance, but I personally have found that the emerging requirements are very different - there are far more subtle varieties in event-driven models than I had imagined…

Why not subscribe on an ID + eventName base? Since many objects in yii’s architecture are basically used as singletons (controllers, components, the application object itself, modules), it wouldn’t be much different from the static approach.

I think about something like this:




'modules' => array(

  'user' => array(

    [...]

  ),

  'admin' => array(

    'subscribeTo' => array(

      // based on ID + event name, where ID is a prefix + key

      array( 'modules.user', 'onUserRegistered', 'askForActivation' )

    ),

  ),

),



Now, if UserModule’s onUserRegistered event is emitted, the event dispatching logic asks the global eventRegistry (which could be a predefined application component) if any other modules, components, … have registered to the event. The relevant object could be lazy instantiated and the eventHandler could be invoked.

One drawback might be, that the configuration for all components (modules, …) needed to be parsed for the ‘subscribeTo’ key upon each request. On the other hand, configuration is something that can be cached very well, since it doesn’t change frequently.

Of course, things become difficult once we start to deal with real "objects" with possibly many instances like AR models.

Just one of countless issues that account for small variations in the design of a static event system.

I’m honestly not sure it’s a good idea to support something like this - global static pub/sub has too many possible variants. The current existing event system serves just fine as it is; you mentioned that there are many entities you consider static, so using the existing event architecture, you can consider their events static too, which should account for most use-cases, and already seems to work well, given that the use of events is fairly limited.

I would in fact build the “global registry” (as I’ve said: simply another app component, preloaded) on top of the current event mechanism. It’s only the connection between the different entities which I feel is missing and which could be easily closed.

Its main task would be lazy instantiating and initializing the subscribers and then forwarding the event to them. Nothing special about that. Maybe this could already be implemented in the current version as an extension…

Another thing that would be nice was some magic to resolve event parameters to method parameters. Similar to the action parameter binding feature. The main goal of this would be to use "event handlers" for both event handling and as normal API.

Referring to the example from above, adminModule’s askForActivation-method would look like this:




public function askForActivation(CEvent $event)

{

  if (isset($event->params['userId']))

  {

    // whatever

  }

}



It could also look like this, if user module provided specialized events:




public function askForActivation(UserEvent $event)

{

  // we know UserEvent has a property "userId"

}



But what I preferred was a solution, that hides if the method was triggered by an event or by simply calling it:




public function askForActivation( $userId )

{

  // whatever

}



I like the idea, looks very useful

I was working with roundcube the last 2 days and it has someting like this too, a global event handler

the implementation could be something like this:

using Ben’s way to define unique ids to events

CGlobalEvent class extends CComponent

define as an application component named event

events that needs a global handler are declared like


function onBeforeSave($event){

  $this->raiseEvent('onBeforeSave',$event);

  Yii::app()->event->raiseEvent('model.onBeforeSave',$event);

}

so anywhere in your app you can attach an event handler, like


Yii::app()->event->attachEventHandler('model.onBeforeSave',array($myComponent,'myMethod'));

that could easiely be done as an extension, but having this in the core is better. To use as a component, you need to override or add a behavior to CModel, CActiveRecord, etc.

What do you think ?

The main problem is, that the code which attaches to an event will (most of the times) not be reached before the event itself fires. Compare to my example above. At the time a user registers, the admin module won’t be loaded. So if it tried to register to the user-modules event somewhere in AdminModule::init(), it would never receive the event.

That’s why there needs to be a global registry, which knows all senders and receivers (by name, not necessarily by instance), is able to create them on demand and can forward events to them.

For consistency, the ‘subscribeTo’ attribute I used above should be removed. It’s not a attribute of the module or component and it isn’t meant to be evaluated when the entity is created, but at startup. Although I liked the connections to be made where the receiver is configured, this would cause confusion.

A better way would be:




'preload' => array( 'log', 'events' ),

'components' => array(

  'log' => array(

    'class' => 'CLogRouter',

    [...]

  ),

  'events' => array(

    'class' => 'CEventRegistry',

    'connections' => array(

      // compare http://doc.qt.nokia.com/latest/qobject.html#connect

      array( 'module.user', 'onUserRegistered', 'module.admin', 'askForActivation' ),

    ),

  ),

),

'modules' => array(

  'user' => array(

    [...]

  ),

  'admin' => array(

    [...]

  ),

),



In its init() method, CEventRegistry would then register to UserModule::onUserRegister. Once it receives the event, it looks up all registered receivers from its config (AdminModule), creates them as necessary and calls the configured methods (probably with event params resolved to method params).

This way, there wouldn’t even be need for a second raiseEvent in the sender.

/////////////////////////////////////

// Edit:

Problem identified:

Imagine we work in the admin module. Now, there’s no need to instantiate the user module. But the EventRegistry would load it in order to attach to its onUserRegistered-event.

Possible solution:

When application creates and initializes a component, it fires an event onComponentInitialized. EventRegistry initially only registers to this event. Then, every time a component is loaded, it receives the notification from application and registers for the configured events with the newly loaded component.

/////////////////////////////////////

// Edit 2:

Yesterday, I tried to implement such an eventRegistry based on the current version of yii. Since I couldn’t find a way to observe component creation, I used the eventInterceptor to notify the registry (which I named “EventBridge”) about events emitted by other components. The drawback is, that you now need to assign a behavior to every component or module that should act as an event source (NotifyEventBridgeBehavior). Also, the EventInterceptor needs to search for the events of the component it is attached to, which isn’t ideal both in terms of speed and accuracy (it only finds “declared” events - methods that start with “onXyz”).

Other problems that I didn’t think of before coding are:

  • The event object doesn’t contain the name of the event. So the bridge has its difficulties to distribute the event, since it doesn’t know which event it just received. This problem is solved by use of the EventInterceptor, which encapsulates the eventName of the intercepted event.

  • Components don’t know about their “id” in config. Again, this introduces problems for the registry, which doesn’t know events from which source it is expected to distribute. The only way I found to work around this was to configure the behavior which is needed to be attached to the components with the id of the component. This is of course a very bad solution…

The setup is basically the same as described above:




'components' => array(

  'eventBridge' => array(

    'class' => 'application.components.EventBridge',

    'connections' => array(

      array('component.test', 'onSomethingHappened', 'module.moduleId', 'someEventHandler'),

    ),

  ),

  'test' => array(

    'class' => 'application.components.EventEmittingComponent',

    // since this component acts as event source, it needs the behavior attached.

    // could be removed once we find a way to get notified about component creation (including id of the component)

    'behaviors' => array(

      'notifyEventBridge' => array(

        'class' => 'application.behaviors.NotifyEventBridgeBehavior',

        // behavior stores id of the component. If you know a better way, please let me know

        'componentId' => 'test',

      ),

    ),

  ),

),

'modules' => array(

  // normal module. nothing special here. needs to define the configured "someEventHandler(CEvent)"

  'moduleId' => array(...)

)



When defining connections, you can use the prefixes “component”, “module” and “application” for both event source and target. The first two need to be postfixed with “.[key]”, “application” doesn’t take a postfix. Nested modules or components inside modules are not supported. Source code is attached.

Currently, events declaration is pretty verbose, I think that it would be good to simplify it.

Hello,

maybe we should use an dispatcher-object. For example take a look at Symfonys Dispatcher.

Then we can let the developers decide whether to use global or local dispatchers by adding a yiiapp-variable and if it is set to true an yii applicationDispatcher have to be specified.

CComponent could look like that:


 

class CComponent { 

  private $dispatcher = null; 

 

  public function setDispatcher($dispatcher){ 

    $this->dispatcher = $dispatcher; 

  } 

 

  protected function notify($event){ 

    $dispatcher = $this->getDispatcher(); 

    if(null !== $dispatcher){ 

      $dispatcher->notify($event); 

    } 

  } 

 

  public function getDispatcher(){ 

    if(null != $this->dispatcher){ 

      return $this->dispatcher(); 

    } 

    if(true == Yii::app()->useGlobalDispatcher){ 

      return Yii::app()->dispatcher; 

    } 

    return null; 

  } 

} 



If i like to response to an beforeSave-event it could look like that


 

$dispatcher = new Dispatcher(); 

$dispatcher->connect('Address.beforeSave', array($this, 'myFunction')); 

 

$model = Address::model()->findByPK(1); 

$model->setDispatcher($dispatcher); 

$model->save(); 



The big advantage of the symfony dispatcher is in my opinion that you cold ask all connected listens to filter

(e.g. change) a given value instead of only inform that something will be or is done.

So the core-developers could ask all listens to adjust the given object before it will be

processed further. The benefit is that i as a programmer don’t have to extend a core-component, but only

have connect me to the corresponding event. Please refer to the Symfony example under "Modifying Arguments"

Greetings from Germany

me23