Using behaviors for a customer loyalty module

Well met, forum,

I was hoping you could advise me on something.

I am trying to build a customer loyalty module for a Yii2 advanced app. It would log specific user actions on the frontend (such as buying a product, registering, writing a review, etc.) and, after a few checks, reward the user accordingly (with points or badges or some such).

I have finished building the model but I need a way to capture user actions and, after browsing the documentation, behaviors seem the solution. With them I can automatically handle events and send the required information to my module.

Yet sometimes I might need some custom events to happen. My question is: is it possible to make new events part of a behavior or do I need to declare them separately in the class that implements the behavior?

Or maybe you know of a more fitting solution to capturing user actions.

Thanks in advance.

Welcome to the forum, this is an excellent question. I would love to see some opinions/examples myself. First thought is behaviors is the way to go because you can attach them to multiple actions at the same time, keeping controller logic clean and simple. The guide has an extensive section on behaviors, but I haven’t done a custom one yet, would love to see this in action.

Thank you for the warm welcome.

Ideally I would enable backend users to configure which events trigger which actions, so the power to dynamically attach behaviors sounds great for that.

So far, it seems behaviors cannot attach events of their own, but I hope to be able to capture almost any useful action with the events already in play, I’ll have to experiment a bit.

I’ll report any success. In the meantime I await more opinions on the subject, I might be going the wrong route here.

Yet I assume it’s common for Yii users to think “hmmm, is this really the smartest, most elegant way to go about this? maybe yii has some clever trick up it’s sleeve” :lol:

Looking forward

Actually, I think it’s a very smart approach. Your backend users could add records to the db, and we know using Gii, creating this UI will be a snap. Then the behavior methods check for the db records and does the additional actions, when those records are present. I just looked over the guide for behaviors and events. I have not done any implementation of these outside of using the predefined behaviors, so let me state that up front :) But it seems like you can define custom events, which in your case need to happen after the controller action, not before like it is in AccessControl behavior. And you would also need to write a custom behavior class to apply the custom events, not sure why you couldn’t. Another point of curiosity, is the inflection part, where the behavior knows which model/actions to refer to. I need to read up on that. Anyway, I’m very interested to see what you come up with. When I get some time, I will playing with this too.

I have successfully made a behavior to do what I need, here’s a snippet, with some sugestive renames (I hope):




class loyaltyBehavior extends Behavior

{

	/**

	 * @var array list of attributes that will trigger a log.

	 * The array keys are the ActiveRecord events that will trigger a log,

	 * and the array values arrays with action names as keys and an array of attribute(s) that will be checked for changes and content that represent the attributes to log. 

	 * You can use a string to represent a single attribute, or an array to represent a list of attributes. For example,

	 *

	 * ```php

	 * [

	 *     ActiveRecord::EVENT_AFTER_INSERT => ['actionName1' =>['attributes' => ['attribute1','attribute2'],

	 *															'content' => ['attribute3','attribute4'],

	 *											'actionName2' =>['content' => 'attribute6']],

	 *

	 *     ActiveRecord::EVENT_AFTER_UPDATE => ['actionName2' =>['attributes' => 'attribute2',

	 *															'content' => 'attribute4']

	 * 											'actionName3' => ''

	 * 											'actionName4' =>['attributes' => 'attribute2',

	 *															'content' => '']],

	 * ]

	 * ```

	 */

	public $userActions = [];

	/**

	 * @inheritdoc

	 */

	public function events()

	{

		return array_fill_keys(array_keys($this->userActions), 'processUserActions');

	}

	/**

	 * Checks if the searched for attribute(s) have been completed or changed and creates a user action log.

	 * @param Event $event

	 */

	public function processUserActions($event)

	{

		if (!empty($this->userActions[$event->name])) {

			$actions = (array) $this->userActions[$event->name];

			$user = $this->getUser();

			if ($user){

				foreach ($actions as $actionName => $action) {

					if (empty($action)) {

						$this->logAction($user,$actionName);

						continue;

					} elseif (empty($action['attributes']) && !empty($action['content'])){

						$this->logAction($user,$actionName,$action['content']);

						continue;

					}

					$attributes = (array)$action['attributes'];

					$content = $action['content'] ?: null;

					foreach ($attributes as $attribute){

						if ($this->owner->$attribute != $this->owner->getOldAttribute($attribute)){

							$this->logAction($user,$actionName,$content);

						}

					}

				}

			}

		}

	}

        public function logAction($userId, $actionName, $content = null){

             // saves the action in the user action log 

	}


	/**

	 * Finds the current user id.

	 * @return int the user id or null if none is found.

	 */

	protected function getUser()

	{

           //get user id from session or from the object if the object is the user model

           //I did this because one of the events I may want to reward is actual user registration

	}

} 

I think the comments explain it pretty well.

Still no idea if I can attach custom events.

Now I wish I could attach these dynamically from some saved settings in the db.

Is there a way to do this through configurations maybe? Can you use configurations like: "I want when this model is created for it to have this behavior attached" ?

Okay, so far, attaching the behavior statically works, as does attaching it dynamically right after model initialization.

The configuration looks like this:


$user = new User();

			$user->attachBehavior( 'test',[

				'class' => loyaltyBehavior::className(),

				'userActions' => [

					User::EVENT_AFTER_INSERT => [

						'user_registered'=> [

							'content' => 'username'

						]

					]

				]]);

So I tried attaching them from a db saved configuration by bootstrapping my module. The function in my module init:


    public function init()

    {

        parent::init();


        // custom initialization code goes here

		if (isset($this->params['db_config']) && $this->params['db_config']>0) {

			\Yii::trace('db_config > 0');

			$configs = ConfigTable::find()->all();

			$i=0;

			foreach ($configs as $cfg){

				\Yii::trace('found config');

				$class = $cfg->class;

				$event = $cfg->event;

				$actionName = $cfg->action_name;

				$behaviorName = 'loyaltyBehavior'.$i;

				$attributes = empty($cfg->attr) ? '' : explode(',',$cfg->attr);

				$content = empty($cfg->content) ? '' : explode(',',$cfg->content);

				$behaviorConfig =[

					'class' => loyaltyBehavior::className(),

					'userActions' => [

						constant($class.'::'.$event) => [

							$actionName => [

								'attributes' =>$attributes,

								'content' => $content,

							]

						]

					]];

				\Yii::trace(var_export($behaviorConfig,true));

				$class::attachBehavior($behaviorName, $behaviorConfig);

				$i++;

			}

		}

    }

The Yii:traces fire and the configuration looks good but the event doesn’t fire.

After that I tried to attach by config:




		'userModel' =>[

			'class' => 'common\models\User',

			'as myBehavior1' => [

				'class' => 'common\\modules\\loyalty\\components\\loyaltyBehavior',

				'userActions' => [

					'afterInsert' => [

						'user_registered'=> [

							'content' => 'username'

						]

					]

			]

		]],



And it still doesn’t work.

I’m pretty stuck at this point, I could really use some veteran help~

I realized that I was doing 2 things wrong.

  1. I was calling attachBehavior statically.

  2. It seems that attachBehavior only attaches a behavior to that particular instance of the component.

I would still like to attach these things from a configuration that is saved in the db. Any ideas would be appreciated.

P.S. still no idea why trying to attach them through the config file doesn’t work.

Good news everyone! I have made it work with a saved configuration by implementing what I tried to do in the init of my module in my behavior’s events().

I don’t know why I didn’t think of this before.

A few things changed, such as the $userActions now being protected, but the code is very similar. I might see performance issues if I have a lot of actions attached to a single component as it does this every time the component is invoked, but nothing noticeable so far.

Now all I have to do is attach my behavior to everything I need, no configuration.

Strangely, the attach by config still doesn’t work. I’ll get back to you on that.