B-Forms

Introduction

I have designed B-Forms as a library for quick development of web based applications. It is not very well suited for high-traffic sites since it is not very fast. (At least I think so, but I have not really tested it). But for sites with a reasonably small number of users it should work well.

The concept behind the package is "creatively" copied from Oracle Forms, simplifying the original idea to the limitations of web-based operation and skipping, at least for now base-table blocks. Maybe I will add them in the future.

Creating a form consists of the following steps:

When making my design decisions, I followed the following principles:


The benefits are:



Currently, the package has limitations, which are:

If you want to participate in the development (by adding new functionality to existing classes, by adding new classes, by improving this manual), or just want a feature that may be useful to other people, please do not hesitate to contact me. I will also be glad to answer questions – questions will help me build a FAQ and improve this manual. You can contact me at alpeter at users dot sourceforge dot net.

Tutorial

Throughout the tutorial we will part of a simple blog application – only those parts that require forms.

Creating a simple form

Let us start with a very simple form: this form allows us to create or edit topics in the blog. This page should be called with one parameter: topic specifying which topic is to be edited. If the parameter is not specified, the page will create a new topic.

The structure of the page looks like this:

<?
require_once("b-forms/b-forms.inc"); // Or wherever you have installed it
require_once("b-forms/layout.inc");  // Optional, only if you want to use
                                     // the standard form layout

// Define form structure
$form = new Form("denied.html");  // Or whatever your tempering-detected-access-denied-page is
........

// Define triggers
........

$form->process();

// Generate HTML-view of the form.
........

?>

The call to $form->process() does all the dirty work of determining whether the form is opened for the first time or should be restored, and fires all the required triggers.

Defining form structure

But back to the beginning – let us define the structure of the form.

Our topics will be extremely simple objects – they will only consist of numeric IDs, and names. The following code defines the form:

..............
// Define the form structure

$form = new Form("denied.html");

$block = & new Block("topic");
$block->add_property(new TextProperty("name", "Name", "", TRUE, 64));
$block -> add_property(new ButtonProperty("save", "Save", TRUE));
$block -> add_property(new ButtonProperty("delete", "Delete"));
$block -> add_property(new ButtonProperty("cancel", "Cancel"));

$form -> add_block($block);
...............

The first thing is to create a Form object. The constructor takes two parameters: the location of the page to which the use will be redirected if the form detects tempering with its data (if the electronic signature will be broken), and an optional parameter $auto_detect_changes. The auto-detect-changes functionality was added in version 1.1. To maintain backwards compatibility by default this parameter is FALSE, so that scripts using previous version of the software do not break. In this example we will not use auto-detection.

The next step is creating a block. Please note the usage of the reference operator (&). Because of the way PHP handles objects by always copying them instead of referencing, B-Forms will not work without this &. Creating the block is simple – the constructor only takes block name.

Then we create the name property (the id property is created automatically). This is done by calling method add_property on the block. This method takes two parameters: the property object, and the display object that will be used to display the property. The second parameter is optional – if you don't specify it, the default display will be used for this property. Default display depends on the type of the property, and in case of TextProperty it is TextBox, with the size of the TextBox equal to the length of the TextProperty – exactly what we need here.

The TextProperty constructor takes 5 parameters:

The TextBox display constructor takes two optional parameters:

Buttons are added using the same method, an again we do not specify the display – there is only one kind of display for them, and it is the default. The only time when you need to specify a display for a button is when you want to specify $extras, which carry the same meaning as in the TextBox display..

The constructor for ButtonProperty takes 3 parameters:

Important: block and property names should not start with and underscore (_). If you do use names that start with an underscore you may overwrite internal variables of the block.

Generating HTML-view of the form

Now, let's make sure that the form shows something in the browser by adding the following code at the end. I will oversimplify the HTML to keep accent on the form.

........
echo "<html><body>\n";
echo "<h1>".($form->topic->is_record_existing()?"Edit":"Create").
     " topic</h1>\n";
if (isset($error))
   echo "<h2>$error</h2>";

$form->start_form();

label("topic","name");
echo ": ";
field("topic","name");

echo "<br/>\n";
field("topic","save");
if ($form->topic->is_record_existing())
   field("topic","delete");
field("topic", "cancel");

$form->end_form();
echo "</body></html>\n";
............

Although it looks a bit messy, the structure of this code is very simple. First, we output the title of the form. Here we use the following construct:

($form->topic->is_record_existing()?"Edit":"Create")

As you have noticed, to refer to a block within a form we just use its name after the -> operator. Our form has one block topic which by default has one record. Records can be in several alternative states, two of which are of importance to us right now:

The is_record_existing() call returns TRUE if the record status is RS_OLD, and FALSE if it is RS_NEW. Instead of calling this method, you can call get_record_status() method and compare the values yourself. However, this will not work for forms running in auto-detect-changes mode. Is_record_existing() will.

By default the block creates all records as new, unless you specifically tell it to create an RS_OLD record, when you are about to fill it with the data from the database. If I ever get to implement base-table blocks, these statuses will be set automatically, along with automatic loading of the data.

Second, we check if there is an error and display it. $error is a global variable that should contain the error that was encountered during form validation. When the form is displayed the first time, it will obviously be empty. If however the user has tried to save a topic without the name, this code will be executed again, but the $error variable will contain text, explaining that the name field should be filled.

Third, we start the form by calling $form->start_form(). This call does three things: creates the FORM tag, fires the pre-display processing (which we will discuss later) and sets the global variable $_form that will be later used by the field() and label() calls. Functions field() and label() are created as typing shortcuts to save on typing $form->field and $form->label every time. Please make sure you do not overwrite $_form variable, or you'll screw your form generation.

Generating labels and fields requires a call to the corresponding function with two parameters: the name of the block, and the name of the field. You do not need to manually generate hidden fields. The label() call also surrounds your label with <label></label> tag, so that clicking the label will activate the corresponding field (of course if the browser supports it).

Note, that we only generate the delete button if the record is in status RS_OLD.

And in the end, we call $form->end_form(), which does three things: generates all hidden fields and the electronic signature, closes the form and unsets the $_form variable.

Using standard layouts to generate forms

Now that we have seen how you can manually generate the form layout, we will from now on abandon manual generation. It is difficult, takes a lot of time, and is only worth it when the layout you are planning to produce has to be really irregular. For the rest of this tutorial we will use standard layouts.

To generate a form using a standard layout, we first need include b-forms/layout.inc file, and the define a layout. I usually define all my layouts right when I define the form – later you will see why, although in this example it is not very important:

require_once("b-forms/layout.inc"); 

.............
$form = new Form("denied.html");
$bl = new BaseLayout(); // Create a default single-row layout
.............

Also we now need to include a link to a stylesheet that will assist our standard layout:

echo "<link rel=\"stylesheet\" media=\"screen, projection\" type=\"text/css\" href=\"layout.css\" />\n";

And then our-form generation code (between the calls to $form->start_form() and $form->end_form()) will be look like this:

$bl->show_block("topic");

Yes, that's right – only one line. The BaseLayout class will show all visible entry fields with their labels one per line, and all the buttons at the bottom. The entry fields (and later – buttons) are displayed in the order you define them using the $block->add_property() call. You can change the appearance of the forms, generate by BaseLayout by means of either CSS stylesheet, or the BaseLayoutConfig class, where you can tune up the HTML that the BaseLayout will generate. More details on this in the reference section.

But at this point, the new code will not do exactly the same thing: the delete button will always be there, since standard layouts by themselves cannot possibly know when to hide it. For that we will need triggers, and that is our next subject.

Adding triggers

In order for this form to become more useful than just a web-page that does not do anything, we will now add triggers. The first trigger we need is the trigger that will load the data when the form is opened:

function form_on_open() {
   global $form, $blog_link, $HTTP_GET_VARS;

   if (isset($HTTP_GET_VARS["topic"])) {

      $query = sprintf("SELECT name FROM topics WHERE id = %d",$HTTP_GET_VARS["topic"]);
      $result = mysql_query($query, $blog_link);

      $num_rows = mysql_num_rows($result);

      if ($num_rows > 0) {
         $row = mysql_fetch_row($result);

         $form->topic->append(RS_OLD);

         $form->topic->id = $HTTP_GET_VARS["topic"];
         $form->topic->name = $row[0];
      }
   }
}

The function implementing this trigger should always be called form_on_open(). Here I use a global variable $blog_link that holds an active link to mysql database containing our blog, defined outside the scope of this example in an include file.

If there is no topic parameter in the request, then we really do not have anything to do. But if the topic has been specified, we load the data from the database and set the fields to the correct values. The first thing we do, is add a record to the block by calling $form->topic->append(RS_OLD). The created record is automatically set to status RS_OLD, which signifies the fact the the object existed in the database before we opened the form (so that saving will require an UPDATE rather then an INSERT).

All blocks start out without any records. Records must be appended before you start data operation on the block. You can append records manually by calling $block->append(). It takes one optional parameter: the status with which the new record will be created, default value – RS_AUTO – will create records in status RS_NEW for multi-row blocks or if the form is not in auto-detect-changes mode. For single row blocks in auto-detect-changes mode it will create a record in status RS_INSERT, but that, together with multi-row blocks is the subject of our next example. In this example, append() will create a record instatus RS_NEW.

However, we do not call $form->topic->append(RS_NEW) explicitly here, since we don't have to do any special initialization for the new record. A new record will be automatically appended after all ON_OPEN triggers have completed their job. If we did have some initialization to do, we would have to call $block->append(RS_NEW) manually to create this record, or rely on the ON_APPEND trigger mechanism that will be discussed later.

Notice, that you refer to the values of fields as $form->block_name->field_name, making it very simple to deal with the fields.

Now let's add a trigger to handle the cancel button:

function topic_cancel_on_action($rownum = -1) {
   close_db(); // Or some other function that will release resources

   header("Location: /examples/");
   exit;
}

There are several rules to keep in mind when writing action triggers:

Since processing of a cancel button is always successful, the code above is the simplest action trigger you are ever gonna get. Now let's add the saving and deleting triggers:

function topic_save_on_action($rownum = -1) {
    global $blog_link, $form;

    if ($form->validate()) {
       if ($form->topic->get_record_status() == RS_OLD) {

          $query = sprintf(
             "UPDATE topics ".
             "SET    name = '%s' ".
             "WHERE  id = %d",
             mysql_escape_string($form->topic->name),
             $form->topic->id);

        }
        else {
           $query = sprintf("INSERT INTO topics (name) VALUES ('%s')",
                            mysql_escape_string($form->topic->name));
        }

        @mysql_query($query, $blog_link);
        
        // Check if the query executed successfully.
        if (mysql_errno()) {
           $error = mysql_error();
           return;
        }

        close_db();
        header("Location: /examples/");
        exit;
    }
}

function topic_delete_on_action($rownum = -1) {
   global $blog_link, $form;

   $query = sprintf("DELETE FROM topics WHERE id=%d", $form->topic->id);
   mysql_query($query, $blog_link);

   // Check if the query executed successfully.
   if (mysql_errno()) {
      $error = mysql_error();
      return;
   }

   close_db();
   header("Location: /examples/");
   exit;
}

In the topic_save_on_action() trigger, we encounter the usage of built-in validation. Since we have not defined any validation triggers ourselves, it will run the standard routine of checking for presence of all mandatory fields. It will return TRUE if the validation was successful. If not, the $error variable will contain an error message with the name of the first missing field found.

If the form validated successfully, we check the status of the record. If it is RS_OLD, then we run an update statement, if it is RS_NEW we do an insert. After that we do the usual trigger exit routine.

If validation failed, the trigger does nothing and returns, resulting in the form being displayed again, but with an error message.

There is only one trigger remaining: the trigger that will hide the delete button if the record we display is RS_NEW and therefore cannot be deleted.

function form_pre_display() {
   global $form;
   if (!$form->topic->is_record_existing())
      $form->topic->_properties["delete"]->visible = FALSE;
}

PRE_DISPLAY triggers are fired when you call $form->start_form() function. At this point all the form data initialization should have completed, but the form itself has not yet been displayed. This is the best time to tune the appearance of our form. In our case, if the record is new, then we set the visible attribute of the delete button property to FALSE.

Please note, that in order to access properties as objects (and not as values) you have to use the following construct:

$form->block_name->_properties["field_name"]->...

Well – this is it – we have got a fully functional form. You can find the full text of the example in the examples directory of the library distribution archive. It will contain calls to functions init_db() and close_db() that are necessary for testing these examples, but they are irrelevant for the tutorial purposes.

Multi-row forms

The idea behind multi-row forms is that you want to edit several objects of the same type at the same time. For example, instead of editing one topic at a time, we could have loaded all topics into one form, and then save the changes to all of them at the same time. However, this is not generally accepted in the web world because HTTP protocol is stateless, and it is not easy to implement locking of the records that you have opened and/or started editing. As a result collisions between different users are quite possible. If you edit only one row at a time – the chance of collision is much lower.

However, multi-row blocks are very useful when you edit compound objects. To continue our example we will create a form that allows creation/editing of entries for our blog. To make them compound, we will allow one entry to be posted to several topics at the same time.

Defining form structure

First let's define the form structure. It will now require two blocks and two layouts to display them. We will also from now on start using the auto-detect-changes mode.

// Prepare the form
$form = new Form("denied.html", TRUE);
$bl = & new BaseLayout();
$tl = & new TableLayout(FALSE);

$block = & new Block("entry");
$block->add_property(new TextProperty("title", "Title", "", TRUE, 128), new TextBox(80));
$block->add_property(new DateProperty("post_date", "Post date", "", FALSE));
$block->add_property(new TextProperty("brief", "Intro", "", TRUE, 10000), new TextArea(80, 15));
$block->add_property(new TextProperty("full", "Content", "", FALSE, 100000), new TextArea(80, 25));
$block->add_property(new LayoutElement("Post in topics"), new InlineBlock(&$bl, "topic", &$tl));

$block -> add_property(new ButtonProperty("save", "Save", TRUE));
$block -> add_property(new ButtonProperty("cancel", "Cancel"));
$block -> add_property(new ButtonProperty("delete", "Delete"));

$form -> add_block($block);

$block = & new Block("topic", TRUE);
$block -> add_property(new CheckBoxProperty("include_fl", "", FALSE));
$block -> add_property(new TextProperty("name", "Name", "", TRUE, 64), new TextDisplay());

$form -> add_block($block);

Here we notice several new things:

Generating HTML-view

Amazinly, now with these 2 layouts and one LayoutElement, the form generation looks just like in a much more simple example above:

// Never forget to call this function before you do any output!!!
$form->process();

echo "<html><body><head>\n";
echo "<link rel=\"stylesheet\" media=\"screen, projection\" type=\"text/css\" href=\"layout.css\" />\n";
echo "</head><h1>";
if ($form->entry->is_record_existing())
   echo "Edit";
else
   echo "Create";
echo " entry</h1>\n";

if (isset($error)) echo "<h2>$error</h2>\n";

$form->start_form();
$bl->show_block("entry");
$form->end_form();

echo "</body></html>\n";

To generate the form we generate the main block, in our case entry. The topic block will be generated automatically by the InlineBlock display of the LayoutElement that we added to block entry.

Defining the triggers

When working with multi-row blocks, the values of fields become arrays. So, if in the previous example $form->topic->id was a scalar variable, $form->topic->id in this example is an array of ids. So if we want the id from the second row, we use $form->topic->id[1]. Same with all other field values.

Important: all functions and methods dealing with fields take a $rownum parameter, which is optional. When dealing with single row blocks, you must remember that you should either omit this optional parameter, or use -1. Zero will not work, since the form will look for the 0th element in an array of values, while in case of single-row blocks the value is scalar.

A word about the data model in the database. We have tables entries and topics, and they are linked through an intermediary table called entry_topics, which only consists of two fields: the ID of the entry, and the ID of the topic. To post an entry to a topic we just need to insert a record into this table with two respective IDs.

Let's start with filling the form with the data. We have already seen usage of FORM_ON_OPEN trigger. We could use the same trigger to initialize both blocks on our form, but we will not. Instead, we will use a separate ON_OPEN trigger for each block. This triggers should be name <BLOCK_NAME>_ON_OPEN, and they will be fired in the same order as we added blocks to the form.

function entry_on_open() {
   global $form, $blog_link, $HTTP_GET_VARS;

   if (isset($HTTP_GET_VARS["entry"])) {
      $query = sprintf(
         "SELECT entries.id, ".             //0
         "       entries.title, ".          //1
         "       entries.brief, ".          //2
         "       entries.post_date, ".      //3
         "       entries.full ".            //4
         "FROM   entries ".
         "WHERE  entries.id = %d ",
         $HTTP_GET_VARS["entry"]);

      $result = @mysql_query($query, $blog_link);
      $num_rows = mysql_num_rows($result);

      if (!$num_rows) {
         header("Location: ".$form->_denied_target);
         close_db();
         exit;
      }
      $row = mysql_fetch_row($result);

      $form->entry->append(RS_OLD);

      $form->entry->id = $row[0];
      $form->entry->title = $row[1];
      $form->entry->brief = $row[2];
      $form->entry->post_date = $row[3];
      $form->entry->full = $row[4];

   }
}

Here we see an alternative strategy for handling cases when submitted IDs are wrong. In our topic editing example, we assumed that if submitted ID was not found in the database, we just created a new topic (with whatever ID would be automatically assigned to it). Here, we redirect to a denied page instead. Note the usage of $form->_denied_target: this is the variable that stores the denied target that we passed to the constructor of the form. Of course this particular piece of code is not a very user-friendly way of handling it – the denied page would not know what was wrong – it will not be able to show a good error message. But for the purpose of this example it will do.

function topic_on_open() {
   global $form, $blog_link;

   $query = sprintf(
         "SELECT id, name, topic_id ".  // 0, 1, 2
         "FROM topics ".
         "LEFT JOIN entry_topics ON topic_id = id AND entry_id = %d ".
         "ORDER BY name",
         $form->entry->id);

      // Because of the left join, topic_id column will be null for those
      // topics for which there is no row in the entry_topics for this entry.
      // And it will be not null, if the entry is already included in that topic.
      //
      // When the form is opened for creation, the $form->entry->id will be null,
      // sprintf() will convert it into 0, and no entry_topics will be found.

   $result = mysql_query($query, $blog_link);
   $num_rows = mysql_num_rows($result);
   for ($i=0; $i<$num_rows; $i++) {
      $row = mysql_fetch_row($result);
      if ($row[2]) { // Entry already belongs to the topic
         $form->topic->append(RS_OLD);
         $form->topic->include_fl[$i] = TRUE;
      }
      else { // NULL, Entry does not yet belong to the topic
         $form->topic->append(RS_NEW);
      }
      $form->topic->id[$i]=$row[0];
      $form->topic->name[$i]=$row[1];
   }
}

The topic block should be filled with all topics available, but only topics that currently include this entry should be checked. To do this we use OUTER JOIN: we get all topics, but the third column will be NOT NULL only for those topics, that already include this entry.

Notice, that we only create RS_OLD records on those ENTRY_TOPICS records, that actually exist in the database. You will see how it becomes useful later.

Let's skip the cancel button trigger, since it is virtually the same. If you are gonna use copy-paste for it, as I usually do, do not forget to change the function name to reflect the different name of the block – it should now be called entry_cancel_on_action().

We will also skip the delete button trigger: it looks very similar. Just do not forget to delete ENTRY_TOPICs for this entry. And if you are writing a fully blown blog – do not forget to delete entry comments too. Let's look at the save button trigger.

function entry_save_on_action($rownum = -1) {
   global $blog_link, $domain, $form, $post_date;

   $post_date = $form->entry->post_date;
   if ($post_date == "")
      $post_date = "SYSDATE()";
   else
      $post_date = "'$post_date'";    

First, let us prepare the post-date value, which will be used later either to insert a new entry or update an existing entry. If the user has entered a date, then we will just enclose it in quotes, otherwise we will put a call to SYSDATE() SQL function.

   if ($form->save()) {

      close_db();
      header("Location: /examples/");
      exit;
   }
}

Here we encounter the built-in saving mechanism. The $form->save() call validates the form, and attempts to save it if the validation was a success.It returns TRUE if the saving was successful, or FALSE if not. In the latter case the global variable $error should contain the description of the $error encountered.

Of course, the form does not know how to actually save the data, so it will fire saving triggers, and you should place the proper saving logic into those triggers. The saving mechanism will go through all defined blocks in the order they were added to the form, and then through each record on the form. Depending on the status of the record, it will call either <BLOCKNAME>_ON_UPDATE, <BLOCKNAME>_ON_INSERT, or <BLOCKNAME>_ON_DELETE trrigger with the record's number as the parameter, or -1 for single-row blocks. The triggers will be fired only for those records that actually require saving. So, for example a new record that was not edited by the user will not be saved. If you want to save such records, you may want to call $block -> mark_changed() method on those records.

Let's define the saving triggers for our block entry. For the post-date value we will use the value we already prepared in the global variable.

function entry_on_update($rownum = -1) {
   global $form, $blog_link, $post_date;

   $query = sprintf(
         "UPDATE entries ".
         "SET    title = '%s', ".
         "       brief = '%s', ".
         "       full = '%s', ".
         "       post_date = %s ".
         "WHERE  id = %d",
         mysql_escape_string($form->entry->title),
         mysql_escape_string($form->entry->brief),
         mysql_escape_string($form->entry->full),
         $post_date,
         $form->entry->id);

   mysql_query($query, $blog_link);
}

function entry_on_insert($rownum = -1) {
   global $form, $blog_link, $post_date;

   $query = sprintf(
      "INSERT INTO entries ".
      "   (title, brief, full, post_date) ".
      "VALUES ('%s', '%s', '%s', %s)",
      mysql_escape_string($form->entry->title),
      mysql_escape_string($form->entry->brief),
      mysql_escape_string($form->entry->full),
      $post_date);

   mysql_query($query, $blog_link);

   // We need to retrieve the autoincrement id assigned to our entry.
   // It will be required for saving the topics of this entry
   $form->entry->id = mysql_insert_id($blog_link);
}

As we see, the automatic saving mechanism makes our code very simple: all we have to do is actually save the data. All other decisions have already been made for us.

Now let's define the saving triggers for the topic block.

// This trigger will only be called for records, that are in status RS_INSERT
// and these are records that previously did not exist in the form, thus the
// checkbox was originally off, and now the record has changed, thus the
// checkbox is on.
function topic_on_insert($rownum = -1) {
   global $form, $blog_link;

   $query = sprintf(
         "INSERT INTO entry_topics (entry_id, topic_id) VALUES (%d, %d)",
         $form->entry->id,
         $form->topic->id[$rownum]);

   mysql_query($query, $blog_link);

}

// This trigger will only be called for records in status RS_UPDATE, and these
// are those records that were originally in the database, thus the checkbox was
// on. Since the record has changed to be in status RS_UPDATE, the checkbox must
// be off now. So this is a record to be deleted. However, will will not delete it now,
// - just build a list of ids.
function topic_on_update($rownum = -1) {
   global $form, $delete_list, $join;

   $delete_list .= $join.$form->topic->id[$rownum];
   $join = ', ';
}

In the comments section you see two new record statuses. Actually, there is a total of 6 meaningful record statuses, but you should never compare them manually and use the status probing functions instead (is_record_changed(), is_record_deleted(), is_record_existing() ). In addition to RS_NEW and RS_OLD statuses which we have seen earlier, the additional 4 are:

The saving mechanism only call respective triggers for records in status RS_INSERT, RS_UPDATE, and RS_DELETE. All other records will simply be ignored as they do not require any actual saving.

In addition to the ON_INSERT, ON_UPDATE, and ON_DELETE triggers, there are two more saving triggers: FORM_PRE_SAVE and FORM_POST_SAVE. The PRE_SAVE trigger can be used to set up a save point, if you are using a transactional database. In our example, we will only use the POST_SAVE trigger – this is where we will actually delete the unneeded entry topics based on the $delete_list that we collected in TOPIC_ON_UPDATE trigger. It is more efficient since we will always do only one DELETE.

function form_post_save() {
   global $form, $blog_link, $delete_list;

   if ($delete_list) {
      $query = sprintf(
         "DELETE FROM entry_topics ".
         "WHERE entry_id = %d ".
         "AND topic_id IN (%s)",
         $form->entry->id,
         $delete_list);
      mysql_query($query, $blog_link);
   }
}

Well, this is pretty much it with this example. The full text can be found in the examples section of the library distribution archive.

Properties vs. Displays

In order to use B-Forms efficiently, it is important to understand the difference between Properties and Displays, and how the work together.

Properties correspond to the data structure – they should roughly mirror your database structure. Unfortunately, due to historic reasons, some of the property names, CheckBoxProperty and ButtonProperty namely, may be quite misleading. They should have been called BooleanProperty and ActionProperty instead. (I am planning to do this renaming some time in the future – however, it will require renaming operations in all the code that uses these property types – and since all the code uses ButtonProperty, I have been postponing this renaming for quite a while).

Displays are widgets that actually display the values to the user and let the user manipulate the values. So, if you have a numeric field, you can work with it using several different displays:

$block -> add_property(new NumericProperty('gender','Gender','',TRUE,1),
                       new DropDown( array(1=>'Male', 2=>'Female', 3=>'Other')));
$block -> add_property(new NumericProperty('gender','Gender','',TRUE,1),
                       new RadioButtons( array(1=>'Male', 2=>'Female', 3=>'Other')));

When dealing with DropDowns and RadioButtons, several property types will suffice: TextProperty, NumericProperty, or even the base class Property.

Triggers

Trigger scope

There are four types of triggers:

Trigger types

There are two types of triggers:

Trigger sequence during $form->process()

When the form is opened for the first time (not restored from a submit), the following sequence is fired:

1. form_on_open()
2. for each block:  <block_name>_on_open()
3. (conditionally) <block_name>_on_append() for those blocks that have AUTO-APPEND on, and do not yet have rows.

You generally have a choice of whether you want to fill all your blocks with data in one big form_on_open function, or you can have separate function for each block. Block-level triggers are fired in the order blocks were added to the form.

In addition to loading the data, ON_OPEN triggers can be used for access control: to ensure that the current user has access to this data object.

If the form is restored, the triggers are fired in the following order:

1. form_after_restore()
2. for each block:  <block_name>_after_restore()
3. <block>_<button>_on_action($rownum) on the button that was pressed  OR
   <block>_<button>_on_action() on the default button if no button was pressed and default button is defined.

Form-level and block-level AFTER RESTORE triggers are as interchangeable as ON_OPEN triggers. I use them for two types of activity:

An example of the latter usage would be a moderator's page to pre-moderate several online chats in parallel (all messages for all chats come to the same moderator). The catch is if the moderator also has to categorize messages, and the set of categories differs between different chats. So you first load the message to be moderated, and only then you can load the set of categories based on the chat, to which the loaded message belongs.

$block->add_property(new NumericProperty("", "category", "Category", "", TRUE)); // Default display

function load_categories() {
    global $link, $form;

    $categories = array();

    // Load categories for the chat to which the message belongs. It should
    // produce an associative array in the form 
    //
    //      array ($category_id => $category_name, ...)
    //
    //    in the order that values should be displayed in the drop down.
    ..............

    $form->message->_properties["category"]->display = new DropDown($categories);
}

function form_on_open() {
   ...........
   load_categories();
}

function form_after_restore() {
   ...........
   load_categories();
}

Here, we first create the category property with default display, and then replace its display with the proper DropDown object. Alternatively, we could setup the display as DropDown object with empty list, and then set the value of that list. That would have to be done with the following line:

$block->add_property(new NumericProperty("", "category", "Category", "", TRUE), new DropDown(array()));

............
        $form->message->_properties["category"]->display->values = $categories;
............

Trigger sequence during $form->validate()

If you call $form->validate() (and you do not need to call it unless you are not using $form->validate()), you usually call it from the trigger that handles the Save or OK button.

Validation sequence more or less fully mirrors Oracle Forms sequence, and is the most complex one in B-Forms. If you want to see how it works, set $form->_debug_triggers to TRUE and you will see all the attempts on triggers as they fired.

Validation happens on three levels: form level, record level, and property level. On each level, there you can define two mutually exclusive triggers: ON_VALIDATE and WHEN_VALIDATE. If you define a ON_VALIDATE trigger, it completely replaces all predefined validation on this level and below. If you don't then the predefined validation is executed and after that the WHEN_VALIDATE trigger is fired (or course, if it is defined).

If auto-detect-changes mode is on, the record-level validation only happens for records in status RS_INSERT or RS_UPDATE.

The only way to explain how this sequence operates is to use pseudo-code:

if form_on_validate defined {
   fire form_on_validate
}
else {

   foreach block {
      foreach changed & ~deleted row of the block {
         
         if block_on_validate defined {         
            fire block_on_validate for row
         }
         else {

            foreach property on the block {
               if block_property_on_validate defined {
                  fire block_property_on_validate for row
               }
               else {
                  do default validation of the property

                  fire block_property_when_validate for row
               }
            }           
 
            fire block_when_validate for row
         }
      }
   } 

   fire form_when_validate
}

The rules of thumb are:

Validation triggers should be functions named to the trigger standard that do not return any value. They indicate success or failure through the value of a global variable $error. If it is empty after the trigger function returned, then the validation was OK. If it is not empty, then the validation process stops, and the text of the $error variable is considered to be the error message. Actually, the whole validation process only runs until the first error is found.

Trigger sequence during $form->save()

This method automates the saving process. In auto-detect-changes mode, this method first checks if any changes were detected. If not – it will just return TRUE as if the save operation was successful. If changes are detected, then it calls the $form->validate() method, that executes the validation sequence described above. If validation is not successful, $form->save() will return with value FALSE. Assuming the validation was successful, the method will fire the following sequence of triggers:

1. form_pre_save()
2. for each block:  
       for each record on the block:
            if record status = RS_INSERT: <blockname>_on_insert(record number)
            if record status = RS_UPDATE: <blockname>_on_update(record number)
            if record status = RS_DELETE: <blockname>_on_delete(record number)
3. form_post_save()

FORM_PRE_SAVE is useful for setting up a savepoint in a transactional database, FORM_POST_SAVE is useful for COMMIT, or some form-wide operations that can be done with one SQL statement (like deletion of entry topics in the second example in the tutorial).

The ON_INSERT, ON_UPDATE and ON_DELETE triggers should usually do what their names imply.

If you detect a problem in any of these 5 triggers, you simply set the global variable $error to the description of the problem (and do a ROLLBACK if you are using a transactional database). As soon as $error is not null, the saving sequence is aborted, the $form->save() call returns FALSE, which would normally (as in the tutorial examples) just make the action trigger return, and the form will be again displayed to the user, showing the error message provided.

Trigger sequence during $form->start_form()

Besides all the other functionality of form generation, this method fires the PRE_DISPLAY triggers:

1. form_pre_display()
2. for each block:  <block_name>_pre_display()

As we have seen in the tutorial examples, these triggers can be used to manipulate visibility of certain elements on the form based on the data that has been loaded into the blocks. These triggers are only useful when you are using standard layouts to generate your form. If you are generating the layout yourself by calling field() and label() functions, then you can manage visibility yourself.

ON_APPEND triggers

ON_APPEND triggers fire when $block->append() method is called to create a new record (statuses RS_NEW and RS_INSERT). It fires after the record has been added and all the fields have received their default values. This is a record level trigger, so unless it is a single row block, the $rownum parameter will contain the number of the record just appended.

If you want to avoid firing this trigger because you are appending a record in order to load an existing object into it, then you should call $block->append(RS_OLD).

This trigger can be used in two cases:

Rendering triggers

Sometimes you want more then just displaying the text of a read-only field – you may, for example, make it a link. You have two options for doing this:

So, if in our entry-editing example we would want to make our topic names ($form->topic->name) clickable by surrounding them with a <label> tag, we could add the following two triggers:

// This function will be called every time just before the field 'name' 
// on block 'topic' is generated.
function topic_name_pre_render($rownum) {
global $form;
echo '<label for="'.
$form->topic->_properties['include_fl']->get_form_name($rownum).
'">';
}

// This function will be called every time immediately after the field 'name' 
// on block 'topic' is generated.
function topic_name_post_render($rownum) {
echo '</label>';
}

Standard Layouts

Starting with release 1.2 there are three standard layouts: one for single-row blocks (BaseLayout) and two for multi-row blocks (TableLayout and TemplateLayout).

BaseLayout

This layout shows single-row blocks in a very simple line-per-property manner. For example, the following peace of code

<?php
...........

// Prepare the form
$form = new Form("denied.html");
$bl = & new BaseLayout();

$block = & new Block("person");
$block->add_property(New TextProperty("user_name", "Username", "", TRUE, 32));
$block->add_property(new TextProperty("first_name", "First Name", "", TRUE, 32));
$block->add_property(new TextProperty("last_name", "Last Name", "", TRUE, 32));
$block->add_property(new LayoutElement("Access rights"), new SectionHeader(&$bl));
$block->add_property(new CheckBoxProperty("admin_fl", "Administrator", FALSE));
$block->add_property(new CheckBoxProperty("manager_fl", "Manager", FALSE));

$block -> add_property(new ButtonProperty("save", "Save", TRUE));
$block -> add_property(new ButtonProperty("cancel", "Cancel"));
$form -> add_block($block);

...........

$form->process();

echo "<html><body><head>\n";
echo "<link rel=\"stylesheet\" media=\"screen, projection\" type=\"text/css\" href=\"layout.css\" />\n";
echo "</head>";

echo "<h4>User Details</h4>\n";
if(isset($error)) echo "<h5>$error</h5>\n";

$form->start_form();

$bl->show_block("person");

$form->end_form();
echo "</body></html>\n";

.............
?>

Will generate the following layout (except it will look different if a stylesheet is used):

User Details

Username:

First Name:

Last Name:

Access rights

Administrator:

Manager:



In this example we define layout using:

$bl = & new BaseLayout();

and then use it with the following line of code:

$bl->show_block("person");

It displays all data properties one per line in the order they were added to the block, and then all control properties in one line at the bottom, also in the order they were added to the block.

To insert the “Access rights” header, we use a special “dummy” property type called LayoutElement. The constructor takes one optional parameter $label, which in case of SectionHeader display displays as a sub-header in the form.

There is another display for LayoutElement – Separator. This is just a vertical spacer and it does not require a label from LayoutElement.

When BaseLayout generates the form, it uses BaseLayoutConfig class for the source of the HTML tags to be used. You can customize the look of your forms in your application by defining an include file that will contain your own config class, for example, if you want to avoid the colon after the labels, you can to the following:

<?
  require_once("b-forms/layout.inc");
  
  class MyBaseLayoutConfig extends BaseLayoutConfig {
     var $label_close = </td>";  
  }
?>

Then, when creating a layout you should use the following code:

.........
require_once(my_config_include_file);

$bl = new BaseLayout(new MyBaseLayoutConfig());
.........

If you provide an instance of a config class to the constructor of BaseLayout, it will use your config instead of the default one.

For detailed contents of the config class see class reference.

TableLayout

This layout shows all properties in the order of adding to the block as table columns, and records – as table rows.

For example, the following code:

<?
............

$form = new Form("denied.html", TRUE);

// Prepare layouts
$tl = & new TableLayout();

//Multi-row block "record", for holding project records
$block = & new Block("record", TRUE);
$block -> add_property(new TextProperty("project_name", "Project Name", "", FALSE, 64), new TextBox(30));
$block -> add_property(new ButtonProperty("delete", "Delete"));
for ($i=0; $i<7; $i++)
  $block -> add_property(new NumericProperty("hours_$i", $days[$i], "", FALSE, 2, 1));

$form -> add_block($block);

.............

$form->start_form();
$tl->show_block("record");
$form->end_form();

.............
?>

will generate a form that looks something like (with a proper stylesheet it would look much better):

Project Name

 

Mon

Tue

Wed

Thu

Fri

Sat

Sun



The usage of TableLayout is very similar to the usage of BaseLayout, with the following exceptions:

Block within a block

You can embed TableLayouts within BaseLayouts using LayoutElement with display InlineBlock, an example of which was the blog-entry example.

// Prepare the form
$form = new Form("denied.html", TRUE);
$bl = & new BaseLayout();
$tl = & new TableLayout(FALSE);

........

$block->add_property(new LayoutElement("Post in topics"), new InlineBlock(&$bl, "topic", &$tl));

........

When the main block is generated using its layout (which is the first parameter to the InlineBlock constructor), the specified block (“topic” in this case) will automatically be generated in place of the dummy property, using the layout provided as the thirs paremeter to the InlineBlock constructor.

If the LayoutElement does not have a label, the inner block will be rendered without a label and for the full width of the form. If you want a layout without a label, but as wide as the data column of the form, they you can use “&nbsp;” as the label.

TemplateLayout

TemplateLayout is designed for multi-row blocks, for cases more complex than TableLayout can handle. For example, if you want to display each record in several short lines rather than one long line.

Basically, TemplateLayout merges the best of two worlds: the automatic generation of multiple rows and manual layout of each row – through the use of a template. A template is a user-defined function that takes one parameter: the number of the record and generates the HTML-code for the given record using label() and field() calls.

Just before the first row, the function is called with TL_START value of the $rownum parameter, and immediately after the last row, this function is called with TL_END value of the $rownum parameter. If there are no records, TL_START and TL_END calls are not made. This allows you to open and close the layout (for example, open and close the <table> tags).

On average, it will look like this:

function template_function($rownum) {
global $form;

if ($rownum == TL_START) {
echo '<table>';
return;
}

if ($rownum == TL_END) {
echo '</table>';
return;
}

echo '<tr class="'.($rownum%2==0?'odd':'even').'">'; // Record numbers start with 0!

// ... your code to generate the row

echo "</tr>\n";
}

$tl = & new TemplateLayout('template_function');
$tl - >show_block('...your-block-name...');

Common Problems

In this section I will address common problems encountered by developers, including myself. Currently there is only one :)

Fatal error: Call to a member function on a non-object in ....\b-forms\b-forms.inc on line 9XX

In version 1.0 there was no validation of parameters that you provide when calling label() and field() fucntions. As a result, when you provide erraneous values, this is the error message you would normally get. It is not at all a problem of the library, it is the problem of the value you provided. Very often comes from copy-paste type of new module creation, which I personally use very often. In version 1.0.1 I have added validation of the provided values, so that you will get a more meaningful message, like:

Fatal error: field(): property name1 does not exist in block topic in ....\b-forms\b-forms.inc on line 9XX

It will still appear as an error in b-forms, although it is not.