Saturday, April 19, 2014

Making the switch from Yii 1.1 to Yii 2.0 - Part 2: Getting Started

Probably my biggest hurdle in getting started with Yii 2.0 (aside from getting my hosting company to upgrade my server from PHP5.2 to 5.4) has been understanding the changes to the application structure and the vast difference between the basic application and the advanced application. Since my personal site runs on a subdomain, this greatly complicated the issue for me. I needed something more than the basic application, but the advanced application was way too much overkill. I finally went with the advanced application, but now that I understand how the composer works to distribute the skeleton files, I'm looking forward to creating something that's between the two.

There is way too much to cover in a single blog, so this is just a look at the basic structure and front end changes that you need to know to get up and running in 2.0.

If you're past all of this and looking to dive into the nitty-gritty detail changes, I suggest you read the official documentation on upgrading.

Tuesday, March 11, 2014

Setting the CDbConnection for Active Records within a Yii Migration

I use migrations whenever possible within Yii applications to help ensure consistency between implementation. I have multiple development environments, a staging environment and a production environment. I really don't want to have to manually issue a lot of sql queries or yii commands if I don't have to. Migrations make that possible and can be integrated automatically in the build process for deployment.

Occasionally, I need to convert some old data into a new structure, and I can do this via the migrations as well!

Create a basic migration to translate data:

For example, to query a set of data from an old table, and create a new set of records from it (that may have additional logical processes added beforeSave etc.) I could start off by doing the following in my migration:

public function up()
{
  // Query for the existing record data
  $results = Yii::app()->db->createCommand()
    ->select('*')
    ->from('tbl_old')
    ->queryAll();

  // Ensure that if we fail, we start with a fresh slate
  try {
    foreach( $results as $row )
    { 
       $m = new NewModel();
       // assign attributes from $row[] that should be
       // mapped to the new model applying any new
       // logical manipulations required
       if (!$m->save())
       {
         throw new Exception(
            CVarDumper::dumpAsString( $m->errors )
         );
       }
    }
  } catch ( Exception $e )
  {
     echo $e->getMessage(), PHP_EOL;
     $this->truncate('tbl_new');
     // indicate migration failure
     return false;
  }

  // acknowledge success
  return true;
}


If you only have one db connection in your application, this will work fine. But, if you also have a test database that you need to migrate, it will quickly fail due to duplicate primary keys, or write double the data in your primary database, rather than your test database.

Update the migration to handle alternate connection components:

To update the migration to work with a different connection, we simply need to add a few lines that specify which connection to use, as follows:

public function up()
{
  // Query for the existing record data 
  // Use migration's db connection rather than Yii::app()->db
  $results = $this->getDbConnection()->createCommand()
    ->select('*')
    ->from('tbl_old')
    ->queryAll();

  // Set the static connection property for active records to the migration's 
  // connnection
  CActiveRecord::$db = $this->getDbConnection();
 
  try {
  //  ... the rest remains the same
  }
}


Now, I can run my migration on the alternate connection with no problems, and no pulling of hair!

./yiic migrate --connectionID=db_test


On the Yii2 front, I was side tracked by the fact that I can't find an equivalent to the CViewAction (which I use extensively), and so much of the main layout involves new widgets in static method form, but I will get back to it soon!

Tuesday, February 4, 2014

Making the switch from Yii 1.1 to Yii 2.0 - Part 1: Before You Begin

So, over the weekend I decided that it was time to dive into Yii2 and start getting familiar with it and be prepared for the official beta release coming up. I've been reading the forums about it since the 2.0 board was first opened, and I thought I was in a pretty good place to start from. The first thing I learned, was that I have a lot to learn before actually starting to convert any of my applications into Yii2.

What You Need to Know Before You Start!


Yii2 takes "full advantage" of some of the "newer" (I know it's been out a long time, but some of us haven't had the liberty to use it) capabilities of PHP - namely (pun intended) requiring PHP5.4 as a minimum, and using Composer to create new Yii applications. They've also switched out the standard PHPUnit tests in the application for Codeception.

If you're at all like me and have been nose to the grindstone on servers locked into PHP in the 5.3 (or lower) family, this will come as quite a system shock, even when you know it's coming ... I'm ashamed to admit that I looked at an array in 5.4 syntax and tried to figure out why there was JSON code in my PHP file.

So, forget all the implementation differences between Yii 1.1 and Yii 2.0 for now -- If you're not already fluent with PHP 5.4, Composer and Codeception, spend a week getting there.

Composer - PHP Package Manager

The documentation for installing Composer is very straightforward and can be found at http://getcomposer.org

Getting it running on my Ubuntu VM was very straight forward, but on my Macbook I had to go through quite a bit of shenanigans to get XCode updated (required by Composer). I ended up having to uninstall and reinstall XCode before Composer would work properly there. Though the PHP upgrade on the Macbook trumped the PHP upgrade on the Ubuntu machine. So scratch off an evening just getting Composer running smoothly across the board. I believe this will be the preferred method of releasing extensions, etc. so if you're planning to move in that arena and aren't already familiar with this, time to read up.

I'm still figuring out all the basics for it, but it seems like a very handy way to keep packages maintained, and I'm looking forward to really becoming familiar with it.

PHP 5.4: Namespaces and Arrays

Oh my. So, the namespaces take a lot of getting used to, seeing all the /path/to/myClass::$variable and it seems like it takes up a lot more characters to do something very simple. Lots of power there though, and once you get used to reading it and writing it that way, it will become second nature. For the full list of what's dramatically different, check out the PHP 5.4 migration guide.

Arrays can now be generated with short array tags, and you'll find this extensively in the Yii2 code.
$config = array('something'=>'else','sub'=>array('marine'=>'sandwich'));
becomes
$config = ['something'=>'else','sub'=>['marine'=>'sandwich']];
Much more concise, but will definitely throw you for a loop the first time you see it. It's going to be a while before I can read that as quickly as I can the old style.

CAUTION: Once you start looking ahead, it'll be hard to go back to the old 5.3 ways -- so be sure you're ready!

Codeception Testing Framework

I haven't looked into Codeception very far yet, but it essentially runs all the tests from a 'story' point of view. This is my personal homework for the next few days. If you're as dependent on TDD as you should be, you will want to brush up on this before diving in.

Finally, my favorite ...

Bootstrap 3.1.0

The new sample applications implement Bootstrap 3.1.0 which has been changed significantly from the Bootstrap v2. Bootstrap 3 is designed not just to be mobile responsive, but mobile first. If you're used to using any of the other CSS libraries or the v2 flavors of Bootstrap, spend a little time getting familiar with the changes that have been implemented in the v3 line. It's well worth it.

Once you're FULLY familiar with the syntax changes for 5.4 / 5.5 (whatever your flavor), and Namespaces no longer throw you for a loop, and Composer is installed and functional, it's finally time to start reading
Upgrade from v1 on the Yii2 docs.

I should also mention, my beloved "yiic" command is no more -- it's now "yii"! So if you get ambitious and download the sample applications, don't go searching for yiic, because you won't find it. ;) I'm excited about the advanced application and the init scripts to toggle configurations, but I'm getting ahead of myself here ...

Part 2 should make its appearance this weekend as I will dive into transitioning an old Yii 1.1 application into Yii 2.0

Thursday, January 30, 2014

Saving and Retrieving Encrypted Data in Yii

A few people have commented about using md5 or the PASSWORD() capabilities and how to retrieve their data from their MySQL database when updating user passwords and other encrypted data, so I thought it would be a good idea to spend a little time talking about how you can use the CSecurityManager to handle your encrypted data in a way that keeps it completely secure, but allows you to retrieve the original data.

This is not a new feature, it has been around since Yii 1.0, but it's easy to overlook it you don't handle a lot of encryption or need to worry about much other than the occasional casual user field.

The CSecurityManager works as an application component, so to access it you simply call
// $key is optional, it will default to NULL if you don't pass one
Yii::app()->securityManager->encrypt( $string, $key );

and

Yii::app()->securityManager->decrypt( $string, $key );

The most important thing to remember is that your key has to be the same when encrypting and decrypting! This may seem like a no-brainer, but if you're using different keys for various applications, components, or modules, you can quickly run into complications. I like to store the key as a constant that I can access with the model, so that the model can then intelligently report the data if needed. Of course, you need to consider the security of your models before implementing anything that will allow decrypted access.

Back in this old post about saving encrypted passwords I demonstrated the more traditional MySQL PASSWORD() approach. Using the CSecurityManager to handle your encryption will use essentially the same approach, only you'll update the encryptPassword function as follows:

const KEY_VALUE = 'someRandom123'; // <-- make this good

public function encryptPassword()
{
  // Nothing to encrypt
  if ( $this->userpass == '' )
    return;

  $this->userpass = Yii::app()->securityManager->encrypt( 
    $this->userpass, 
    self::KEY_VALUE 
  );
}

Note: I'm using the password field here since it's the previous example and the one thing most people actually make an effort to encrypt. You can encrypt any data that you save, so please don't feel that this example applies only to password data.

Now, if your administrators need to be able to access the encrypted data so that they can answer questions for people, you need a way for the model to see that data cleanly - hello decrypt! You should be VERY cautious about using this. If you handle the decrypted data lightly you're losing the point of encrypting it in the first place.

/**
 * Return the decrypted value of the field (does NOT assign
 * the decrypted value back to the attribute)
 * @return string
 */
public function decryptPassword()
{
  // Nothing to decrypt
  if ( $this->userpass == '' )
    return '';

  return Yii::app()->securityManager->decrypt( 
     $this->userpass, 
     self::KEY_VALUE
  );
    
}

Remember, as in the original example, you're not going to re-encrypt it before every save, only when the data has been updated or the record is new, unless you're decrypting it by default afterFind, which I do not recommend (see above note about the wisdom of handling decrypted secure data).

If you really want to get fancy with your data storage, you could run an encryption filter on all the attributes before saving, and then decrypt them afterFind so that it's impossible to read anything in the database directly. There may be some merit to that idea if you're on a shared database server, but if you trust your database security then that's probably a lot more complicated than you need to make your data storage for any data that isn't truly sensitive.


So, now looking back to the original post about authentication via database, we should update our authentication a bit:

public function authenticate()
{
  $record=User::model()->findByAttributes( array('username'=>$this->username) );
  if($record===null)
    $this->errorCode=self::ERROR_USERNAME_INVALID;
  else if( $this->password != $record->decryptPassword())  // <-- Note the change
    $this->errorCode=self::ERROR_PASSWORD_INVALID;
  else {
    $this->_id=$record->id;
    $this->setState('title', $record->title);
    $this->errorCode=self::ERROR_NONE;
  }
  return !$this->errorCode;
}

Wednesday, December 11, 2013

Adding Authorship and Publisher data to your site with Yii

Adding the appropriate metadata is actually quite straight forward. If you're not sure what information I'm talking about, please check out my recent blog post on Authorship and when to use it.

Adding the publisher information is the easiest to tackle, as your entire site should be tagged as one publisher, so you can add it to the main layout simply by using the client script manager. The hardest part is making sure you have a brand Google+ page set up and getting the proper ID number.

In your main layout, or wherever you feel it is most appropriate for your situation, simply add:
Yii::app()->clientScript->registerLinkTag( 'publisher', null,
    "https://plus.google.com/{$profileId}"
); 

If you are the only author on the site, you can add your authorship information at the same time. Otherwise, you should add the author metadata only on the pages where it is most appropriate and using the Google+ profile page for the relevant author. You can do this from the Controller action, the view file or in a beforeAction method or a filter of the Controller if you have one author for a whole section of CViewAction pages, etc.
Yii::app()->clientScript->registerLinkTag( 'author', null,  
    'https://plus.google.com/u/0/'.$gPlus.'/'
);


That's really all there is to it!!

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.