I have been poking around with Microsoft OneNote recently, and at first glance it appears to be a very powerful note taking app. You can dump stuff in from other Microsoft productivity applications like Word, Excel, and Outlook, and you can even create an Excel spreadsheet directly inside OneNote. It makes taking screen shots easy and has a two dimensional tabbing system that feels just like using a multi subject notebook with the capability of adding as many subjects as you want. I can see this being very useful for students. But with all that power, it feels unfinished to me. You can add notes, draw arrows and circles on top of them for added emphasis, take pictures and highlight the portion of the picture you want to bring attention to, but don't move anything around on the page because all those annotations are just a bunch of independent lines, boxes, and circles. They are not attached to anything, and if you move the big text box, you have to also move all the annotations as well. If you make a correction to your notes in a way that adds space to the text box, you now have to re-annotate the entire page because all your annotations are now in the wrong place. This is a major issue. There is no way to group drawings together (which can be done with the other Microsoft drawing tools), and there is no way to anchor a drawing to a bit of text or a table or an image, or anything else for that matter.
So bottom line is that OneNote is a simple little app that has a lot of potential, and a lot of headache. If you can deal with the pain, it might be able to help you organize your thoughts. I am going to try Evernote.
Mark's Tech Blog
Musings on various tech subjects. Tips and Tricks, and things I've learned.
Tuesday, August 02, 2016
Monday, May 23, 2016
ILE RPG - Sub-procedures vs. Subroutines
When writing new RPG code, I use free format and sub-procedures exclusively. There is no advantage to using the older fixed format or subroutines. Fixed format limits the space you have for expressions, and prevents nicely indented control structures while there is nothing that a subroutine can do that a sub-procedure cannot. Even when maintaining existing code I tend to use free format and sub-procedures unless it is significantly more convenient to do otherwise. that being said, there are a few things to take into consideration. Particularly when it comes to using sub-procedures vs. subroutines.
Sub-procedures control scope as well as providing for code reuse. Subroutines are all about code reuse. It is the scope of a sub-procedure that makes is so useful. Items defined within a sub-procedure cannot be used or referenced outside it, and override similar items defined in the global scope. This is fairly well understood for variables defined with dcl-?. For example:
dcl-s ix Int(5) Inz(10);
procA();
dsply ix;
return;
dcl-proc procA;
dcl-s ix Int(5) Inz(1);
dsply ix;
end-proc;
Will result in:
1
10
Displayed on the console. Why? Because ix in procA is different from the global ix declared above. Sub-procedures allow me to pass in parameters, to allow even easier code reuse. No need to set some global variables, then call the subroutine, then retrieve the result out of other global variables. I simply pass the necessary values as parameters, and retrieve the result as a return value, or from one or more output parameters.
field1 = 'A';
field2 = 15;
exsr convert;
result = field3;
vs.
result = convert('A': 15);
It is this capability of sub-procedures that compels me at times to convert existing subroutines to sub-procedures. Particularly if a subroutine is working with a single set of values, and I need to call it with a different set of fields. This can be a simple conversion, just change BEGSR to DCL-PROC, ENDSR to END-PROC, add parameters, and pass them properly in the call. This is where a hidden feature of the subroutine becomes apparent. A subroutine is not a global entity. It is always scoped to a procedure, either the main procedure, or a sub-procedure. If you simply convert a subroutine to a sub-procedure, but the subroutine contained an EXSR op code, that called subroutine will suddenly be out of scope for the new sub-procedure. To fix this you will need to convert these subroutines called by the sub-procedure you just converted, and any subroutines called by them to sub-procedures as well. This additional work may take the use of a sub-procedure in this particular instance and put it in the realm where just setting and retrieving global variable values is significantly more convenient than converting all the necessary subroutines (and their calls) to sub-procedures.
Here is a quick diagram of how procedures, sub-procedures and subroutine scopes work within an RPG program.
Main Procedure {
subroutine A
subroutine A1
}
sub-procedure 1 {
subroutine B1
subroutine B2
}
sub-procedure 2 {
subroutine C1
subroutine C2
}
Anything in the Main procedure can call sub-procedure 1 or 2, and subroutines A1 or A2, but not subroutines B1, B2, C1, or C2. Sub-procedure 1 can call sub-procedure 2, and subroutines B1 and B2, but not any of the others, and likewise for sub-procedure 2. this is kind of sudo-code as curly brackets are not a part of the RPG syntax. In fact, a main procedure has no explicit starting point or ending point unless it is a liner main procedure. This is just an easy way to visualize the scope.
What do you think, was this helpful to you?
Sub-procedures control scope as well as providing for code reuse. Subroutines are all about code reuse. It is the scope of a sub-procedure that makes is so useful. Items defined within a sub-procedure cannot be used or referenced outside it, and override similar items defined in the global scope. This is fairly well understood for variables defined with dcl-?. For example:
dcl-s ix Int(5) Inz(10);
procA();
dsply ix;
return;
dcl-proc procA;
dcl-s ix Int(5) Inz(1);
dsply ix;
end-proc;
Will result in:
1
10
Displayed on the console. Why? Because ix in procA is different from the global ix declared above. Sub-procedures allow me to pass in parameters, to allow even easier code reuse. No need to set some global variables, then call the subroutine, then retrieve the result out of other global variables. I simply pass the necessary values as parameters, and retrieve the result as a return value, or from one or more output parameters.
field1 = 'A';
field2 = 15;
exsr convert;
result = field3;
vs.
result = convert('A': 15);
It is this capability of sub-procedures that compels me at times to convert existing subroutines to sub-procedures. Particularly if a subroutine is working with a single set of values, and I need to call it with a different set of fields. This can be a simple conversion, just change BEGSR to DCL-PROC, ENDSR to END-PROC, add parameters, and pass them properly in the call. This is where a hidden feature of the subroutine becomes apparent. A subroutine is not a global entity. It is always scoped to a procedure, either the main procedure, or a sub-procedure. If you simply convert a subroutine to a sub-procedure, but the subroutine contained an EXSR op code, that called subroutine will suddenly be out of scope for the new sub-procedure. To fix this you will need to convert these subroutines called by the sub-procedure you just converted, and any subroutines called by them to sub-procedures as well. This additional work may take the use of a sub-procedure in this particular instance and put it in the realm where just setting and retrieving global variable values is significantly more convenient than converting all the necessary subroutines (and their calls) to sub-procedures.
Here is a quick diagram of how procedures, sub-procedures and subroutine scopes work within an RPG program.
Main Procedure {
subroutine A
subroutine A1
}
sub-procedure 1 {
subroutine B1
subroutine B2
}
sub-procedure 2 {
subroutine C1
subroutine C2
}
Anything in the Main procedure can call sub-procedure 1 or 2, and subroutines A1 or A2, but not subroutines B1, B2, C1, or C2. Sub-procedure 1 can call sub-procedure 2, and subroutines B1 and B2, but not any of the others, and likewise for sub-procedure 2. this is kind of sudo-code as curly brackets are not a part of the RPG syntax. In fact, a main procedure has no explicit starting point or ending point unless it is a liner main procedure. This is just an easy way to visualize the scope.
What do you think, was this helpful to you?
Thursday, April 14, 2016
Resequence Records in a Table Using SQL
Every now and then I hear a question like "How can I reset the sequence numbers in my file using SQL?" If you are using DB2 for i, there is a simple solution involving OLAP specifications. These are expressions you can use to query things like rank, dense rank, and row number. It is ROW_NUMBER that we are specifically interested in here. Lets set up a sample table:
create table resequence_sample (
id long int primary key,
order_number dec(7,0) not null,
detail_line dec(5,0) not null,
seq_number dec(5,0) not null,
...
)
So you can see this table has a primary key named id an order number, a detail line number, and a sequence number for sorting the records, among other fields not named. Periodically this sort sequence gets gaps due to deletions, or other operations on the file, and we want to close those gaps up. If I were using RPG and record level access methods, I could simply read the records in order, and reassign seq_number from a counter starting at 1 and continuing until the rows were read. I might also want to reset back to 1 for each new detail_line and order_number. It would look something like this:
counter = 0;
read record;
dow not %eof();
counter += 1;
if order_number <> sv_order_number or
detail_line <> sv_detail_line;
counter = 1;
sv_order_number = order_number;
sv_detail_line = detail_line;
endif;
seq_number = counter;
update record;
read record;
enddo;
This is pretty simple and performance is lightening fast. But what if you want to do this with SQL? Well as I mentioned earlier you can use an OLAP feature of the database called ROW_NUMBER to do the same thing. It would look like this:
update resequence_sample s
set s.seq_number =
( select x.row
from
( select row_number()
over(partition by order_number, detail_line
order by seq_number)
as row,
a.id
from resequence_sample a
) x
where x.id = s.id
)
The magic is in the inner subselect which returns a list of record ids with it's row_number with breaks (partition by) on order_number and detail_line, sorted by (order by) seq_number. Notice the syntax of ROW_NUMBER expression. It is row_number() over(...). The clauses that you can put in the parenthesis of over() determine how the row numbers are assigned. Partition by defines how to group the selected records. Each group get's it's own sequence of numbers starting with 1. You could for example assign a new group of sequence numbers to each order, or as we have done above, to each order detail line. Or if you leave out the partition by clause entirely, you get a single sequence of numbers starting with 1 and incrementing by 1 to the last row selected. Order by allows you to select the sequence of records within each group. This is for numbering purposes only, and is independent of the statement order by. So you could select employee records ordering by name, but assigning row numbers by salary. This makes more semantic sense with the RANK and DENSE_RANK, but it works the same way with ROW_NUMBER.
One last point. You may be wondering why we didn't just put that where x.id = s.id on the inner subselect, and remove the outer subselect all together. The reason is in the way row numbers are assigned. That is by group of selected records starting with 1. If we moved the were clause to the inner subselect, only a single record would be selected there causing the row_number to always be 1. Pulling that where clause out one level allows us to assign row numbers to a group of rows, and then select the row we want the sequence number for. DB2's optimizer is smart enough that it doesn't perform that query more times than it needs to.
This example only shows the business end of the code for RPG. There are various declarations required to support the file and control variables to make it work that I have chosen to leave out. However the SQL example will run as is once you create the table.
create table resequence_sample (
id long int primary key,
order_number dec(7,0) not null,
detail_line dec(5,0) not null,
seq_number dec(5,0) not null,
...
)
So you can see this table has a primary key named id an order number, a detail line number, and a sequence number for sorting the records, among other fields not named. Periodically this sort sequence gets gaps due to deletions, or other operations on the file, and we want to close those gaps up. If I were using RPG and record level access methods, I could simply read the records in order, and reassign seq_number from a counter starting at 1 and continuing until the rows were read. I might also want to reset back to 1 for each new detail_line and order_number. It would look something like this:
counter = 0;
read record;
dow not %eof();
counter += 1;
if order_number <> sv_order_number or
detail_line <> sv_detail_line;
counter = 1;
sv_order_number = order_number;
sv_detail_line = detail_line;
endif;
seq_number = counter;
update record;
read record;
enddo;
This is pretty simple and performance is lightening fast. But what if you want to do this with SQL? Well as I mentioned earlier you can use an OLAP feature of the database called ROW_NUMBER to do the same thing. It would look like this:
update resequence_sample s
set s.seq_number =
( select x.row
from
( select row_number()
over(partition by order_number, detail_line
order by seq_number)
as row,
a.id
from resequence_sample a
) x
where x.id = s.id
)
The magic is in the inner subselect which returns a list of record ids with it's row_number with breaks (partition by) on order_number and detail_line, sorted by (order by) seq_number. Notice the syntax of ROW_NUMBER expression. It is row_number() over(...). The clauses that you can put in the parenthesis of over() determine how the row numbers are assigned. Partition by defines how to group the selected records. Each group get's it's own sequence of numbers starting with 1. You could for example assign a new group of sequence numbers to each order, or as we have done above, to each order detail line. Or if you leave out the partition by clause entirely, you get a single sequence of numbers starting with 1 and incrementing by 1 to the last row selected. Order by allows you to select the sequence of records within each group. This is for numbering purposes only, and is independent of the statement order by. So you could select employee records ordering by name, but assigning row numbers by salary. This makes more semantic sense with the RANK and DENSE_RANK, but it works the same way with ROW_NUMBER.
One last point. You may be wondering why we didn't just put that where x.id = s.id on the inner subselect, and remove the outer subselect all together. The reason is in the way row numbers are assigned. That is by group of selected records starting with 1. If we moved the were clause to the inner subselect, only a single record would be selected there causing the row_number to always be 1. Pulling that where clause out one level allows us to assign row numbers to a group of rows, and then select the row we want the sequence number for. DB2's optimizer is smart enough that it doesn't perform that query more times than it needs to.
This example only shows the business end of the code for RPG. There are various declarations required to support the file and control variables to make it work that I have chosen to leave out. However the SQL example will run as is once you create the table.
Tuesday, June 23, 2015
RPG Service Programs Best Practices
At my current client, I have been doing a lot of work using ILE and service programs. I have decided to start compiling a list of best practices that I have found work out well. Many of these are not my ideas, but have been gleaned from the discussions at midrange.com. This post will not function so much like a blog post, but more as a wiki page. I will return to update it as I have time, or learn new things.
- Create a new binding directory for each Service Program
- This service program specific binding directory will contain only the modules and service programs necessary to build this one service program. When you first create a service program, it will not be readily apparent that you need to do this. However, if you intend to maintain your service program, having a separate binding directory will allow you to add procedures without duplicate symbol errors. Consider an example of just a single common binding directory that contains all your service programs. You can (and should) use a common binding directory to build all the programs in your application, but if you try to use this binding directory to build your service programs you will find that both the existing service program (which is in your common binding directory), and the module you are using to build it have mostly the same exports which will produce duplicate symbol errors. You need a binding directory that does not contain the service program you are building to avoid these errors. Or you need to ignore them in some way. Save yourself the headache of trying to determine which errors you can safely ignore, and just avoid them all. Always create a service program specific binding directory for each service program in your application, even if it is empty. And name it the same as your service program, your builds will run far smoother for it.
- Create a binding source with a single fixed signature for each Service Program
- This is more about the signature than anything else since the binding source if you use it, must be specific to your service program. Name your binding source the same as your service program, and store it in QSRVSRC. Create a signature, and never change it. This has a few implications:
- You cannot reorder the exports in your binding source. Ever, unless you want to recompile the world!
- You cannot change the parameters in your procedures. Without taking extreme care, unless you want to recompile the world!
- Create a prototype file that contains only exported procedure prototypes
- This file is named the same as the service program, and is stored in QPROTOSRC. While you can have procedures that are not exported from your service program, and in a multi-module service program, you may need prototypes for those procedures, keep those separate from the public exports. There is no need to show prototypes that can't be called from programs. Procedures exported from modules, but not exported from the service program can be stored in a separate source file, I suggest naming that the same as the service program with an underscore suffix.
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
Field | Type | Key |
---|---|---|
id | numeric | yes |
description | character | no |
Categories
Field | Type | Key |
---|---|---|
id | numeric | yes |
description | character | no |
Linkages
Field | Type | Key |
---|---|---|
standard_id | numeric | yes |
category_id | numeric | yes |
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:- I need to make sure that I have defined the many to many relationship in my standards model.
- I need to add the check box list to my view and populate it with all the available categories.
- I need to add an attribute to my model to hold the array of selected categories
- I need to add a method to my standards model that actually builds an array of selected categories for a given standard.
- 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.
- 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.
* '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
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:
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.
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.
- 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)
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:
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:
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.
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.
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.
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.