RocketSled Tutorial 4: The Nozzle

Author: David Grinton thegrinch@workingsoftware.com.au

Table of Contents

Introduction

Introduction

Welcome to RocketSled Tutorial 4.

This tutorial follows on from RocketSled Tutorial 3, and introduces database functionality.

Database backend setup

First we need to set up the required database. We will be using MySQL, substitute the appropriate root password for PASS:

echo "CREATE DATABASE pastries;" | mysql -u root --password=PASS
echo "GRANT ALL ON pastries.* TO 'user'@'localhost' IDENTIFIED BY 'dbpass'" |
mysql -u root --password=PASS

... and create a table `sugary_pastry` in the new database with fields `pastry_id` and `pastry_name`:

echo "CREATE TABLE sugary_pastry (pastry_id int(6) PRIMARY KEY NOT NULL
auto_increment, pastry_name varchar(50) NOT NULL);" | mysql -u user
--password=dbpass pastries

... also a table `ingredient` with fields `ingredient_id`, `pastry_id` and `ingredient_name`:

echo "CREATE TABLE ingredient (pastry_id int(6) NOT NULL, ingredient_id
int(6) NOT NULL auto_increment, ingredient_name varchar(50) NOT NULL, PRIMARY
KEY (pastry_id,ingredient_id));" | mysql -u user --password=dbpass pastries

Note that "user" and "dbpass" are not a very good username/password choice for a database running on a publically accessible system, however for convenience we will use them in the tutorials.

Nozzle Configuration

Next edit WEBROOT/yourdir/packages/nozzle/nozzle.config.php and set the DB_USER, DB_PASS and DB_NAME constants:

<?php
    define('DB_SERVER','localhost');
    define('DB_USER','user');
    define('DB_PASS','dbpass');
    define('DB_NAME','pastries');
    define('CLEAR_CACHE',1);
?>

Connect to the database

As we are now using a database we must initiate access to it, this can be done by adding an override method to MyApplication as follows:

public function systemSetup()
{
    Query::connect();
}

DBObjects

Now we will create a new class in WEBROOT/yourdir/packages/my/my_db_object.class.php for all our db objects to extend:

<?php
   class MyDbObject extends DbObject
   {
      public function MyDbObject($override_class_name)
      {
         $this->DbObject('',$override_class_name);
      }
   }
?>

... then we will alter the existing SugaryPastry and Ingredient classes to use the database:

<?php
   class Ingredient extends MyDbObject
   {
      public function Ingredient()
      {
         parent::MyDbObject('Ingredient');
         $this->depend('SugaryPastry');
      }
 
      public function toString()
      {
         return $this->get('ingredient_name');
      }
   }
?>

<?php
   class SugaryPastry extends MyDbObject
   {
      public function SugaryPastry()
      {
         parent::MyDbObject('SugaryPastry');
         $this->support('Ingredient');
      }
 
      public function toString()
      {
         return $this->get('pastry_name');
      }
   }
?>

In the above classes note:

Pastry Ingredients

Next, we modify the NewPastryDetails form to handle a list of ingredients as well as the new pastry name:

<form name="NewPastryDetails" handler="AddPastry">
   <p>
   <form-error name="pastry_name">MESSAGE<br/></form-error>
   <label id="newPastryDetailsPastryNameLabel" for="pastry_name">Pastry name:
   </label>
   <text-line name="pastry_name">
      <validation type="REQUIRED">
         <error-message>Please enter the pastry name.</error-message>
      </validation>
   </text-line>
   </p>
   <p>
   <form-error name="ingredients">MESSAGE<br/></form-error>
   <label id="newPastryDetailsPastryIngredientsLabel" for="ingredients">Pastry
   ingredients:</label>
   <text-multiline name="ingredients">
      <validation type="REQUIRED">
         <error-message>Please enter the pastry ingredients.</error-message>
      </validation>
   </text-multiline>
   </p>
   <p>
   <submit name="addPastry" value="Add"/>
   </p>
</form>

Note the added <text-multiline> section. The rest of the form doesn't change as forms aren't dependent on what database (if any) the data is going to be used in. There is however a convenience method parse() which automatically inserts data into tables - for this to work the the form inputs must have names which correspond to table columns.

Adding records

Now we will modify the performHandlerTasks() method in the AddPastry class so that it inserts a new object into the database when the form is successfully validated:

public function performHandlerTasks()
{
   if(Application::formPosted())
   {
      $form = Form::load('my.NewPastry');
 
      if ($form->validate())
      {
         $p = new SugaryPastry();
         $p->parse();
 
         foreach (explode("\n",Application::param('ingredients')) as
         $ingredient)
         {
            $i = new Ingredient();
            $i->setSafe('ingredient_name',trim($ingredient));
            $p->add($i);
         }
         $p->save();
 
         Application::redirect('ListPastries');
      }
   }
}

The parse() method above looks for GET/POST etc variables (ie from a posted form) with names matching database fields and sets them correspondingly in the DbObject $p. To actually write these values to the database the save() method must be called.

To manually set database fields use setSafe() instead of parse(). Both parse() and setSafe() automatically apply SQL injection protection.

We use the add() method to add each Ingredient to the new SugaryPastry, this automatically sets the pastry_id field of the new Ingredient to the pastry_id of the new SugaryPastry.

Also note the Application::redirect() method which causes a different Handler to be invoked (in this case, it will display the ListPastries Handler after successfully adding a pastry).

The ListPastries class

Now we can write the ListPastries class. Create WEBROOT/yourdir/packages/my/list_pastries.class.php and add the following code:

<?php
   class ListPastries extends Handler
   {
      public function ListPastries()
      {
         $name = 'ListPastries';
         $description = 'See our delicious sugary pastries!';
         $this->Handler($name, $description);
      }
 
      public function display()
      {
         $disp = Display::current();
         $disp->setValue('pastries',$this->objects());
         $disp->addView('page_content','my.ListPastries');
         $disp->displaySiteTemplate();
      }
 
      public function objects()
      {
          $ob = new SugaryPastry();
          $ob->also('Ingredient');
          return $ob->fetch();
      }
 
      public function performHandlerTasks()
      {
      }
   }
?>

This class is similar to the Home Handler written previously but it is now setting a value 'pastries' with the results of a database query. In the code above the query simply returns all available SugaryPastry objects, but we could have called the clause() method before fetch(), which allows us to restrict the returned results.

Note the call to also() which tells the Nozzle to load the classes we set up with depend() and support() methods in the Ingredient and SugaryPastry classes. This call returns an object of the requested type, which can then have the clause() method called on it to to build a complex query.

The also() method will only return SugaryPastrys that have Ingredients attached to them, to return all SugaryPastrys regardless of whether they have Ingredients or not use maybe(), and to return only SugaryPastrys with no Ingredients use exclude().

Also of note is the page() method and corresponding DataPage class which is used to return only a limited "page" of results (eg the first 50 results out of 2000).

Nozzle flavoured RSML

We will also need a new RSML document to fetch data from the database, create WEBROOT/yourdir/packages/my/list_pastries.rsml.php with the following content:

<fragment>
   <h2>Pastry List</h2>

   <pattern data="pastries">
      <ul>
         <loop>
            <li>
               <local data="toString"/>
               <pattern data="Ingredient">
                  <ul>
                     <loop>
                        <li>
                           <local data="toString"/>
                        </li>
                     </loop>
                  </ul>
               </pattern>
            </li>
         </loop>
      </ul>
   </pattern>
</fragment>

The first <pattern> still references the "pastries" value set with Display::setValue() and <local data="toString"/> still references a method toString() of the objects in the "pastries" value.

The second <pattern> tag refers to the Ingredient class which is support()ed by the SugaryPastry class, and the corresponding <local> tag pulls data from the Ingredient class.

Note that from within the second <pattern> we could refer to the toString() method of the first loop with <local name="pastries" data="toString"/>.


Generated on Wed Oct 22 18:48:19 2008 for RocketSled by  doxygen 1.5.7