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.