Welcome to RocketSled Tutorial 4.
This tutorial follows on from RocketSled Tutorial 3, and introduces database functionality.
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.
<?php
define('DB_SERVER','localhost');
define('DB_USER','user');
define('DB_PASS','dbpass');
define('DB_NAME','pastries');
define('CLEAR_CACHE',1);
?>
public function systemSetup() { Query::connect(); }
<?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:
parent::MyDbObject('Ingredient') is to tell the Nozzle that this object refers to the table 'ingredient'parent::MyDbObject('SugaryPastry') is to tell the Nozzle that this object refers to the table 'sugary_pastry' (note the naming conventions)$this->depend('SugaryPastry') and $this->support('Ingredient'); tell the Nozzle that there is a one SugaryPastry to many Ingredient relationship$this->get('ingredient_name') refers to the field 'ingredient_name' in the 'ingredient' table
<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.
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).
<?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).
<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"/>.
1.5.7