Wednesday, February 13, 2013

How to use Behaviors in Yii

Recently someone posted on my blog about keeping your yii models lean, asking for an exmaple of how to implement behaviors.

At their heart, behaviors are really just a collection of event listeners that you can "plug and play" on different models as needed.

The CTimestampBehavior is probably the easiest to understand out of the box, but it also does things completely behind the scenes.

There are a few ways that you can attach a behavior to a model. If the behavior is one that you want the model to have all the time, then the best practice would be to add it to the behaviors() array of the model the same way the CTimestamp behavior is implemented, like so:
public function behaviors(){
  return array(
      'CTimestampBehavior' => array(
      'class' => 'zii.behaviors.CTimestampBehavior',
      'createAttribute' => 'create_time_attribute',
      'updateAttribute' => 'update_time_attribute',
    )
  );
}

If there are multiple behaviors, you simply add them all — indexing them by the class name or some other unique key that you may wish to access them by in the future:
public function behaviors(){
  return array(
    'CTimestampBehavior' => array(
      'class' => 'zii.behaviors.CTimestampBehavior',
      'createAttribute' => 'create_time_attribute',
      'updateAttribute' => 'update_time_attribute',
    ),
    'amazingBehavior' => array(
      'class' => 'application.components.behaviors.amazingModelBehavior',
    ),
  );
}

Depending on what class you base your custom behavior on, you will have different levels of implementation necessary. If you extend CActiveRecordBehavior, then you will inherit event listeners for all the standard CActiveRecord events such as beforeSave, afterSave, beforeValidate, afterValidate, beforeFind, afterFind, etc. The full list of events that the CActiveRecordBehavior listens for can be found in the event details section of the documentation.

This is the kind of behavior that the CTimestampBehavior is. It knows when the model has been updated, and automatically updates the timestamps for the model without you having to do so directly. There is nothing you need to do to implement it beyond including it in the behaviors array and ensuring that the attribute parameters match the names of the timestamp attributes on the model you're attaching it to.

Lets flesh out the amazingModelBehavior a bit as an example. This custom behavior will activate when the active record is successfully saved, and ONLY when the record is successfully saved.
/**
 * @property-read myModel $owner (Code hinting mojo)
 */
class amazingModelBehavior extends CActiveRecordBehavior
{
  public $didNotifyUser = false;

  /**
   * Override the parent method so we will auto-fire notifications
   * @param CModelEvent $event
   */
  public function afterSave( $event )
  {
     // Do something amazing here, like notify the owner of the record that 
     // it has been updated, since this event will not fire if the save fails.
     $this->notifyOwnerOfPasswordChange(); 
  }

  /**
   * Note, because this is a public function of the behavior, it can ALSO
   * be accessed directly by the owner
   * This is part of what makes behaviors so powerful
   */
  public function notifyOwnerOfPasswordChange()
  {
     // Message here to $this->owner->userRelation->emailAddress; 
     // Assuming this behavior's owner doesn't have the email address on 
     // it directly. You can access any of the model's properties that are
     // available to the behavior by referencing $this->owner->xxx
     $this->didNotifyUser = true;
  }
}

Ok, but what if I don't always want those events to fire off messages to the user when I update their data? Not a problem -- just attach the behavior dynamically in the few situations where you DO want to use it, like so:

// i.e., lets say we have an action where we're updating a password via form
public function actionUpdatePassword()
{ 
   // Load the model
   $model = $this->loadModel();

   // Different validation rules so that they can't change the email 
   // address etc.
   $model->scenario = 'updatePasswordRestricted';

   // Do all the normal post stuff ...
   if ( isset( $_POST['myModel'] ))
   {
       // Attach our amazing behavior so that when/if the model successfully
       // saves, the owner will be sent their notification via email, just 
       // in case it was some nefarious third party changing their password 
       // without their knowledge.
       $model->attachBehavior( 
          'amazingBehavior', 
          'application.components.behaviors.amazingModelBehavior'
       );
       
       $model->attributes = $_POST['myModel'];

       // If the save is successful, the attached event listener will
       // trigger the amazingBehavior, without us having to do anything 
       // else
       if ( $model->save() )
       {
          Yii::app()->user->setFlash(
             'success', 
             'Yay! You changed the information.'
          );

          // We can confirm that the user was notified by checking ...
          if ( $model->didNotifyUser )
          {
             Yii::app()->user->setFlash(
               'warning',
               'A message has been sent to the address of record.' 
             );
          } else {
             Yii::app()->user->setFlash(
               'error',
               'Oh snap -- no messages for you.'
             );
          }

          // Send them on their way ...
          $this->redirect( array('index'));
       }
       Yii::app()->user->setFlash(
           'error', 
           'There was a problem updating your information...'
       );
     
   }
   $this->render('form', array('model'=>$model ) );
}

If you're not using a CActiveRecord as your base model, or simply want to avoid having all those additional event listeners on the model, you can always extend the CBehavior or CModelBehavior.

To do so, if the event you want to use is not already defined in both the events() method of the behavior and on the model your attaching it to as an event trigger, then you will need to create them.

See the onAfterSave() documentation for how to create the event listeners, and the CActiveRecord update() on how to modify your methods to trigger those events and react to them. (That's a whole OTHER blog post...)

I hope that helps to clarify things for those who were asking.