Wednesday, October 16, 2013

Many to Many Relationships in Yii

Overview

Some time ago I said that I would write an article about how to deal with a many to many relationship in Yii. If you remember I was working on a web site with some very basic database maintenance requirements. In this case I have a set of standards and a set of categories. Logically there is a many to many relationship between the standards and the categories. Any one standard can be associated with multiple categories, and any one category can be associated with multiple standards.

The Schema

Standards

FieldTypeKey
idnumericyes
descriptioncharacterno


Categories

FieldTypeKey
idnumericyes
descriptioncharacterno


Linkages

FieldTypeKey
standard_idnumericyes
category_idnumericyes

The Approach

Since I only have a limited number of categories per standard, it would be nice if I could just list the categories on the maintenance page with check boxes and allow the user to select the appropriate categories for the standard. It turns out that this is a bit more work than populating a drop down list, but not much more. I will need to take the following steps:

  1. I need to make sure that I have defined the many to many relationship in my standards model.
  2. I need to add the check box list to my view and populate it with all the available categories.
  3. I need to add an attribute to my model to hold the array of selected categories
  4. I need to add a method to my standards model that actually builds an array of selected categories for a given standard.
  5. I need to add some code to my create and update actions in my standards controller which will be responsible for loading the new attribute, and for saving the selected categories to the linkage file.
  6. I need to add a method to my categories model that will replace the current selected categories with the newly selected categories.

The Relationship

You will need to create a many to many relationship in your Standards model  this relationship will tell Active Record how to retrieve all the categories for a given standard. It looks like this:

$categories=array(
          self::MANY_MANY,
          'Categories',
          'linkages(standard_id,category_id)'
     )

You can add a similar relation to the Categories model to point back to the Standards if you wish. I define all my relations up front whether I am using them or not. That way if I need the relation, it is there. And Yii's lazy loading prevents querying the relations unless I use it, or explicitly use a with() or through() method.


The View

Here is where things start looking very familiar to those who have used the drop down list. To display my list of check boxes on my view, I am first going to have to generate the list with CHtml::listData(), then pass that list to the CActiveForm::CheckBoxList() method. It looks like this:

<div class="radio-list">
 <?php echo $form->labelEx($model,'cl_categories'); ?>
 <?php $list = CHtml::listData(
     Categories::model()->findAll(),'id','description'); ?>
 <?php echo $form->checkBoxList($model,'cl_categories',$list); ?>
 <?php echo $form->error($model,'cl_categories'); ?>
</div>

Notice that my attribute cl_categories is not part of the data model. I will have to add this attribute to the Standards model for this to work. The categories attribute added to the Standards model via the relationship described above will contain an array of Categories models. Unfortunately the CheckBoxList() method requires an array of values rather than an array of models, and the $_POST['cl_categories'] variable will also be an array of values rather than an array of models. So we need a temporary attribute here which I have named cl_categories. The $list array built by listData() contains the list of category descriptions with their ids. The category id will be mapped to the value of the check box, and and the category description will be mapped to the check box label.

The Standards Model

So back to the Standards model, we need a temporary attribute (cl_categories) to hold an array of category id values to be used both to populate the check box list, and to return the selected categories from the view. This is accomplished by adding a var $cl_categories; line to the beginning of the Standards model. We also need a method to populate this temporary attribute from the $categories relation.  That will look like this:

/**
 * Loads categories from model
 */
public function loadCategories()
{
    $this->cl_categories = array();
    foreach ($this->categories as $category) {
        $this->cl_categories[] = $category->id;
    }
    return $this;
}

The Standards Controller

A few simple changes in the controller will allow us to use this new standards model attribute and pass the correct values to the view. I will make similar changes to both the create and update actions.  I present the update action here for example with notes on how the create action differs.

/**
 * Updates a particular model.
 * If update is successful, the browser will be redirected to the
 * 'view' page.
 * @param integer $id the ID of the model to be updated
 */
public function actionUpdate($id)
{
    // For the create action, model creation is simply
    // $model = new Standards;
    $model=$this->loadModel($id)
            ->loadCategories();

    if(isset($_POST['Standards']))
    {
        $model->attributes=$_POST['Standards'];
        if ($model->save()) {
            Linkages::model()->saveAll($model->id, 
                    $model->cl_categories);
            $this->redirect(array('view','id'=>$model->id));
        }
    }

    $this->render('update',array(
            'model'=>$model,
        ));
}

You will notice that this differs from the normal Gii generated update action in that I have chained the call to loadCategories() to the end of loadModel(id). This causes attribute cl_categories to be populated from the categories relation after the model object is created and loaded. I have also added a call to save the categories once the Standards model has been successfully saved. I should wrap the saves in a transaction since it involves multiple files, but will leave that to you as an exercise.

You may be wondering how this all works since Yii does lazy loading, and won't this cause a lot of extra queries? Yes, yes it will. But we can alleviate those issues by telling the Standards controller to go ahead and query the Categories relation when it loads the Standards model.  This happens in the loadModel($id) method. Like so:

$model=Standards::model()->with(array('categories'))
        ->findByPk($id);

You see the extra piece? that with(array('categories')) in there tells findByPk($id) to go ahead and load the categories relation with the Standards model.

The Linkages Model

One thing remains I need to add some code to the Linkages model to replace the category id values for the selected standard with the new category id values selected by the user.

/**
 * Replaces job categories with current array of categories
 */
public function saveAll($standard, array $categories)
{
    self::model()
        ->deleteAllByAttributes(array('standard_id'=>$standard));
    foreach ($categories as $category) {
        $model = new Categories();
        $model->standard_id = $standard;
        $model->category_id = $category;
        $model->save(false);
    }
}

Final Notes

Thanks for hanging in there with me.  This has been quite a post. If you find you need to process a many to many relationship on a Yii web page, maybe you should try using the CheckBoxList(). It is as natural a process as they come. Let me know how it works for you.

Tuesday, September 17, 2013

I Hate Suffix Data types

Lotusscript has been around a long time, and it has some nuances that can drive a person crazy. In particular ME. This is even more true when attempting to process data from an external database with long legacy roots.  Taking a page from BASIC, Lotusscript provides a convenient way to implicitly type variables called the data type suffix.  I really don't know what they are any more, and I don't care.  I would rather explicitly declare my variables anyway.  Fortunately Lotusscript allows me to force that.  Just put Option Declare in the (Options) section of the script, and you are on your way. Usually...

I was happily porting a Domino database which retrieved certain data points from a PRMS database to get that data from an Epicore CMS database. This all resides on a state of the art Power System running IBM i as the operating system.  But Epicore has a long tradition with IBM going back at least o the AS/400 days and a time where RPG only allowed 8 character field names. This led to many creative abreviations, and the frequent use of special characters like # to mean number.  So I am trying to retrieve an invoice number from and A/R Transaction file and the field name is something like ARINV#. Makes sense if you only have 8 characters to work with.  Enter Lotusscript and the Lotus connectors extension.  It is easy enough to read the record and pull out most of the data I want into a Notes document like so:

doc.customer = rec.arcust(0)
doc.name = rec.arcnam(0)
doc.invoice = rec.arinv#(0)

And there I have a problem.  Even though I have told Lotusscript to force explicit definitions, Lotus connector extension (*lclsx) has declared ARINV as in integer because of the data type suffix at the end of the variable name.  Not only does it now think a character variable is really an integer (CMS defines this field as a character), but it thinks the variable name is really ARINV not ARINV#. Charming.

Now the fix looks ok for this field

Dim arinvno As LCField
Set arinvno = rec.Lookup("arinv#")
doc.invoice = arinvno.Text(0)

But you have to remember that doesn't work for all data types.  If it were an integer I would have had to type:

Dim arinvno As LCField
Set arinvno = rec.Lookup("arinv#")
doc.invoice = arinvno.GetInt(1)

And now we are so far from consistent code that if you don't do it every day, or you don't have a reference manual laying around you are just plain out of luck.  Where the heck did the 1 come from?  Turns out that the Get<Type> functions use a 1 based index where nearly everything else in Lotusscript uses a 0 based index. You gotta love that.

SOAPBOX OFF

Wednesday, May 01, 2013

Selecting From a List

Building a Yii Model from an existing database file is very easy.  The tutorial here tells how to build the model and quickly add some CRUD functions.  That is fine if you have a simple flat file, but most useful databases have relationships between their files.  Unfortunately, while the pieces covering how to handle this are easily searchable, you will have to go all over the web to put all the pieces together.  Here are some of the things I wanted to do:
  • populate a drop down box with the available values for the field
  • sometimes I want that drop down box to be loaded based on the value in another field
  • I have a Values List that consists of a header and details, sometimes I want the drop down populated from a given values list. For example I want to display a list of customer statuses that are stored in my values list tables.
  • In some places I want to translate from my foreign key to a description or name.  That is I want to display the customer name instead of the customer id that is stored in my file.
  • In a Grid View, I want to display, sort, and filter by the customer name instead of the customer id stored in my table.
  • I want to be able to select multiple values from a list using check boxes, and have those values configurable from a table (Many to Many relationship)
Lets Get Started.

Placing a Drop Down List

If you have created your CRUD Functions with Gii as directed in the above tutorial, you will have several types of components generated in your views directory.  User input forms, however are consolidated into a single file _form.php.  This will be generated as a CActiveForm widget with Ajax Validation disabled, and populated with a row for each attribute in your model.  These rows will look like this:
<div class="row">
    <?php echo $form->labelEx($model,'country_id'); ?>
    <?php echo $form->textField($model,'country_id',array(
        'size'=>60,'maxlength'=>255)
    ); ?>
    <?php echo $form->error($model,'country_id'); ?>
</div>
This does several things for us.  The labelEx function retrieves the label text from the model.  It is also smart enough to be decorated with an asterisk (*) when entry is required for the field.  The textField inserts a text box control on into the form that is 60 characters wide, and will allow 255 characters to be entered.  And error displays any validation errors that occur for this field when the form is posted.

The problem here is that country_id is a foreign key, and no one will know to enter 3 for the USA, or 7 for Canada.  The solution is to use a drop down list that shows the names of the countries instead of the text box.  To do that we must swap out the following code with a drop down list control.
<?php echo $form->textField($model, 'country_id', array(
    'size'=>60,'maxlength'=>255));?>
Is replaced by:
<?php $list = CHtml::listData(Country::model()->findAll(), 
     'id', 'name'); ?>
<?php echo $form->dropDownList($model, 'country_id', $list,
     array('empty'=>'(Select a Country)')); ?>
First we build the list data, then we load it into a drop down list.  Notice that Country in the listData function is the class name of the Country model. listData builds an array of key=>value pairs which are loaded from the array of models provided in the first parameter.  The second parameter 'id' names the model attribute used to populate the key, and the third parameter names the attribute used to populate the value. There is an optional fourth parameter that is used to provide group names for the list.  dropDownList has four parameters as well.  The first two are the same as the first two parameters for textField.  The third parameter is the array formatted by listData, and the fourth is an array of html options for the drop down. list.  There is a special option which specifies some text for the empty value of the drop down list, and that is 'empty'.  I have set the empty value to '(Select a Country)'.

So that is how to replace a foreign key with a more meaningful drop down box on an entry form.  In future posts I will discuss the remaining points listed above.

Monday, April 29, 2013

Yii Active Record Events

I am skipping ahead a bit, but I just worked out an important detail that I want to capture. CActiveRecord pre-defines some events that you can use to save yourself some time and effort. The documentation tells that they are there, but doesn't really tell how to use them. These events are:

onBeforeValidation - in CModel - executes just before the validation rules are processed - can be used to stop save
onAfterValidation - in CModel - executes just after the validation rules are processed - can be used to stop save, but it is harder.
onBeforeSave - executes after validation but before the record is saved - can be used to stop save
onAfterSave - executes after the record is saved
onBeforeDelete - executes before the record is deleted, can be used to stop deletion
onAfterDelete - executes after the record is deleted
onBeforeFind - executes before the find command is executed
onAfterFind - executes after each found record is instantiated
onAfterConstruct - executes after a new model is constructed

I'm not going to go through all  these to tell you what they are useful for. I will leave that to you. But I will show you how to enable one of them (the rest will work the same way), and give you a bit of code that I am using it for.

I was asked to put some audit fields on each of my files to track create date and time, create user, last changed date and time, and last change user. Ordinarily it would take the same bit of code in each of my models to populate these audit fields. That code isn't all that complex. In fact here it is:

   if ($this->isNewRecord) {
      $this->create_user = Yii::app()->user->name;
      $this->create_time = new CDbExpression('current_timestamp');
   } else {
      $this->change_user = Yii::app()->user->name;
      $this->change_time = new CDbExpression('current_timestamp');
   }

This is not a large piece of code, but if things change, and they decide that they want the name of the function that called it or something else, I have to go back and change a lot of code. Instead, I can wrap this in an event handler and call it when the BeforeSave event is raised. So, I created a class in Components named Audit which has a single static method:

class Audit
{
   public static function beforeSaveHandler($event)
   {
      $model = $event->sender;
      if ($model->isNewRecord) {
         $model->create_user = Yii::app()->user->name;
         $model->create_time = new CDbExpression('current_timestamp');
      } else {
         $model->change_user = Yii::app()->user->name;
         $model->change_time = new CDbExpression('current_timestamp');
      }
   }
}

I still have to attach the handler to each model, but that is easy. It is attached in the init() method of the model. If you don't have one, just create it. It will override the init() method from CActiveRecord.

   public function init()
   { 
      parent::init();
      $this->onBeforeSave = array(new Audit, 'beforeSaveHandler');
   }

You do not really have to call the CActiveRecord::init() function because it does nothing, but I do it anyway just in case the developers of Yii put something there in the future.

So there you have it, a simple beforeSave event handler that populates audit fields in a table. You can add as many event handlers as you want to that init() function. I'm not sure what order they will be executed in, I suspect in the order that they are added. But, I wouldn't count on that as it could change. I also wouldn't write dependencies into the handlers as this will likely add unnecessary complexity to your application.

Let me know what you think.

Yii - Not a bad PHP Framework

I've been working on a little PHP project lately, and to make things a bit easier on myself I decided to use an MVC framework.  In the past I have worked a little with Zend Framework, and it is certainly a full featured framework, backed by Zend, the PHP company.  But in my experience it has a very steep learning curve.  In addition the many parts have great documentation if used in isolation, but the documentation of how the parts work together within an MVC framework is a bit lacking, and in addition I never did figure out how to get dates from the database to a form and back without jumping through a whole lot of hoops.  I am sure that there is an easy way to do it, but I spent hours searching the internet, and found nothing.

I have also looked at Code Igniter.  As much as Zend Framework is an industrial strength framework, Code Igniter is on the opposite side of the MVC framework ledger. About as light as a framework can be yet still be called a framework.  I want a framework to provide at least some assistance.  I don't want to have to completely define my own way to do things.

So I heard about Yii and decided to try it.  Yii occupies the space somewhere between Code Igniter and Zend Framework.  It is fairly responsive, yet contains database classes, form classes and several helpers, validators and filters built in.  It also features a slick code generator called Gii which you can configure with your own templates.  So far in my project I have had several things I wanted to do that were a bit unclear, or uncovered in the documentation.  However, I found that the community around Yii is extremely helpful, and most of my questions already had answers either in the Yii forums or on StackOverflow.

Over the next couple of weeks I will post more of my findings about Yii here on my blog.