Events 2.0 - How To?

Hi all.

New event system looks very cool and promising.

Are there any "best practices" or guidelines for using yii2 events?

Suppose I want to log AR field changes. How should I do that?

Should I use old plain afterSave() right in my model? Should I use "global" events (Event::on)? Some third option?

And where should I place the handlers? Model? Module init? some other place?

Btw, speaking of field change log: it seems like afterSave is firing after _oldAttributes are set to new values, so there’s no way to check for isAttributeChanged(). Thus another question: what handler should I use?

You must know that every component has its own events system. You may attach and trigger so called "global" event:




Yii::$app->on($eventName, $handler);

....

Yii::$app->trigger($eventName);



And you can attach and trigger event on any component:




$this->on($eventName, $handler);

....

$this->trigger($eventName);



Events can not exist without component.

Yes, I understand that.

The question was, how and where should I attach event listeners?

For example, right now I’ve attached a couple right inside module’s init(). Is that a good place?

First off, you must find the place where you want trigger event (event is triggered by trigger() method). It is the place where you want to call any of your future callbacks. Later you will encounter situation when you will want trigger something in your place. And then you can attach event to that place via on() method.

Can I haz an example please?

For example, I want to attach an event to AR model for EVENT_BEFORE_UPDATE.




$yourAr->on('EVENT_BEFORE_UPDATE', $yourCallback); //$yourAr is your AR



You mean, this must be at controller?

BEFORE_UPDATE events are system-wide usually, so that they can fire if MyAR::save() happens in any controller.

(yes, I can use model’s beforeSave for that, but this is kinda oldschool)

If $yourAr is your AR then you must find out in you AR $this->trigger(‘EVENT_BEFORE_UPDATE’). Every time this code is executed your event will be executed too. If you AR is used in every controller then your internal $this->trigger(‘EVENT_BEFORE_UPDATE’) of your AR will be executed in every controller.

I’m losing it.

Ok, here’s my model:


<?php

namespace app\models;

use yii\db\ActiveRecord;


class MyModel extends ActiveRecord

{

}



and here’s the controller:




<?php


namespace app\controllers;


use app\models\MyModel;


class MyFirstController extends Controller

{

	public function actionTest()

	{

		$model = MyModel::find(1);

		$model->name = 'some text';

		$model->save()

		return 'done';

	}

}

and here’s another one:




<?php


namespace app\controllers;


use app\models\MyModel;


class MySecondController extends Controller

{

	public function actionTest()

	{

		$model = MyModel::find(2);

		$model->name = 'some another text';

		$model->save()

		return 'done';

	}

}

So I want to do something everytime MyModel::name changes.

How am I supposed to do that?

You may do




<?php


namespace app\controllers;


use app\models\MyModel;


class MySecondController extends Controller

{

        public function actionTest()

        {

                $model = MyModel::find(2);

               $mycallback = []; //It is your sample callback

                $model->on('after_name_changing', $mycallback);

                $model->changeName('some another text');

                $model->save()

                return 'done';

        }

}






<?php

namespace app\models;

use yii\db\ActiveRecord;


class MyModel extends ActiveRecord

{

        public function changName($name)

        {

                $this->name = $name;

                $this->trigger('after_name_changing');

        }


}



In your example $mycallback exists only in MySecondController, so it will not fire if MyFirstController is processing the request.

Also I see no sense of using such a complicated logic (create callback, then call model method, that fires this callback).

The question is, how should I create app-wide callbacks for events.

Here’s an example of what I (probably) need:


<?php

namespace app\models;

use yii\db\ActiveRecord;


class MyModel extends ActiveRecord

{

	public function init()

	{

		parent::init();

		$this->on('EVENT_BEFORE_UPDATE', function($event) {...});

		$this->on('EVENT_AFTER_UPDATE', function($event) {...});

		$this->on('EVENT_MY_EVENT', function($event) {...});

	}

}

I’m just not sure it’s the good way.

If you want to log changes for ALL AR classes, you should use class-level events. This should be during bootstrap process (you can create a PHP file, include it in your index.php, and put your event attaching code there).

You can attach attach both BEFORE_ and AFTER_ events. In the BEFORE_ event you save the current attributes. In AFTER_, you get the latest attributes and compare them with the one you saved to find out the changes.

Maybe you need override some methods to get what you want:




class MyModel extends ActiveRecord

{

        public function beforeSave($insert)

        {

                

/*


 Your extra code here


 */


                parent::beforeSave();

        }

}



Looking at the events documentation, I think you can just do


Event::on(MyModel::className(), ActiveRecord::EVENT_AFTER_INSERT, function ($event) {

    Yii::trace(get_class($event->sender) . ' is inserted.');

});

inside of EVENT_BEFORE_REQUEST.