Showing posts with label yii. Show all posts
Showing posts with label yii. Show all posts

Tuesday, March 30, 2021

Converting an Application From Yii2 to Yii3

Converting Yii2 to Yii3

A Minimalist Conversion Example

I’ll admit, I put off playing with Yii 3 for quite a while because I was daunted by the quick peeks into the code base and sample applications. The differences between configuring and bootstrapping a Yii 2 application vs a Yii 3 application are significant. At first glance, the two versions of a base application are completely different. But, once you start digging in, the similarities become apparent and you can start to draw the connections between the Yii 2 methodologies and the Yii 3 counterparts. These are my first impressions and the results of my own investigations thus far. I expect things to continue to evolve as the Yii 3 project advances.

PLEASE NOTE: Yii 3 is *not* yet ready for production release. This blog is an attempt to get more people playing with it and correspondingly, more people involved with providing feedback and assistance in developing and supporting the project. If you are not feeling adventurous, table this project for when there is a stable release and official documentation. Check the Release Cycle for production timelines and updates.

Saturday, March 19, 2016

Swivel Extension for Yii

In my last post, I mentioned that I was looking forward to making a Swivel extension for Yii. I'm excited to share that I've FINALLY found the time to do so.

It's a fairly straight forward extension, simply providing access from the application to a Swivel component which interacts with the core Swivel library, and which handles the general bucket assignment for users as swivel is accessed. I've also provided a class for integrating the existing Swivel logger over to the primary Yii Logger, and provided a data structure and model for storing the list of features for the application.

The extension is available on the Yii Framework Extensions list, from the GitHub repository or as a composer package.

It's really designed to be installed via composer:
composer require "dhluther/yii-swivel":"~1"

I've tried to keep it lean, but provide a good bit of information about how to implement it and how the Swivel features work on the Yii-Swivel Wiki on GitHub.

I will be creating a 2.0 version shortly as well.

Enjoy! I hope others find it as useful as I think I am going to, and huge props to the Zumba team that developed it and to Stephen Young and his presentation at SunshinePHP 2016!

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

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.

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!

Monday, August 1, 2011

Keeping your Yii Models Lean - Use Behaviors

Active Record models are fantastic for consolidating "black box" logic and keeping your models self-aware of their business logic, but what do you do when the business rules and object specific operations keep adding up?

Do yourself a favor, and get into the habit of putting those business logic methods into attachable behaviors.

Friday, June 3, 2011

Referencing views from alternate locations and themeability

This is something very simple, but it's very easy to do it 'wrong' and end up creating more work for yourself in the long run.

I ran into this again today when I was trying to create a theme for a site, and I knew that I want to be able to deploy the site in multiple locations (I will make another post later regarding distributed Yii applications) which employ different themes to present the same forms.

Friday, March 18, 2011

Using Scopes with DataProviders

I used to use an extension called EActiveDataProvider to get this functionality, but that broke with the release of 1.1.2 or 1.1.3, I can't quite recall. Suddenly, today, it dawned on me that I could easily get this functionality once more, by simply calling the scope function of the model and then passing in the particular scope I was looking for as the criteria parameter of the CActiveDataProvider.

For example...

In model Post, I add a scope as follows:
public function scopes()
{
   return array(
      'stats'=>array(
         'select'=>'post_id',
         'condition'=>'post_status=1', 
         'order'=>'post_date DESC',
       ) 
   );
}

Then, in my controller where I want to output just this set of data:
public function actionStats()
{
    $p = new Post();
    $scopes = $p->scopes();
    $criteria = $scopes['stats'];

    // Note: I could probably just Post::model()->stats()  but I haven't 
    // tested that.

    $dataProvider = new CActiveDataProvider( 'Post', array('criteria'=>$criteria));

    ... business as usual
}

Where previously I was doing something like this to be able to take advantage of the scope:
$dataProvider = new CArrayDataProvider( Post::model()->stats()->findAll() );

While the CArrayDataProvider is an acceptable alternative, I find that it does have some peculiarities when used with a CGridView, which the CActiveDataProvider avoids.

(Note: My scope is actually quite different and more complex, but I thought this made a clearer example).

EDIT:

Apparently, I need to read more about active finders, because you can actually do THIS as well:
$dataProvider = new CActiveDataProvider( Post::model()->stats() );

Wednesday, March 9, 2011

Published ESitemap Extension

The extension can be found here: Yii Extensions: ESitemap

This is an extension that provides class based actions which can be applied to any controller to generate a valid sitemap document. It comes with both a sitemap.xml option and a 'human readable' option.

You can configure any ActiveRecord class to be used as a source of site links, simply pass in the configuration options for the proper view of the object you wish to display.

I'm looking for feedback and ways to improve, so please share any comments/suggestions that you may have, either here, on the ESitemap Extension, or on the forum thread for the extension.

Hope it's helpful!

Saturday, January 22, 2011

Yii 1.1.6 and the beauty of yiic migrate

The new Migration feature is amazing. A-mazing.

In addition to migrating production servers from one version of your software to another, it's excellent for coordinating among a group of developers *AND* for coordinating between primary and test databases. Farewell SQL alter script coordination, it's been real ;)

If you haven't read it yet, I highly recommend reading this guide to migrating:

http://www.yiiframework.com/doc/guide/1.1/en/database.migration#creating-migrations

(Also, I recommend getting the bug-fix from the current yii trunk here: http://code.google.com/p/yii/source/detail?r=2907 which fixes the issue with the migrate not using the connectionId option properly.)

So, how do we actually apply this new wonderful feature? It's quite simple!

Tuesday, November 9, 2010

Posting Multiple of the Same model within a form

Recently, I was asked about posting multiple models of the same class within a form and how that can be accomplished.

The solution is to override the default name values for the form items to allow them to be presented in an array format, then in the controller, process each array item for the Class.

Lets say that I have a set of user bookmarks (Bookmark), and I want them to be able to update all the bookmarks from a single administrative page. This is a little rough, but it should explain the concept clearly:

Friday, October 29, 2010

Publishing to alternate assets directory

Sometimes, it's not possible to use the standard 'assets' directory for publishing assets. After struggling with this for a while, I discovered that the solution is, as is typical with Yii, even simpler than I could have hoped for.

Tuesday, September 7, 2010

Action paramater binding in Yii 1.1.4

The new Yii 1.1.4 contains numerous improvements. One that I wasn't clear on from the changelog was  the line saying "automatic action parameter binding" for $_GET values.  This is a very subtle way of saying that they've made it much easier to handle some of the 'grunt work' for actions that work with multiple $_GET variables.


http://www.yiiframework.com/doc/guide/basics.controller#action-parameter-binding

Quite simply pass in the name of the $_GET variables to the controller action method as parameters, including any default value you would like applied if they did not specify a value. 

Easily looked over, but very nice addition to the framework!!

Thursday, June 24, 2010

Adding Foreign Keys Manually

Using MyISAM tables (in MySQL), I'm unable to set foreign keys in the database. However, I'm working with several tables which utilize foreign key type relationships. The problem was how to get Yii to recognize those relationships properly.

This one took me a while to figure out. With the help of the experts over at the Yii Forums, I was able to put together this solution.