Как использовать событие ( человеческим языком ) ?

Общие вопросы по использованию второй версии фреймворка. Если не знаете как что-то сделать и это про Yii 2, вам сюда.
Ответить
zxczxc12
Сообщения: 133
Зарегистрирован: 2013.01.24, 21:16

Как использовать событие ( человеческим языком ) ?

Сообщение zxczxc12 » 2016.02.27, 08:16

Hi

Прочитал все доки по событиям , даже просмотрел 2-х часовой курс от Дмитрия Елисеева , но нифига не понятно как РЕАЛЬНО применять события .

Может кто нить посоветовать как реализовать следующее :

Есть сайт , и нужно писать в лог клики юзеров по нему ( лог трафа )

Как я планировал это сделать :

Все контроллеры у меня наследуются от BaseController , а по сему, было бы замечательно сразу после init() делать вызов события типа EVENT_PAGE_REQUEST и вызывать метод класса countRequestPage который бы считал клик.

Вот мой BaseController

Код: Выделить всё

const EVENT_PAGE_REQUEST='EVENT_PAGE_REQUEST';

    public function init() {
        parent::init();

        $this->trigger($this::EVENT_PAGE_REQUEST);
    } 

и в файл конфигурации conf/web.php я присобачил следующее:

Код: Выделить всё

...
'bootstrap' => ['log', 'maintenanceMode'],

    'on EVENT_PAGE_REQUEST' => ['common\modules\partnership\common\models\TrafficLog', 'countRequestPage'],

    'modules' => [
    ....
 

Но ничегошеньки не происходит :-(

Я в ярости ибо по идее должно быть все просто , но везде в мануалах нет какой то досказанности как именно , конкретно , применять эти вещи .

Помогите советом

PS Пока сделал вот так

Код: Выделить всё

const EVENT_PAGE_REQUEST='EVENT_PAGE_REQUEST';

    public function init() {
        parent::init();

        TrafficLog::countRequestPage();
    } 
но хотелось бы не привязываться к конкретному модулю а иметь таки глобальное событие на которое можно было бы реагировать где угодно


Спасибо

Аватара пользователя
samdark
Администратор
Сообщения: 8794
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Как использовать событие ( человеческим языком ) ?

Сообщение samdark » 2016.02.27, 14:49

'on EVENT_PAGE_REQUEST' в вашем конфиге навешивает обработчик на Application. Этот обработчик обрабатывает события Application. Вы же выкидываете событие через $this->trigger, то есть для базового контроллера. См. http://www.yiiframework.com/doc-2.0/gui ... bal-events

Аватара пользователя
ElisDN
Сообщения: 4998
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Как использовать событие ( человеческим языком ) ?

Сообщение ElisDN » 2016.02.27, 14:59

Вы навесили 'on EVENT_PAGE_REQUEST' к объекту приложения Yii::$app, а trigger вызываете в объекте контроллера $this. Поэтому ничего не происходит. Но, первым делом, можно порассуждать, куда мы можем события поместить и откуда дергать.

Всё зависит от того, что Вы хотите отслеживать: запрос к приложению, запрос к модулю или запрос к контроллеру.

Так, в зависимости от ситуации, можно напридумывать кучу способов:

Либо оставить своё событие:

Код: Выделить всё

'on pageRequest' => ['common\modules\partnership\common\models\TrafficLog', 'countRequestPage'], 
но генерировать его для самого Yii::$app:

Код: Выделить всё

Yii::$app->trigger(self::EVENT_PAGE_REQUEST); 
Либо перенести событие в модуль и в init() самого модуля цепляться на $this->on(...), вызывая из BaseController метод модуля:

Код: Выделить всё

$this->module->trigger(...); 
Либо оставить код в контроллере как есть, но навеситься глобально:

Код: Выделить всё

Event::on(BaseController::EVENT_PAGE_REQUEST, ['...']); 
Но всё это - костыли и компромиссы. Первый не соответствует простейшей логике использования всего по назначению и привносит обратную лишнюю зависимость модуля от приложения. Второй - верный, но его можно упростить, отказавшись от собственного события. Но всё равно к задаче глобального трекинга он не подходит. Третий лёгок, но не слишком явен и сильно мешает тестированию, так как это глобальный синглтон, умеющий только накапливать обработчики.

Как сделать это более-менее явно?

В системе уже есть события, генерируемые разным объектами (приложением, модулями, контроллерами и прочими). Добавлять события в другие объекты мы не должны (за исключением работы через общую шину событий, но это не наш случай), и от этого они там выполняться не начнут. Поэтому логичный вариант - подписаться на события объекта, который их сам генерирует.

Если нужно отловить вообще каждый запрос отовсюду, то навесьтесь на встроенное событие EVENT_BEFORE_REQUEST (или EVENT_AFTER_REQUEST) либо EVENT_BEFORE_ACTION (или EVENT_AFTER_ACTION) самого приложения в конфиге:

Код: Выделить всё

[
    'on beforeRequest' => ['common\modules\partnership\common\models\TrafficLog', 'countRequestPage'],
] 
В других случаях помещение своих собственных событий в Yii::$app засоряет этот объект приложения (используя его ещё и как общую глобальную шину). Незачем в объект класть то, чего у него нет и подо что он не приспособлен.

Либо, если нужно отслеживать переходы только в модуле, навесьтесь аналогично на EVENT_BEFORE_ACTION или EVENT_AFTER_ACTION самого модуля в его методе init() для своих обработчиков:

Код: Выделить всё

class MyModule extends Module
{
    public function init() {
        parent::init();
        $this->on(self::EVENT_BEFORE_ACTION, [$this, 'countRequestPage']);
    }
    
    public function countRequestPage(Event $event) {
        ...
    }
} 
или в конфиге для чужих:

Код: Выделить всё

[
    'modules' => [
        'my-module' => [
            'class' => '...',
            'on beforeRequest' => ['...\OrherTrafficLog', 'countRequest'],
        ].
] 
Оба варианта позволяют легко избавиться от такого BaseController вообще.

Если же событие возникает только в одном контроллере (а не в базовом BaseController, от которого у Вас наследуются другие), то поместите его именно в этот контроллер. Но такое редко бывает нужно, так как нормальный контроллер - это расходный материал, никакой собственной логики не имеющий.

В любом случае следуйте здравому смыслу. У объекта могут быть его встроенные свойства (имя, цвет), его методы (открыть, закрыть), его события Events (проголодался, завял) и, возможно, его исключения Exception (не доел). Всё скомпоновано внутри объекта и живёт своей жизнью. Он нам сам говорит методом trigger(), что он завял или проголодался, а не мы ему так сделать приказываем. И только внешний код уже устанавливает его свойства, вызывает его методы и подписывается через on() на его внутренние события.

Так что стремитесь, чтобы события лежали в самих объектах, чтобы метод trigger() вызывался только внутри объекта им самим (представьте, что он protected, а не public) и чтобы любое навешивание чего-то на объект происходило только снаружи.

Так что события (Events) и исключения (Exceptions) (как и просто свойства и методы) полезны для односторонней коммуникации внешних систем с внутренними. А неконтролируемая встречная связка одноуровневых объектов напрямую друг с другом (через тот же Event::on()) без парящего над этим всем дирижёра нередко приводит к хаосу циклических зависимостей.

zxczxc12
Сообщения: 133
Зарегистрирован: 2013.01.24, 21:16

Re: Как использовать событие ( человеческим языком ) ?

Сообщение zxczxc12 » 2016.02.28, 06:07

Sam Dark писал(а):'on EVENT_PAGE_REQUEST' в вашем конфиге навешивает обработчик на Application. Этот обработчик обрабатывает события Application. Вы же выкидываете событие через $this->trigger, то есть для базового контроллера. См. http://www.yiiframework.com/doc-2.0/gui ... bal-events

Был я в том мануале и не раз

Ничего не понятно там :-(

zxczxc12
Сообщения: 133
Зарегистрирован: 2013.01.24, 21:16

Re: Как использовать событие ( человеческим языком ) ?

Сообщение zxczxc12 » 2016.02.28, 06:35

Во первых большое спасибо за детальное объяснение .

Но , первое :
Либо оставить код в контроллере как есть, но навеситься глобально:

Код: Выделить всё

Event::on(BaseController::EVENT_PAGE_REQUEST, ['...']);
Вот по мне , неправильная такая подача объяснений , не понятно куда "весить".
Куда конкретно прописывать это ? Я имею в виду схематически .
Всё скомпоновано внутри объекта и живёт своей жизнью. Он нам сам говорит методом trigger(), что он завял или проголодался, а не мы ему так сделать приказываем.И только внешний код уже устанавливает его свойства, вызывает его методы и подписывается через on() на его внутренние события.
Аналогия интересна , только непонятен ключевой момент, ибо "мы " , я так понимаю , и есть внешний код . Получается коллизия .

Что касается предложений использования системных событий , мне мысль и принципы ясны .

Все же некоторые мутности событий в плане необходимости в том или ином месте продолжает меня выводить из себя :-(

Посему останусь я с моим изначальным вариантом без всяких событий . В настоящий момент мне кажется это самый понятный способ без извращений .

Ещё раз спасибо !

Аватара пользователя
samdark
Администратор
Сообщения: 8794
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Как использовать событие ( человеческим языком ) ?

Сообщение samdark » 2016.02.28, 14:54

Без событий лучше всего, если без них можно обойтись.

zxczxc12
Сообщения: 133
Зарегистрирован: 2013.01.24, 21:16

Re: Как использовать событие ( человеческим языком ) ?

Сообщение zxczxc12 » 2016.03.02, 06:50

Меня немного попустило и опять с вопросом :-)

C логированием трафа я разобрался но вопрос применения событий мне не даёт покоя :-)

Вопрос теперь такой . У меня есть модель TransactionLog в ней есть поле статуса .
Статус может меняться в разных местах :
- В бекенда ( админке ) где администратор может выставить что товар заказан /отправлен / полученs деньги за него и тп
- Во фронтенде где юзер делает предзаказ / подтверждение заказа
- В фоне где приходит сообщение от платежной системы что оплата за товар поступила и тп

При всем при этом , есть модуль партнерской программы которая должна получать сигнал о изменении статуса заказа и делать свою логику по начислению процентов партнеру и тп ( в зависимости от полученного нового статуса транзакции )

Мне кажется что это тот случай где нужно использовать события и если это так , то у меня осталась наверное последняя проблема в понимании реального использования событий и это - как навешивать хандлеры , в данном случае на модель TransactionLog что бы срабатывал метод insertUserAction в модели ActionLog модуля партнерской программы ?

Жестко прописывать работу с моделью ActionLog партнерского модуля из модели TransactionLog нельзя ибо этого модуля может и не быть .

Я прописал в модели TransactionLog следующее:

Код: Выделить всё

public function afterSave($insert, $changedAttributes)
    {
        parent::afterSave($insert, $changedAttributes);

          $this->trigger($this::EVENT_STATUS_CHANGE);
    }
Но возникла жесткая проблема с вопросом куда прописать хандлер что бы сработало событие в модуле

Подскажите плиз как/куда приткнуть хандлер что бы срабатывала реакция на событие ?

( по предыдущим ответам , как вариант , я понял что можно использовать глобальную шину Yii::$app
Но моя тяга к перфекционизму серьёзно восприняла критику использования данного метода .
Есть ли другой способ ?

Спасибо !

Аватара пользователя
samdark
Администратор
Сообщения: 8794
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Как использовать событие ( человеческим языком ) ?

Сообщение samdark » 2016.03.02, 14:36

Есть много способов. В любом случае стоит завести что-то типа компонента EventDispatcher, который прописать в preload. Его задача — навесить обработчики на события. То есть кроме этого компонента никто не будет знать, какое событие что у нас обрабатывает.

Аватара пользователя
ElisDN
Сообщения: 4998
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Как использовать событие ( человеческим языком ) ?

Сообщение ElisDN » 2016.03.02, 23:56

zxczxc12 писал(а):При всем при этом, есть модуль партнерской программы которая должна получать сигнал о изменении статуса заказа и делать свою логику по начислению процентов партнеру и тп ( в зависимости от полученного нового статуса транзакции )
Sam Dark писал(а):В любом случае стоит завести что-то типа компонента EventDispatcher, который прописать в preload...
Как раз сейчас дописываю философскую статью про неявное связывание модулей через события на основе этой темы. Подпишитесь пока, чтобы не пропустить. А пока, как и сказали, можете захардкодить в предзагрузке:

Код: Выделить всё

class Bootstrap implements BootstrapInterface
{
    public function bootstrap($app)
    {
        Event::on(TransactionLog::EVENT_STATUS_CHANGE,  [$this, 'onTransactionLogStatusChange']);
    }
    
    public function onTransactionLogStatusChange(Event $event)
    {
        $transactionLog = $event->sender;
        ...
    }
} 
и подключить в конфиге:

Код: Выделить всё

'bootstrap' => [
    'log',
    'app\bootstrap\Bootstrap',
] 
Если не пишете тесты, то костыль Event::on() подойдёт. А про альтернативы в статье опубликую.

mkramer
Сообщения: 527
Зарегистрирован: 2014.12.14, 13:02

Re: Как использовать событие ( человеческим языком ) ?

Сообщение mkramer » 2016.03.03, 00:45

Интересно, что даже Sam Dark не посоветовал автору изначально использовать уже существующее событие - beforeAction, задать его для Application, и всё. Зачем тут своё событие городить, когда уже есть стандартное. zxczxc12, события в Yii бывают разные - уровня экземпляра, уровня класса, глобальные. Событие уровня экземпляра вызывается для конкретного объекта конкретного класса, т.е.

Код: Выделить всё

$a = new A; $b = new A;
$a->on("eventName", function () {});
$a->trigger("eventName");
 
вызовет событие только для A, если оно навешено как событие уровня экземпляра. А ещё можно навесить обработчик на весь класс, тогда для любого объекта этого класса будет вызываться обработчик:

Код: Выделить всё

Event::on(A::className(), function () {});
 
Навешивать его надо до того, как оно произойдёт. Можно к примеру в том же beforeAction.

И наконец глобальные события навешиваются на \Yii::$app

Аватара пользователя
samdark
Администратор
Сообщения: 8794
Зарегистрирован: 2009.04.02, 13:46
Откуда: Воронеж
Контактная информация:

Re: Как использовать событие ( человеческим языком ) ?

Сообщение samdark » 2016.03.03, 12:28

В beforeAction вешать можно. Я просто дал немного более широкий вариант. Вдруг что-то выкинет событие ещё до этого.

zxczxc12
Сообщения: 133
Зарегистрирован: 2013.01.24, 21:16

Re: Как использовать событие ( человеческим языком ) ?

Сообщение zxczxc12 » 2017.12.22, 14:09

Опять 25 :|

Вот что есть :

Код: Выделить всё

class MessageTopicForm extends Model
{
	const EVENT_NEW_MESSAGE = 'addNewMessage';

	public function save()
    {
    	$this->trigger(self::EVENT_NEW_MESSAGE);
    }
}
в web.php

Код: Выделить всё

$config = [
...
'on ' . \common\modules\forum\frontend\models\forms\MessageTopicForm::EVENT_NEW_MESSAGE => function () {
        print "test-1";
        exit;
    },
...
]
событие не подхватывается

почему ?

( прям проклятие какое-то с этми событиями )

urichalex
Сообщения: 732
Зарегистрирован: 2015.08.07, 11:03

Re: Как использовать событие ( человеческим языком ) ?

Сообщение urichalex » 2017.12.22, 14:25

по тому, что событие не глобальное

urichalex
Сообщения: 732
Зарегистрирован: 2015.08.07, 11:03

Re: Как использовать событие ( человеческим языком ) ?

Сообщение urichalex » 2017.12.22, 14:28

Код: Выделить всё

'bootstrap' => [
	\common\components\NewMessage::class
]

// \common\components\NewMessage
use common\modules\forum\frontend\models\forms\MessageTopicForm;
class NewMessage implements BootstrapInterface
{
	public function bootstrap($app)
	{
		Event::on(MessageTopicForm, MessageTopicForm::EVENT_NEW_MESSAGE, function() {
			print "test-1";
        		exit;
		});
	}
}

zxczxc12
Сообщения: 133
Зарегистрирован: 2013.01.24, 21:16

Re: Как использовать событие ( человеческим языком ) ?

Сообщение zxczxc12 » 2017.12.22, 14:44

urichalex писал(а):
2017.12.22, 14:28

Код: Выделить всё

'bootstrap' => [
	\common\components\NewMessage::class
]

// \common\components\NewMessage
use common\modules\forum\frontend\models\forms\MessageTopicForm;
class NewMessage implements BootstrapInterface
{
	public function bootstrap($app)
	{
		Event::on(MessageTopicForm, MessageTopicForm::EVENT_NEW_MESSAGE, function() {
			print "test-1";
        		exit;
		});
	}
}

Спасибо !

Но опишу тогда задачу всю.

Хочу сделать Timeline где будут фиксироваться всякие действия юзеров ( регистрация, лайки фото, комментарии и тп ). Сами действия могут быть как в модулях( как правило собственно написанных ) так и нет

Поэтому надо как то перехватывать нужные события откуда бы они не шли ( я так понимаю глобально )

Правильно ли я понимаю что мне нужно все нужные мне "перехватчики" событий нужно разместить здесь :

Код: Выделить всё

public function bootstrap($app)
	{
....
}
?

Или есть ещё какие решения этой задачи ?

Аватара пользователя
ElisDN
Сообщения: 4998
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Как использовать событие ( человеческим языком ) ?

Сообщение ElisDN » 2017.12.22, 15:24

zxczxc12 писал(а):
2017.12.22, 14:44
Сами действия могут быть как в модулях( как правило собственно написанных ) так и нет
Если нужно и в чужих, то только глобально через Event::on, если там автор не предусмотрел неглобальные.
zxczxc12 писал(а):
2017.12.22, 14:44
Поэтому надо как то перехватывать нужные события откуда бы они не шли (я так понимаю глобально)
Да.
zxczxc12 писал(а):
2017.12.22, 14:44
Правильно ли я понимаю что мне нужно все нужные мне "перехватчики" событий нужно разместить здесь
Правильно.
zxczxc12 писал(а):
2017.12.22, 14:44
Или есть ещё какие решения этой задачи ?
В других фреймворках для централизованной обработки используют общий объект EventDispatcher. В Yii его нет, и на каждый объект нужно навешиваться отдельно: напрямую через $object->on() либо глобально статически через Event::on().

zxczxc12
Сообщения: 133
Зарегистрирован: 2013.01.24, 21:16

Re: Как использовать событие ( человеческим языком ) ?

Сообщение zxczxc12 » 2018.01.23, 12:27

Общем полностью разобрался ( как мне кажется ) с событиями и начал их применять .

Но мой перфекционизм столкнулся с ситуацией , когда например нужно из разных мест слушать разные события.
Если вешать все в bootstrap то получается некрасивая картина:

Код: Выделить всё

$config = [
    ...
    'bootstrap' => [
        'log',
        'maintenanceMode',
        'devicedetect',
        \common\modules\timeline\TimelineEventsHandler::class,
        ..... и тут куча всяких хендлеров ....
        
    ],
И появилась у меня мысль об какой то централизированном хранилище хендлеров событий ( в БД ) которые будут иницилизироваться в начале загрузки приложения.

То есть, я делаю класс типа InitEventHandlers и вставляю его в bootstrap:

Код: Выделить всё

$config = [
    ...
    'bootstrap' => [
        'log',
        'maintenanceMode',
        'devicedetect',
        InitEventHandlers::class
        
    ],
а он в свою очередь берет из хранилища ( БД ) все обработчики и вешает их в соответствии необходимости :

Код: Выделить всё

class InitEventHandlers extend BaseObject
{
	public function init()
	{
		foreach(Events::find() as $event){
			Event::on($event->class_name, $event->event_name, [$event->worker_name, 'process']);
		}
	}
}
Вот вопрос - нормально ли такое решение ?

Аватара пользователя
ElisDN
Сообщения: 4998
Зарегистрирован: 2012.10.07, 10:24
Контактная информация:

Re: Как использовать событие ( человеческим языком ) ?

Сообщение ElisDN » 2018.01.23, 12:55

zxczxc12 писал(а):
2018.01.23, 12:27
Вот вопрос - нормально ли такое решение ?
Нормально. Это как раз и получился мой class Bootstrap implements BootstrapInterface из ответа выше.

Ответить