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.

Monday, February 11, 2013

Yii Translations - With Great Power ...

The ability of Yii to translate with ease is one of its primary benefits for many people. You have the ability to translate individual strings or entire views based on the current language selected for the application, and the definitions that you have provided. It's a tremendously powerful tool, when used responsibly. I discovered today that when used with less than extreme care, the results can be ... unexpected.

The core documentation for Yii::t() can be found here: http://www.yiiframework.com/doc/api/1.1/YiiBase#t-detail

Recently, I encountered a bug in some production code that was causing 0s to be stripped from within strings before relaying the ActiveRecord's attribute values. After some convoluted research, I discovered that the problematic code was in an overloaded getAttribute() method, which was calling Yii::t() to translate the attribute value before returning it. Normally, that would not be a problem, however, the model had actually added a method for t, so that it could be referenced as $this->t( $message ); and avoid having to type in the category for the translation, since the translations were being based on the __CLASS__ of the model. Again, this would be fine ... except that the default parameters established for the method were incorrect.

Passing NULL to the $params value of the Yii::t() method in no way equates to the default empty array that the method actually uses. If you pass NULL through as the param value, it will in fact continue through all of the remainder of the Yii::t() method, and execute the final line, which is:
return $params!==array() ? strtr($message,$params) : $message;
Side note: It should be noted that if $params == array() the translated message is returned after the first if block ( https://github.com/yiisoft/yii/blob/1.1.13/framework/YiiBase.php#L580 ). If that same conditional also checked for === null, well, this would be a different post ;)

Now, imagine that the original string was something like:
myemail2001@example.com

Should email addresses be going through Yii::t()? No, not really. The code in question was more on the order of translating the favorite color of the user to the proper language, but when applied universally, without proper thorough research (no, it's NOT safe to assume that null will work as a default value ...), bad things can happen.

The following statements are all true, though the last is a bit shocking:
$attributeValue = 'myemail2001@example.com';

// Translate the value with no parameters - assuming translations are stored 
// in the messages/(lang)/user_preferences.php file
$valueTranslated = Yii::t( 'user_preferences', $attributeValue );

// At this point, $valueTranslated and $attributeValue are the same, since 
// it's an email address, and there is no direct translation.
echo $valueTranslated ,' equals ', $attributeValue;

// Now, lets assume that you presumed the params parameter was null, 
// rather than array() ...
$valueTranslatedWrong = Yii::t( 'user_preferences', $attributeValue, null ); 

// At this point, $valueTranslatedWrong is: myemail21@example.com
// Note that the 0s have all been stripped out of the string
echo $valueTranslatedWrong , ' does not equal ' , $attributeValue;

What?! Where are my 0s? Well, as we all know, in PHP, 0 is considered equal to null, unless you're using ===. So, when you strtr out NULL, you're pulling out the 0s.

Moral of the story? Always double check the method declaration.

Thursday, March 29, 2012

Using Packages with the clientScript Component

With the advent of Yii 1.1.10 the package manager of the CClientScript became even more powerful with the ability to dynamically add packages to the list and chain the return value.

If you're not familiar with using packages, I will run through it very briefly. The Class reference for the CClientScript can be found here: http://www.yiiframework.com/doc/api/1.1/CClientScript

Most people who have used Yii for a while are quite familiar with using the clientScript component to dynamically add CSS and Javascript files to the page headers, so lets start there.

Wednesday, February 29, 2012

Leveraging Widgets, the Widget Factory and Skins to make DRY code in themed sites

From the most basic of sites to the most complex, Yii offers us a way to consistently format the styles and data we present across multiple pages and even multiple sites easily though the use of the CWidgetFactory and skins. 

I have used and created numerous CWidget based components, and often use the CWidgetFactory for adding consistency, but it was only today that I finally delved into skins for Widgets and the capabilities that they provide. I could never quite wrap my head around all the different pieces of how to fit it into a site or multiple sites. When I did, it was game changing.

Full information about the widget factory can be found in the Yii API documentation here: http://www.yiiframework.com/doc/api/1.1/CWidgetFactory

I will share an example of how to progress from a simple set of similar views and adjust those views step by step until we have simplified them to the point that they can be themed without the need to replace the view file, or in replacing the view file, negate the need to specify all the parameters for the widget contained in that view directly (thereby reducing the chance that a crucial change will not be propagated from one theme to the next).

I should probably have broken this down into a series of posts, but I wanted to be sure that I actually got around to posting it all, and this seemed the best way. If some of the later portions are confusing, set it aside and come back to it later once the basic idea of using the Widget Factory has sunk in.

Tuesday, October 18, 2011

Learn to Love the Cache

I've avoided using the caching aspects of Yii for a long time, simply because I never had time to dig into it. My cursory understanding was that you needed to have APC or memcached enabled on your server for it to work, and that wasn't something I could guarantee. That understanding was NOT correct.

There are several caching options built in to Yii, including a simple CFileCache which will write files to your application/runtime/cache/ folder and a CDummyCache that you can use if you need to bypass the real cache without altering your code.

This example starts with just the most basic situation where we are consuming data from an external source and caching it locally so that we do not continually ping the remote server for data.

The Guide to Caching can be found here: http://www.yiiframework.com/doc/guide/1.1/en/caching.overview

Tuesday, August 9, 2011

Linked Required Fields

This came up today as something that is a very basic requirement when developing complex forms, but there was no way built in to Yii to handle it quickly and consistently across multiple models, so I created a custom validator for it. I hope it helps others to move forward quickly with their complex form development ;)


The need was that when a specific field was set or a specific value was selected for a field, additional fields would become required.

The solution? ChildrenRequiredValidator


http://www.yiiframework.com/extension/childrenrequiredvalidator/

Any feedback or suggestions are more than welcome!

Friday, August 5, 2011

Using GIT to control your production releases

In the process of looking up some documentation for work, I came across this article and I think it does a fantastic job of explaining a solid git development workflow and thought I should share it. Enjoy!!

http://nvie.com/posts/a-successful-git-branching-model/