Using Smarty Templates With PEAR HTML_QuickForm

Contents

A note from the author

I hope you find this guide, or tutorial, call it what you will, useful, and I also hope it saves you some time. If you do find it useful, I would be grateful if you could make a donation using the button below (and if it wasn't useful, mail me and tell me why not).

I receive a great deal of mail as a result of this article, much of it asking me to solve various PEAR/HTML_QuickForm/Smarty problems. Please bear in mind that the money that puts food on my family's table comes from the consultancy work that I do, so here's the deal: if you would like some help with any of the points discussed in this article, mail me and tell me what that help is worth to you. Quote any amount you like, and if I'm able to help you out you can make a PayPal donation by way of thanks. Fair enough?

Thank you
Keith Edmunds

1. Introduction

What this is and what it isn't

This page is a simple introduction to using Smarty templates with the PEAR HTML_QuickForm classes. It is by no means exhaustive; in fact, it covers a very small fraction of the total functionality of Smarty templates. It is also not definitive: that role is taken by the source code itself, which is of course always right. However, for the newcomer to Smarty templates, the following should be a useful foundation to build upon.

Pre-requisites

It is assumed that you have installed both PHP and PEAR, and that you are familiar with PHP and HTML. You should also be familiar with using PEAR's HTML_QuickForm classes. Help with PEAR can be found at the PEAR website; help with PHP can be found at the PHP website, and an introduction to using HTML_QuickForm can be found in my PEAR HTML_QuickForm Getting Started Guide.

This page is written from a practical perspective. If you want to get the most from it, you should copy the code examples and run them yourself. Quite apart from anything else, templates are about presenting data, and there is no example output present on this page. If you want to see templates in action, you'll need to run the examples for yourself. Finally, the code samples here are for illustrating the use of Smarty templates only. In other words, features such as validating data have been omitted in order to more clearly demonstrate the template features.

2. Basic forms

A brief review

Let's start with a simple form that does not use templates. Later, we will later we will modify it so that it does use a template.

Create the following file and place it in a directory accessible by your web server, and display it in a browser:

  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
  <html>
  <head>
      <title>A simple form</title>
  </head>

  <body>
  <?php
      require_once "HTML/QuickForm.php";

      $form = new HTML_QuickForm('frmTest', 'get');
      $form->addElement('header', 'hdrTesting', 'A simple form');
      $form->addElement('text', 'txtFirstName', 'First name?');
      $form->addElement('text', 'txtLastName', 'Last name?');
      $form->addElement('text', 'txtAge', 'Age?');
      $form->addElement('text', 'txtTelephone', 'Telephone number?');
      $form->addElement('reset', 'btnClear', 'Clear');
      $form->addElement('submit', 'btnSubmit', 'Submit');
      if ($form->validate()) {
          # If the form validates then freeze the data
          $form->freeze();
      }
	  $form->display();
  ?>
  </body>
  </html>

You should see the form displayed; filling in details and submitting it should redisplay the form and data in a static format. If this does not work, or if you do not understand why it works, then please review the PEAR HTML_QuickForm Getting Started Guide.

Notice the following about the file shown above:

3. Some theory

What are templates for?

As we have seen, by default forms devleoped with the PEAR HTML_QuickForm classes are displayed in very simple format, typically a label followed by the form element and with each element placed below the previous one. While perfectly functional, and arguably a good format for development work, the presentation lacks the finishing touches expected of a professional application.

One of the challenges with any kind of program-controlled HTML generation is separating the display elements from the programming code, and this is something that the PEAR classes do well. Templates allow the designer to create far more attractive or complex pages, and to dictate precisely where each element of data will appear. The designer works files which are almost pure HTML; the programmer works with pure PHP code and PEAR classes, and changes to one do not require changes to the other.

How Smarty templates work with HTML_QuickForm

The $form->display(); line above calls the HTML_QuickForms default renderer. By replacing that line, far more control can be gained over the resultant HTML. This also allows us to split the source file into two: a PHP/PEAR code source file, and a template file.

The template file contains HTML with some additional tags. At the simplest level, these tags are just placeholders for data. The role of the renderer is to take the form data and package it in a format that can be used by the template object. The role of the template object is to parse the template file and replace the additional tags with the data from the form.

To create and render a form with a template is a multi-stage process:

  1. The form is created using PEAR classes in the same way as before
  2. A template object is created.
  3. A renderer object is created.
  4. The renderer generates HTML fragments for each element of the form, and these fragments, together with other infomation about the form, are collated in one large array.
  5. The array of form data is passed to the template object
  6. The template object then parses the template file, replacing the tags with the data from the array, and displays the resultant HTML.

4. A simple template form

A simple example

We will now modify the earlier file to use a template. We need a couple more require_once lines at the top of the file, and we replace the $form->display(); line with the template code. We can also strip out all of the HTML; the resultant PHP file is shown below.

  <?php
      require_once "HTML/QuickForm.php";
      require_once 'HTML/QuickForm/Renderer/ArraySmarty.php';
      require_once 'Smarty.class.php';

      $form = new HTML_QuickForm('frmTest', 'get');
      $form->addElement('header', 'hdrTesting', 'Testing Smarty');
      $form->addElement('text', 'txtFirstName', 'First name?');
      $form->addElement('text', 'txtLastName', 'Last name?');
      $form->addElement('text', 'txtAge', 'Age?');
      $form->addElement('text', 'txtTelephone', 'Telephone number?');
      $form->addElement('reset', 'btnClear', 'Clear');
      $form->addElement('submit', 'btnSubmit', 'Submit');
      if ($form->validate()) {
          # If the form validates then freeze the data
          $form->freeze();
      }

      // Create the template object
      $tpl =& new Smarty;
      $tpl->template_dir = '.';
      $tpl->compile_dir  = '/tmp';

      // Create the renderer object	
      $renderer =& new HTML_QuickForm_Renderer_ArraySmarty($tpl);

      // build the HTML for the form
      $form->accept($renderer);

      // assign array with form data
      $tpl->assign('form_data', $renderer->toArray());

      // parse and display the template
      $tpl->display('smarty1.tpl');

  ?>

We also need a template file, which is shown below. This should be placed in the same directory as the PHP source file above.

  <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
  <!--  $Id: smarty-guide.tpl,v 1.1 2005/03/03 19:26:54 kae Exp $ -->
  <html>
  <head>
    <title>Smarty template demonstration</title>
    <style type="text/css">
      {literal}   
        th {
          text-align: right;
        }
      {/literal}
    </style>
  </head>

  <body>
  <h2>
    {$form_data.header.hdrTesting}
  </h2>
  <form {$form_data.attributes}>
    <!-- Display the fields -->
    <table>
      <tr>
        <th>{$form_data.txtFirstName.label}</th>
        <td>{$form_data.txtFirstName.html}</td>
        <th>{$form_data.txtLastName.label}</th>
        <td>{$form_data.txtLastName.html}</td>
      </tr>
      <tr>
        <th>{$form_data.txtAge.label}</th>
        <td>{$form_data.txtAge.html}</td>
        <th>{$form_data.txtTelephone.label}</th>
        <td>{$form_data.txtTelephone.html}</td>
      </tr>
      <!-- Display the buttons -->
      <tr>
        <td align="center" colspan="4">
          {$form_data.btnClear.html}&nbsp;{$form_data.btnSubmit.html}
        </td>
      </tr>
      <!-- Display the copyright -->
      <tr>
        <td style="font-size:11px; color: navy; text-align: center" colspan="4">
          &copy;2004 Tiger Computing Ltd
        </td>
      </tr>
    </table>
  </form>
  </body>
  </html>

The form displayed by the above code, while unlikely to win any design awards, does differ in display somewhat from the previous example. If you have problems making this work then check the following:

  1. Are the filenames the same as listed here?
  2. Are both files in the same directory?
  3. Are both files readable by your web server process?
  4. Is /tmp writable by your web server process?

Step by step: the PHP source

Notice the following differences when compared to the first version:

Let's analyse the files starting with the PHP source code. Other than the extra require_once lines, the first change is when we instantiate a Smarty template object:

  // Create the template object
  $tpl =& new Smarty;
  $tpl->template_dir = '.';
  $tpl->compile_dir  = '/tmp';

After instantiation, we tell the template object where the template file can be found (in this case the current directory, represented by a period), and in which directory to place the compiled version of the template (in this case /tmp). Any directories may be used so long as the web server process has read access to the template directory and write access to the compile directory, and indeed there is some sense in placing the template file in a directory which is not directly accessible by the web server process.

Next, we instantiate a renderer, passing the template object as a parameter:

  // Create the renderer object	
  $renderer =& new HTML_QuickForm_Renderer_ArraySmarty($tpl);

The next step is to use the renderer to create the array containing information about the form, including the HTML fragments for each element. This is done with the following line:

  // build the HTML for the form
  $form->accept($renderer);

The next step is to pass that array data to the template:

  // assign array with form data
  $tpl->assign('form_data', $renderer->toArray());

This assign statement creates a variable form_data within the template, and populates it with the previously-defined array data.

Finally, we call the display method of the template object, which causes it to parse the template file and substitute the placeholders in the template with the actual values passed in the array. The resultant HTML is then output:

  // render and display the template
  $tpl->display('smarty1.tpl');

The data array

Before we consider the template file, let's look in more detail at the array of data created by the renderer. It consists of a number of elements, some of which are themselves arrays. The following pseudo code shows the structure of the array:

  array (
   ['frozen']                   // whether the complete form is frozen
   ['javascript']               // javascript for client-side validation
   ['attributes']               // attributes for <form> tag
   ['hidden']                   // html of all hidden elements
   ['requirednote']             // note about the required elements
   ['errors'] // An array as follows:
       (
           ['1st_element_name'] // Error text for the 1st element
           ...
           ['nth_element_name'] // Error text for the nth element
       ),

   ['header'] // An array as follows:
       (
           ['1st_header_name']  // Header text for the 1st header
           ...
           ['nth_header_name']  // Header text for the nth header
       ),

   ['1st_element_name']         // Array for the 1st element
   ...
   ['nth_element_name']         // Array for the nth element
  );

  where an element array has the form:

  array(
           ['name']             // element name
           ['value']            // element value
           ['type']             // type of the element
           ['frozen']           // whether element is frozen
           ['label']            // label for the element
           ['required']         // whether element is required
  // if element is not a group:
           ['html']             // HTML for the element
  // if element is a group:
           ['separator'] // separator for group elements
           ['1st_gitem_name']   // Array for the 1st element in group
           ...
           ['nth_gitem_name']   // Array for the nth element in group
  );

If we wish to examine this array populated with data, we can add extra line to the PHP file as shown below:

  // render and display the template
  $tpl->display('smarty1.tpl');
  echo "<pre>";var_dump($renderer->toArray());echo "</pre>";

Now that we are familiar with the format of the data array we can examine the template file in detail.

Step by step: the template file

Most of the template file is simple HTML, and does not concern us here. The various tags and commands for Smarty, however, are placed between braces (for example, ). Let's examine these.

The first command, {literal}, appears near the top of the file:

  <style type="text/css">
  {ldelim}literal{rdelim}
    th {
      text-align: right;
    }
  {ldelim}/literal{rdelim}
  </style>

The {literal} tag, delimited by {/literal}, simply means that the enclosed text is to be taken literally, or in other words, is not to be interpreted by the Smart template. This is necessary in this case because the text is an in-line style definition, which requires the use of braces itself. Without the {literal} tag, Smarty would try to interpret the text-align: right; string as a Smarty tag.

The next command is {$form_data.header.hdrTesting}, which forms the text of the level two header. Recall earlier that when we passed the form data array to the template, we stored it in a variable called form_data with the following line:

  $tpl->assign('form_data', $renderer->toArray());

Within that array is an element called header, which is itself an array keyed by the header element names, and with a data component comprising the text of the header. Thus the line {$form_data.header.hdrTesting} references the text of the header called hdrTesting which was passed to the template in the form_data array.

The remaining tags in the template file simply access different elements of the form_data array. Each element of the form, other than headers, has its own array within the form_data array so, for example, the txtFirstName element may be represented within the form_data array as:

  ["txtFirstName"]=>
  array(8) {
    ["name"]=>
    string(12) "txtFirstName"
    ["value"]=>
    string(5) "Keith"
    ["type"]=>
    string(4) "text"
    ["frozen"]=>
    bool(true)
    ["label"]=>
    string(11) "First name?"
    ["required"]=>
    bool(false)
    ["error"]=>
    NULL
    ["html"]=>
    string(62) "Keith"
  }

In this example, form_data.txtFirstName.label would be "First name?", and form_data.txtFirstName.html would be "Keith".

Finally, the template can include static data. In the example above, the copyright statement will always be output verbatim because it is embedded in the template file as HTML.

5. Intelligent template processing

Introduction

Whilst not trying to be a definitive or in-depth guide the use of Smarty templates (that role is best fulfilled by the Smarty website), there are some relatively simple features of Smarty templates which can make templates much more powerful, and this section looks at some of those features.

if-then-else processing

Smarty templates support if-then-else processing. We can modify our original template file as follows:

  <h2>
    {$form_data.header.hdrTesting}
    {if $form_data.frozen}
      <font color="red"> (frozen)</font>
    {/if}
  </h2>

The variable $form_data.frozen is a boolean value that is true when the form is frozen. When the form is first displayed, the heading will be the value of hdrTesting as defined in the PHP file; after the form has been submitted the heading will have " (frozen)" appended in red text.

A more practical example would be to only display the form buttons when the form is not frozen, which we can do as follows:

  <!-- Display the buttons -->
  {if not $form_data.frozen}
  <tr>
    <td align="center" colspan="4">
      {$form_data.btnClear.html}&nbsp;{$form_data.btnSubmit.html}
    </td>
  </tr>
  {/if}

Notice that the {if} clause is closed with {/if}. Smarty templates also support {else} and {elseif} keywords, which work as expected.

Local variables

It's possible to include local variables in a template. The following example sets the style of the labels according to whether or not the form is frozen. While this may not be a particularly useful thing to do, it does demonstrate how local variables may be used. The template file is the same as before except for the following changes:

  <head>
    <title>Smarty template demonstration</title>
    <style type="text/css">
      {literal}   
        th {
          text-align: right;
        }
        .frozen {
          color: red;
        }
        .thawed {
          color: green;
        }
      {/literal}
    </style>
  </head>

  {if $form_data.frozen}
    {assign var="lbl" value="class=frozen"}
  {else}
    {assign var="lbl" value="class=thawed"}
  {/if}

  <body>
  <h2>
    {$form_data.header.hdrTesting}
  </h2>
  <form {$form_data.attributes}>
    <!-- Display the fields -->
    <table>
      <tr>
        <th {$lbl}>{$form_data.txtFirstName.label}</th>
        <td>{$form_data.txtFirstName.html}</td>
        <th {$lbl}>{$form_data.txtLastName.label}</th>
        <td>{$form_data.txtLastName.html}</td>
      </tr>
      <tr>
        <th {$lbl}>{$form_data.txtAge.label}</th>
        <td>{$form_data.txtAge.html}</td>
        <th {$lbl}>{$form_data.txtTelephone.label}</th>
        <td>{$form_data.txtTelephone.html}</td>
      </tr>
      <!-- Display the buttons -->
  

Template comments

Comments may be placed in templates by enclosing them in {* and *}. An example:

  {* This is a comment *}

6. Practical considerations

Introduction

The example above has been kept simple in order to clearly illustrate the points being made. However, in reality there are a few extra items we should include. For example, as it stands there is no indication to the user of which fields are required, something we have taken for granted when using the default renderer. There are some other housekeeping items that we need to take care of too.

Hidden fields and Javascript

Hidden fields are often used on forms, perhaps to hold record numbers or other contextual information. Many forms use Javascript to fulfil various fucntions. As it stands, neither hidden fields nor form Javascript will appear on our pages; however, that information is present in the array passed to the template object. We need to make the following changes to use that data:

  <body>
  {if $form_data.javascript}
  <script language="javascript">
  <!-- 
      {$form_data.javascript}
  //-->
  </script>
  {/if}
  <h2>
    {$form_data.header.hdrTesting|upper}
  </h2>
  <form {$form_data.attributes}>
    <!-- Output hidden fields -->
    {$form_data.hidden}
    <!-- Display the fields -->

Required fields

In the default renderer, required fields are flagged with a red asterisk, and a note is placed at the foot of the form indicating that such fields are required.

When building our own templates, we have to ensure that we include code to flag and explain required fields (if we want that functionality). Let's make our Age field mandatory by making the following change to our PHP file:

  $form->addElement('submit', 'btnSubmit', 'Submit');
  $form->addRule('txtAge', 'Age is required', 'required');
  if ($form->validate()) {
      # If the form validates then freeze the data
      $form->freeze();
  }
  

When we display this form, we see no indication that the Age field is required, and if we leave the Age field blank the form refuses to submit, but we are not told why. To overcome these problems, we need to make some changes to the renderer with the setRequiredTemplate method. This takes a template as a text string argument, although of course we could read an external template file by using PHP's file_get_contents() function. For simplicity, we will specify the template directly.

The template recognises three Smarty variables:

An example should make this clear. Modify the PHP source as follows:

  $renderer =& new HTML_QuickForm_Renderer_ArraySmarty($tpl);

  $renderer->setRequiredTemplate(
     '{if $error}
          <font color="red">{$label}</font>
      {else}
          {$label}
          {if $required}
              <font color="red" size="1">*</font>
          {/if}
      {/if}'
  );
  // build the HTML for the form
  $form->accept($renderer);

The code says that if there is an error, the element label should be printed in red. If there is not an error then print the label as usual, and if this is a required element then print a small red asterisk after the label.

If we display the form now, we will see that the Age field is flagged with a red asterisk, and if we submit the form without a value in the Age field then the label is printed in red. However, we are still not told either what the red asterisk means nor what the error is if we leave the Age field blank.

Error reporting

Similar to the setRequiredTemplate method, the renderer also has a setErrorTemplate. In addition to the three Smarty variables supported by setRequiredTemplate, the setErrorTemplate method also supports the variable. This holds the error text associated with the element as defined in the second argument of the addRule method of the form.

Further modify the PHP source as follows:

  $renderer->setRequiredTemplate(
     '{if $error}
          <font color="red">{$label|upper}</font>
      {else}
          {$label}
          {if $required}
              <font color="red" size="1">*</font>
          {/if}
      {/if}'
  );
  $renderer->setErrorTemplate(
     '{if $error}
          <font color="orange" size="1">{$error}</font><br />
      {/if}
      {$html}'
  );
  // build the HTML for the form
  $form->accept($renderer);

This code says that if there is an error then the error message should be output in a small, orange font followed by a linebreak. The element itself is always output, so the total effect is that any elements with errors have the error message output in orange above them.

Finally, we still need to explain to our user exactly what the red asterisk (or whatever marker we have chosen) means. We do this by adding the requirednote field to the template. Modify the template file as follows:

  <!-- Display the buttons -->
  {if not $form_data.frozen}
  <tr>
    <td align="center" colspan="4">
      {$form_data.btnClear.html}&nbsp;{$form_data.btnSubmit.html}
    </td>
  </tr>
  {/if}
  {if $form_data.requirednote and not $form_data.frozen}
    <tr>
      <td>&nbsp;</td>
      <td valign="top">{$form_data.requirednote}</td>
      <td>&nbsp;</td>
      <td>&nbsp;</td>
    </tr>
  {/if}
  <!-- Display the copyright -->

This will display the default "requirednote" text, but we can customise it in the PHP file if we wish:

  $renderer->setErrorTemplate(
     '{if $error}
          <font color="orange" size="1">{$error}</font><br />
      {/if}{$html}'
  );
  $form->setRequiredNote(
	  '<font color="red" size="2">
		  *&nbsp;
	  </font>
	  <font color="blue" size="2">
		  This field is mandatory
	  </font>');
  // build the HTML for the form  

6. Conclusions

This guide has only scratched the surface of what Smarty can do. Often, however, the hardest part is getting started and I hope this guide will get you past that stage. A good next step would be to read through the Smarty documentation to understand what else Smarty can do, and identify those areas of interest to you.

Keith Edmunds