Yii2: Use 3rd party event library for events in 2.1

Created on 26 Jun 2017  路  55Comments  路  Source: yiisoft/yii2

The wiki mentions Events as a candidate for generic PHP packages. (ie removal from the core).
When looking on packagist, there are several big (in terms of # downloads) event-related packages:

  • symfony/event-dispatcher (54 million downloads, lots of dependencies)
  • zendframework/zend-eventmanager (11 million, no dependencies, good documentation, last commit < 6 months ago)
  • evenement/evenement (3.1 million, no dependencies, few docs, last commit > 3 years ago)
  • illuminate/events (2.5 million, some dependencies, last commit < 6 months ago)
  • league/event (1.9 million, no dependencies, good documentation, last commit > 1 year ago)

I've had good experiences with packages from The PHP League as well as their handling of contributions.
Zend EventManager seems to be well documented, maintained and its has a high number of installs.

Going through the documentation for ZendEventManager I feel it supports everything that Yii needs, and more. One thing it explicitly does not support that Yii does is listing event handlers; at Zend they removed this in the latest version stating that there is no real use case for it.
Yii often checks for event handlers before firing an event:
if (...hasEventHandlers()) { trigger the event. }
I feel we could live without it.

The specific library to use could even be made configurable by the developer. We could define a simple interface that is used by Component for use within Yii. Then anyone can implement a connector and use the library of their choice.

under discussion

Most helpful comment

There's PSR-14 about events management: https://github.com/php-fig/fig-standards/blob/master/proposed/event-manager.md. It's still in the draft but worth looking at.

All 55 comments

what about the introduction of some mechanism in which the developer can choose its own library?

@dynasource As my last paragraph mentions, that is definitely possible; still we should pick a default.

There's PSR-14 about events management: https://github.com/php-fig/fig-standards/blob/master/proposed/event-manager.md. It's still in the draft but worth looking at.

11389

@samdark, can you clarify usage of PSR-14?
In which way particular component (like yii\web\User or yii\db\ActiveRecord) should interact with Psr\EventManager\EventManagerInterface? Should every class which uses events (e.g. current yii\base\Component descendants) implement this interface or they should store event manager instance as an internal property?
What about class-level events? Does PSR-14 handles them somehow, or they should be dropped?
And what about issue #14978 then? Its implementation is possible only in case of single-application-level event dispatcher usage. But in case EventManagerInterface should appear in single instance, how developer will be able to attach event for particular class instance without affecting other ones, or he should not?

I'll check it in detail this week.

  1. Event manager should be a component by itself.
  2. Storing an instance (i.e. having it as dependency) sounds more like it.
  3. If you need events at class level you can create another manager so events are isolated. We may provide a trait for it but I'd avoid it because of #14978.
  4. I don't see many use cases for object-specific events.

agree to have an event manager

Event manager should be a component by itself.

If I understand it correctly, it means:

class Component extends BaseObject implements EventManagerInterface

This may cause the problems, since names of the methods reserved by EventManagerInterface might be too generic. For example: EventManagerInterface::attach() may be confused with MessageInterface::attach(),
EventManagerInterface::clearListeners() make confusion with Queue::listen()

If you need events at class level you can create another manager so events are isolated.

Sorry, but I do not understand this statement. According to PSR-12 Event firing is performed via EventManager::trigger(). Thus in case event manager belongs to compoent itself, some other abstract EventManager can not interfere in this process.
Following PSR either class-level or object-level events will be impossible, depending on decision whether event manager should belong to event source object, or be a global one.

I don't see many use cases for object-specific events.

Object specific event handles are used at yii2-authclient while creaing HTTP requests:
https://github.com/yiisoft/yii2-authclient/blob/master/BaseOAuth.php#L283

Enabling/disabling behaviors feature is based on the object-bound events.

@klimov-paul no. That means separate component. I'm against all components being event managers.

Enabling/disabling behaviors feature is based on the object-bound events.

True. Should be taken into account. To be honest, I forgot it :(

Tried implementing that draft PSR that exists ATM. It's a bit weird. In particular, detaching a handler. Asked PHP-FIG about its current state because now it's clearly far from being complete.

I can not see how PSR or any suggested 3rd party event libraries can replace current Yii Event system.

Yii introduces events as a object-based entity, e.g. event handler is attached per object.
As far as can see PSR-12 as well as symfony/event-dispatcher (Synfony), illuminate/events (Laravel), league/event(https://github.com/thephpleague/event) and, I suppose, all other libraries introduce events per unique name.

In particular: Yii attaches event handler per class instance:

$item = Item::findOne($id);
$item->on('beforeInsert', function($event) {...});

As an alternative Yii allows class-level events attachment:

Event::on(Foo::className(), Foo::EVENT_HELLO, function ($event) {
    var_dump($event->sender);  // displays "null"
});

While any dispatcher-based events use only event name:

use Symfony\Component\EventDispatcher\Event;

$dispatcher->addListener('acme.foo.action', function (Event $event) {
    // will be executed when the acme.foo.action event is dispatched
});

In Yii for event listener specification 2 entity are required:

  • event name
  • class name or object

while suggested Dispatcher-based libraries operates only event name.

It seems that usage of centralized event dispatcher makes object or class based events impossible.

Consider common Yii event related functionality: usage of TimestampBehavior for ActiveRecord. This behavior is attached for particualr ActiveRecord descendant class. Some ActiveRecord descendant may have it attached, while other should not. E.g. class User has TimestampBehavior, while class MenuItem has not.

It attaches handlers for events beforeInsert and beforeUpdate, which in PSR terms should be called active.record.before.insert and active.record.before.update or something.
Using centralized event-dispatcher I can set event handler only for particular event name, e.g. active.record.before.insert, which means this handler will be invoked for any ActiveRecord descendant, e.g. both for User and MenuItem without any segregation.

@SamMousa , @samdark can you provide some pseudo code, which uses PSR, symfony/event-dispatcher or illuminate/events, which replaces current TimestampBehavior functionality? How does this code look like?

Couldn't we make each object have its own emitter?
That emitter could then forward the events to the global / class level emitter.

Note that having more objects vs having larger objects doesn't really matter. (Currently we have Yii objects implementing an emitter and listener, this could easily be replaced by Yii objects owning an emitter object).

That way you have the best of both. I know @samdark doesn't agree with that approach as he mentioned earlier:

@klimov-paul no. That means separate component. I'm against all components being event managers.

In the end though, all classes being event managers is basically what we have now, only we use inheritance instead of composition.

I'll look at providing a (psuedo)code sample!

Yes. You're absolutely correct. These are, in fact two separate use cases:

  1. Application-level events. These are handled well by centralized dispatcher such as the ones you've referenced and having them centralized has certain pros. Especially for logging and debugging.
  2. Object-level events that we use for behaviors. I'm afraid none of the centralized event handlers could handle that except being used as @SamMousa noted that I don't really like... I'd, instead, go simpler way and use a trait for the purpose.

@samdark I don't "get" that argument though; using a separate class and combining that with a trait is still fine.

This is a very crude example.
trait EmitterTrait { private $_emitter; protected function getEmitter() { if (!isset($this->_emitter)) { $this->_emitter = new League\Emitter() || new PSR14Emitter()... // whatever you want here. } // In case you want to use setter / method injection: public function setEmitter(EmitterInterface $emitter) { $this->_emitter = new $emitter; } }
I'd rather have a simple trait that just creates the object than a trait that inserts the actual event handling code into the class.

So, we are talking about dropping existing functionality here, eliminating current behavior mechanism in favor of traits, aren't we?

In this case, it raises more questions than it solves: in which way trait will handle events (ActiveRecord in particular)? It can not simply override beforeInsert() method because it will make method name collision in case of attaching multiple traits to the single ActiveRecord class.

Also using events or behaviors in application configuration will become impossible.
At the present state following code may be used in application config:

return [
    'components' => [
        'user' => [
            'on beforeLogin' => function($event) {...}
        ],
        'formatter' => [
            'as extra' => [
                'class' => 'my\fomatter\extension\Behavior'
            ]
        ],
    ],
];

At the present state, any component can be extended using a behavior at configuration or DI level, while trait usage require new class declaration.
At the present state, event handlers are attached to component itself, instead of being declared at EventDispatcher component instead.

No, by trait I've only meant not extending from BaseObject. Nothing more in this context.

@samdark, what do you mean by Application-level events? What kind of term is this?
For example: is yii\web\User::EVENT_BEFORE_LOGIN is a application level event? My opinion: it is a object-level event: it will be raised as much times as amount of instantiated yii\web\User objects.
I may have multiple yii\web\User components in my application. For example: creating a web API I may have one component for the OAuth application tracking and access check and another one for the user himself. Their before.login events will have different meaning then.

No, by trait I've only meant not extending from BaseObject. Nothing more in this context.

So how TimestampBehavior should be implemented then? I can not see any way for it with centralized event dispatcher.

At the present state I can not see how any mentioned here 3rd party event library can be used without a functionality loss.

Either event-behavior approach should be re-considered or dropped or current event system remains intact.

Possible compromise might be usage of the centralized event dispatcher for class-level events. But it can not solve even such task, because all suggested solutions do not segragte events by sender class name.
Currently in Yii following is possible:

class MyWebUser extends \yii\web\User {...}

Event::on(MyWebUser ::className(),  \yii\web\User::EVENT_BEFORE_LOGIN, function ($event) {
    // triggered only for instances of `MyWebUser`, NOT triggered for `\yii\web\User`!!!
});

Event::on( \yii\web\User::className(),  \yii\web\User::EVENT_BEFORE_LOGIN, function ($event) {
    // triggered both for `MyWebUser` and `\yii\web\User` instances!!!
});

while with suggested replacements such functionality is unreachable.

img_20171211_145223

Application level events are global events accessible from any point of the application through central event dispatcher. yii\web\User::EVENT_BEFORE_LOGIN makes sense as application level event since there's only one web user at the same time. If there are multiple users as in your case, I'd prefix them i.e. user_before_login, api_before_getting_token. Currently, of course, there are no application level events at all.

So how TimestampBehavior should be implemented then? I can not see any way for it with centralized event dispatcher.

It can not be implemented using centralized event dispatcher since it's clearly not application level event.

usage of the centralized event dispatcher for class-level events

Should not be done. Class-level events are for internal usage and behaviors only.

I think we should pause until we get some POC i'm working on one atm

The closest well-known event system is HTML+JavaScript. It operates both event name and target HTML element. Consider following JQuery example:

// attaching handlers for the same event name - `click`:
$('div').on('click', function() {}); // multiple elements
$('#foo').on('click', function() {}); // single element
$('.some').on('click', function() {}); // multiple elements
$('li').last().on('click', function() {}); // single element

At the present state Yii event system has as much flexibility as HTML+JavaScript one: it can specify event handler for several objects as well as for single one. I can not see how usage of the centralized event manager, which operates only an event name, can be considered as an improvement.

Application level events are global events accessible from any point of the application through central event dispatcher.

This sounds like and event triggered by yii\base\Application class. This is already possible, by the way.
Such events are internal business of the application instance and should be incapsulated into an Application class.
It does not seem to be right that slave component such as yii\web\User should trigger an event on behalf of the application.

Here is a very crude proof of concept:
https://github.com/SamMousa/yii2/tree/poc-league-events

Try it by:

  1. Clone
  2. composer install
  3. php test.php test

The goal here is to show that the concepts used in Yii actually map very closely to a generic event handler library.

@klimov-paul centralized events system is not an improvement in terms of flexibility or features but it solves the problem of wildcard-collection of events data i.e. debugger events panel that collects all events or a handler that handles all db.* events. The main pro here is that you don't have to know which component triggered an event in order to subscribe to it. That's the main reason why I'm thinking about it.

@SamMousa fundamentally the prototype doesn't change anything except replacing actual implementation. See https://github.com/yiisoft/yii2/issues/14349#issuecomment-350717898 about why the topic was raised.

I know, it does however open the door to stuff like that.

We could implement traversal to the application (i'd rather listen to events on the application object than pollute the global namespace with static Event::on..)
for several entities.
A module could listen to all events on a controller it has created and emit them on the module.
The same goes for a module listening to events on its submodules.

When we have that working the only thing that's left is to subscribe to all events on the application object.

@samdark Zend EventManager supports the propagation / bubbling that I mentioned earlier via a SharedEventManager.

https://zendframework.github.io/zend-eventmanager/quick-start/

http://www.michaelgallego.fr/blog/2013/05/12/understanding-the-zend-framework-2-event-manager/

Yes, that could solve our problem. That's actually what's different between Yii 2.0 and jQuery. jQuery bubbles DOM hierarchy while Yii does not bubble runtime hierarchy.

Note that it won't solve the issue for AR though; it is the same problem as we run into with DI when it comes to AR.
AR objects don't have a reference to the application so they will need to trigger events globally via some service locator.

centralized events system is not an improvement in terms of flexibility or features but it solves the problem of wildcard-collection of events data i.e. debugger events panel that collects all events or a handler that handles all db.* events. The main pro here is that you don't have to know which component triggered an event in order to subscribe to it. That's the main reason why I'm thinking about it.

This problem can be solved easily simply acception an old proposal from #3250.
Wildcards can be used for both class name and even name matching. It does not need any revolution to be implemented. Such issue can be solved at 2.0.x in easy way.

Note that it won't solve the issue for AR though

So, there is no way it can fully replace current functionality.
I do not like to have such kind of change set just "to be" without coherit reason.

@klimov-paul it can replace it fully, it'll just use the same method that is currently used: global / static service locator.

@SamMousa the changeset your posted is just a redundant wrapper, which does not change anything. You creating multiple event managers (emmiters) per each class, while there is not utilized interface, which can be the only reason in favor of 3rd party library usage.

Also note that binding Component::trigger() to the application internal component, which EventManager is, creates a coupling, which means particular component unable to function without application instance created.

it can replace it fully

If it is so, where is the example for TimestampBehavior replacement I am asking for?

That's actually what's different between Yii 2.0 and jQuery. jQuery bubbles DOM hierarchy while Yii does not bubble runtime hierarchy.

Disagreed, Yii does bubble the runtime hierarchy.

1) All similar entities:

JQuery:

$('div').on('click', function() {}); // All div

Yii:

Event::on(ActiveRecord::className(),  'beforeInsert', function ($event) {
    // All `ActiveRecord` descendants, including `Item`, `User` and so on
});

2) Sub set of entities:

JQuery:

$('.some-class').on('click', function() {}); // All div with CSS class 'some-class'

Yii:

Event::on(Item::className(),  'beforeInsert', function ($event) {
    // All `Item` instances, excluding other ActiveRecord such as `User`
});

3) Single entity

JQuery:

$('#foo').on('click', function() {}); // particular single element

Yii:

$item = Item::findOne($id);
$item->on('beforeInsert', function() {...});

The missing ones are wildcards event names and wildcard class names:

1) All elements:

JQuery:

$(document).on('click', function() {}); // all elements 'click'

Yii:

Event::on('*',  'some.event', function ($event) {
    // All events named 'some.event' for any class
});

2) All events:

For JQuery I can not recall such fucntionality

Yii:

Event::on('*',  '*', function ($event) {
    // All events from all classes
});

Last 2 examples are easy to add. Can be done for Yii 2.0.x without any BC break or significant performance loss.

What I tried to illustrate is that we can map the current framework usage of events.
The component can either create their own emitter or have one injected, this is done using interfaces thus decoupling from a specific implementation.
The example I've shown actually includes a behavior.
The only difference for AR is that there is no way to application object and thus, in case you need global events you must pollute the global namespace somewhere; where you do it is irrelevant. The current Yii approach is to have a static Event class that manages these global events, something I have shown can be done using an external library as well.

This issue was created originally by me with a different goal then the new requirement to support wildcards. I'd rather have less code in the framework and reuse existing implementations.
That being said the new requirement can be implemented using bubbling either in the current event system or using any library. And some libraries, like Zend, already support it out of the box.

If it is so, where is the example for TimestampBehavior replacement I am asking for?

The TimestampBehavior isn't anything special, as you can see the behavior i've created works with all Yii events so it'll work even with class level events.

Also note that binding Component::trigger() to the application internal component, which EventManager is, creates a coupling, which means particular component unable to function without application instance created.

The coupling you mention is also there if you just implement everything inside the component class itself.
However if you use composition you can decouple from the implementation (not the interface).

@klimov-paul those examples do different kinds of bubbling.
You are bubbling across class hierarchies, that's most definitely not the same as bubbling from:
Action > Controller > Module > Application.

Yii2 does not do that at the moment.

You are bubbling across class hierarchies, that's most definitely not the same as bubbling from:
Action > Controller > Module > Application.

For what purpose this is needed? Usually 2 separated modules can not function at the same time during single request processing - it always ends with single controller and its trace back to application.
There is no need to process objects in DOM like style. Such functionality can make sense only for residental applications, like the one written on JSP or .NET.

At the present state there are 3 stages of the same event listening:

  • object based
  • event name based
  • class name based

Following code should fucntion:

Event::on(ActiveRecord::className(),  'beforeInsert', function ($event) {
    echo "ActiveRecord."
});

Event::on(Item::className(),  'beforeInsert', function ($event) {
    echo "Item."
});

$item = new Item();
$item->on('beforeInsert', function () {
    echo "Object"
});

$item->save(); // outputs: `ActiveRecord.Item.Object` !

With your proposal this can only in case object-based event manager invokes the global one as well. creating a coupling between them. Thus same event should be actually triggered twice: at object level and at application level, including 'stop propagation' logic and so on.

Replacement of the event manager at object level may cause such behavior loss in case developer do not consider it or use 3rd party manager directly. This makes global events unreliable.

I'd rather have less code in the framework and reuse existing implementations.

Sorry, but I can not detect significant code reducation in your code example: it jsut wraps relatively simple library into Yii interface with extra code to support class-based segregation. It looks like overcomplication to me at the present state.

For what purpose this is needed? Usually 2 separated modules can not function at the same time during single request processing - it always ends with single controller and its trace back to application.
There is no need to process objects in DOM like style. Such functionality can make sense only for residental applications, like the one written on JSP or .NET.

This reasoning is what causes Yii application to pollute the global namespace. Basically it's a chicken and egg problem; as long as it the global namespace is polluted you cannot run 2 applications at the same time. There are tickets related to this, for example using ReactPHP or similar async frameworks.
Having this kind of bubbling would actually allow application level events while having multiple active application objects simultaneously.

Sorry, but I can not detect significant code reducation in your code example: it jsut wraps relatively simple library into Yii interface with extra code to support class-based segregation. It looks like overcomplication to me at the present state.

Reduction is not necessarily the number of lines, but the complexity of them. Forwarding a call to a class that implements logic is not code that generally needs a lot of maintenance.

Also I've never suggested that that is how we should implement it, it's just a proof of concept showing that the concepts used by event libraries are not so different from the ones used in Yii. Yii is not so special that it really needs its own implementation...

I do not see how your coupling via static class calls is anything but a bad solution to a problem without really good solution... Having Event::on is the strongest kind of coupling there is.

you cannot run 2 applications at the same time. There are tickets related to this, for example using ReactPHP or similar async frameworks.

Yii can not be used in asynchronous way or a part of ReactPHP. Changing Event management will not solve it. Running several applications or controllers in parallel requires absolutely another framework structure.
Why all developers and applications should suffer performance degradation cause by code overcomplication in order to support hypothetical use case of converting Yii into ReactPHP?

Reduction is not necessarily the number of lines, but the complexity of them.

Sorry, but I can not see any complexity reducation neither: creating a separated abstraction layer around 3rd party library does not simplifies anything to me.

I do not see how your coupling via static class calls is anything but a bad solution to a problem without really good solution... Having Event::on is the strongest kind of coupling there is.

Setting something in the global scope requires some kind of global interface - there is no good solution for that. DI actually is just an another way of handling global data and function calls. It can be called a noble one or more advanced, but in the end it is the same.

Static call eliminates the necessity of object instantiation, although it can be solved in other way as well.

it's just a proof of concept showing that the concepts used by event libraries are not so different from the ones used in Yii.

Am I talking to myself here? There IS a significant difference: Yii suggests event is an object-based entity, which has both name and owner class bound, while all suggested 3rd party libraries consider events to be static entities, which have only a name. Yii segragates events per objects - not only per program execution stage.
The fact that events in all libraries have a name and sender does not make them all equal.
It is unwise to suggest that solution created for static events can substitute solution for object-based events. As well as approach from JSP and .NET can be good for PHP - we have already passed this back at Prado.

Am I talking to myself here? There IS a significant difference: Yii suggests event is an object-based entity, which has both name and owner class bound, while all suggested 3rd party libraries consider events to be static entities, which have only a name. Yii segragates events per objects - not only per program execution stage.

It is unwise to suggest that solution created for static events can substitute solution for object-based events.

Yii has an event manager per object instance since every object IS an event manager.
Not only have I suggested it, I've proven it by giving an example implementation. Whatever you say about the details of that implementation you cannot possibly try to argue that it does not exist??

Setting something in the global scope requires some kind of global interface - there is no good solution for that.

Exactly so if at all possible it should be avoided.
If there can only ever be 1 instance of the application (something you state)
and it is reachable via \Yii::$app (a fact I think we can both agree on)
then why would we ever need to further pollute the global namespace using static class state??!

Does it not make much more sense to trigger those events on the application object?
It does not allow me to run 2 applications at once, but at least I'll be able to destroy the application object and be sure that no state is lingering anywhere.

Very well. Since there is no other proposition or visible solution then PSR should be adopted as an event manager.

So, summarize:

  • Adopt PSR-12 for event and event manager
  • introduce event manager as a Module (which covers Application as well) internal component
  • create and interface and trait implementation for the class, which have its own EventManager, e.g. having getEventManager() method. Those entities should be used at yii\db\BaseActiveRecord.
  • eliminate static methods at yii\base\Event class in favor of application component
  • implement EventManager and some "slave" EventManager, which should be used at object level and fall back to application event manager as well

Related BC breaks:

  • behavior usage for components, like yii\web\User will be dropped, behavior will affect only classes, which have thier own event manager
  • event handlers specification at component config will be no longer available, Application::$eventManager should be used instead.

If there is no objections I consider issue to be "ready for adoption".

What's an "internal component"?

Regarding the BC breaks, we could still support event handlers via component configuration even with the new system, I think.

Why must we remove behavior support in components? Behaviors can work even without events, but could also check if their owner implements the event manager interface before adding event listeners.

What's an "internal component"?

It could be either belong to Module::$components list or be internal private field with setter and getter. It makes no signiifcant difference to me.

Regarding the BC breaks, we could still support event handlers via component configuration even with the new system, I think.
Why must we remove behavior support in components? Behaviors can work even without events, but could also check if their owner implements the event manager interface before adding event listeners.

That is impossible since everyone here insist that class yii\base\Component should be dropped in favor of traits.
This means random component can not have methods on() or attachBehavior() anymore. Particular component should not loger have any trace of event management in its public interface.

However this matter can be reconsidered in the scope of DI improvement in case configuration keys like on ... or as ... will be processed at Yii::createObject() level.

Ah, I understand.

I think that attachBehavior() is not directly related to events, though I will concede that in a lot of cases behaviors will very likely like to respond to events on their owner.

Of course we could have attachBehavior() in a trait as well; this trait could then use the event trait and add the features for behaviors, like attaching detaching and writing or reading properties.

I also suggest dropping class-level event at all, as they are not peculiar to the PSR and existing 3rd party event libraries. It seems to be redundant as in case of necessity instanceof check can be added inside particualr event handler.

More complex event names should be used with some namepace notation. E.g. ActiveRecord::EVENT_BEFORE_INSERT should be equal to yii.db.active.record.insert.before.

btw, is PSR-12 still active? it hasn't changed for 2 years... might not be the best to implement a PSR that has stalled and has no largely implementations

// PSR-12 is coding style guide I'm working on ;)

If you're talking about events PSR, as I've mentioned, it will change totally before acceptance and current variant isn't good at all. Should not be adopted.

Closing since Paul solved wildcard matching and PSR is far from being ready.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Mirocow picture Mirocow  路  56Comments

cebe picture cebe  路  53Comments

samdark picture samdark  路  63Comments

Ragazzo picture Ragazzo  路  44Comments

schmunk42 picture schmunk42  路  47Comments