Introduction to MVC programming with Agavi, Part 2: Add forms and database support with Agavi and Doctrine

Learn to build scalable Web applications with the Agavi framework

Work with the scalable, open-source Agavi framework to create an input form, use Doctrine to auto-generate the data models for the project, and integrate these models into the Agavi project in Part 2 of this five-part series.

Vikram Vaswani, Founder, Melonfire

Photo of Vikram VaswaniVikram Vaswani is the founder and CEO of Melonfire, a consulting services firm with special expertise in open-source tools and technologies. He is also the author of the books PHP Programming Solutions and PHP: A Beginners Guide.



27 October 2009 (First published 28 July 2009)

Also available in Chinese Russian Japanese Vietnamese Portuguese

Introduction

Frequently used acronyms

  • API: Application program interface
  • CRUD: Create Read Update Delete
  • CSS: Cascading stylesheet
  • HTML: Hypertext Markup Language
  • MVC: Model-View-Controller
  • ORM: Object-Relational Mapping
  • SQL: Structured Query Language
  • SVN: Subversion
  • URL: Uniform Resource Locator
  • XHTML: Extensible Hypertext Markup Language
  • XML: Extensible Markup Language

In the first part of this article series, I introduced you to Agavi and explained some of the features that make it uniquely suited to building scalable, standards-compliant Web applications. Through the use of an example application named Web Automobiles Sales Platform (WASP), I guided you through the basics of creating a new Agavi project, understanding Agavi's recommended file system layout, and becoming familiar with Agavi's command-line build script. I also introduced you to the fundamental components of any Agavi application—Actions, Views and Routes—and showed you some of Agavi's built-in input validators.

While Agavi can certainly be used to serve up static content, it really shines when you use it for something more complex. And in this second part, you'll do just that—over the next few pages, you'll learn how to receive, validate, and process input from Web forms, as well as connect your Agavi application to a MySQL database.


Creating forms with Agavi

First up, here's a quick reminder of what the WASP application's index page looks like (Figure 1):

Figure 1. The WASP application index page
Screen capture of the WASP application index page

You'll remember that you've already put code in place to handle the two links to static content. So let's move on, and deal with the "Contact Us" link. As the name suggests, this link points to a contact form that interested parties can use to contact the automobile dealership. The general procedure you'll use to implement this functionality is similar to that used when you built the StaticContentAction in the previous article; only the code will differ.

To begin, fire up your Agavi build script and enter values as below:

shell> agavi action-wizard
...
Module name: Default
Action name: Contact
Space-separated list of views to create for Contact [Success]: Input Error Success

This creates a new ContactAction and three additional views. You're already familiar with the two standard views, ContactSuccessView and ContactErrorView, which are displayed depending on whether the Action succeeds or fails. The third view, ContactInputView, is new to you; it is the initial view the user sees, and it represents the Web form that will accept user input.

Add a new route for the ContactAction in $WASP_ROOT/app/routing.xml file, as in Listing 1:

Listing 1. The Default/ContactAction route definition
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes> 
       ...          
      <!-- action for contact form "/contact" -->
      <route name="contact" pattern="^/contact$" module="Default" 
       action="Contact" />

    </routes>
  </ae:configuration>
</ae:configurations>

While you're at it, update the master template at $WASP_ROOT/app/templates/Master.php and hyperlink the "Contact Us" link in the main menu to the above route (Listing 2):

Listing 2. The Master template
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
...
      <div id="menu">
        <ul>
          <li><a href="<?php echo $ro->gen('index'); ?>">
           Home</a></li>
          <li><a href="#">For Sale</a></li>
          <li><a href="<?php echo $ro->gen('content', 
           array('page' => 'other-services')); ?>">Other 
            Services</a></li>
          <li><a href="<?php echo $ro->gen('content', 
           array('page' => 'about-us')); ?>">About Us</a></li>
          <li><a href="<?php echo $ro->gen('contact'); ?>">
           Contact Us</a></li>
        </ul>
      </div>
...

Next, within the ContactAction class file at $WASP_ROOT/app/modules/Default/actions/ContactAction.class.php, specify that the ContactInputView displays by default and on all GET requests, by defining the getDefaultViewName() and executeRead() methods (Listing 3):

Listing 3. The Default/ContactAction definition
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }
  
  public function executeRead(AgaviRequestDataHolder $rd)
  {           
    return 'Input';
  }
}
?>

Update the corresponding template file at $WASP_ROOT/app/modules/Default/templates/ContactInput.php with a simple Web form, as in Listing 4. Note that you can find the CSS rules for this form in the code archive that accompanies this article. (See Download.)

Listing 4. The Default/ContactInput template
<h3>Contact Us</h3>
<form action="<?php echo $ro->gen(null); ?>" method="post">
  <label for="name" class="required">Name:</label>
  <input id="name" type="text" name="name" />
  <p/>
  <label for="email" class="required">Email address:</label>
  <input id="email" type="text" name="email" />
  <p/>
  <label for="message" class="required">Message body:</label>
  <textarea id="message" name="message" style="width:300px; height:200px">
  </textarea>
  <p/>
  <input type="submit" name="submit" class="submit" value="Send Message" />
</form>

And now, when you revisit the WASP index page in your browser and click the "Contact Us" link, you should see a Web form, as in Figure 2:

Figure 2. The WASP contact form
Screen capture of the WASP contact form

You're only halfway there. You still need to define what happens when a user fills in the form fields and submits the data.


Validating form input

When the user submits the form, the client browser sends the input data to the server in a POST transaction. And you'll remember, from earlier, that Agavi operates an extremely strict input filter: Any GET or POST variables that aren't explicitly specified in its validation rules are automatically dropped. Therefore, when you define how form input is handled, the first step is always to define validators for the form's input fields.

Since the ContactInput form contains three fields, it's necessary to define validators for each of these fields. Here's what the validation rules at $WASP_ROOT/app/modules/Default/validate/Contact.xml look like (Listing 5):

Listing 5. The Default/ContactAction validator
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Default/config/validators.xml"
>
  <ae:configuration>
    
    <validators method="write">
      <validator class="string">
        <arguments>
          <argument>name</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Name is missing</error>
          <error>ERROR: Name is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
      
      <validator class="email">
        <arguments>
          <argument>email</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Email address is missing</error>
          <error>ERROR: Email address is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
      
      <validator class="string">
        <arguments>
          <argument>message</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Message body is missing</error>
          <error>ERROR: Message body is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
    </validators>
    
  </ae:configuration>
</ae:configurations>

The configuration in Listing 5 sets up two validators for three form fields: AgaviStringValidators for the name and message fields, and an AgaviEmailValidator for the email field. In particular, notice the <error> blocks that accompany each validator; these define error messages for different failure scenarios, and they're a key component in something you will see a little further along (trust me, it's worth waiting for!).

Assuming the input data passes validation, Agavi will look for an executeWrite() method in the ContactAction that defines what is to be done with the POST-ed input. In this case, all that's needed is to format the input into an e-mail message and send it to a designated e-mail address. So, the second step is to add an executeWrite() method to the ContactAction that does just this, as in Listing 6:

Listing 6. The Default/ContactAction definition with a new executeWrite() method
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
  ...

  public function executeWrite(AgaviRequestDataHolder $rd)
  {           
    $name = $rd->getParameter('name');
    $email = $rd->getParameter('email');
    $message = $rd->getParameter('message');
    $subject = 'Contact form submission';
    $to = 'webmaster@wasp.example';
    if (@mail($to, $subject, $message, "From: $name <$email>\r\n")) {
      return 'Success';
    } else {
      return 'Error';
    }     
  }
}
?>

Depending on whether the e-mail message is transmitted, Agavi will render the ContactSuccessView or the ContactErrorView. You don't need anything very fancy in either of these View templates: a simple message indicating the result of form submission will suffice. Here's what $WASP_ROOT/app/modules/Default/templates/ContactSuccess.php looks like:

Your message was successfully sent!

And here's what $WASP_ROOT/app/modules/Default/templates/ContactError.php looks like:

There was an error. Your message could not be sent. Please try again later.

And you're done! Try it out, and see what you think.


Using the form population filter

Now, if you're sharp-eyed, you'll notice something: the way things are set up right now, the application shows the same behavior (rendering the ContactErrorView) if there's an error sending the e-mail message, or if one or more of the form fields fails input validation. In the real world, it's quite likely that you'd want the application's response to these two cases to be different. More specifically, if the user's input fails validation, you usually want the application to re-render the same form with the invalid fields highlighted, so that the user can correct them and resubmit the form.

Agavi comes with a tool that's almost magical in how easily it lets you accomplish the above: the AgaviFormPopulationFilter. To see it in action, go back to your ContactAction (Listing 3) and add the code in Listing 7 to it:

Listing 7. The Default/ContactAction definition with the FPF activated
<?php
class Default_ContactAction extends WASPDefaultBaseAction
{
   ...
  
  public function handleError(AgaviRequestDataHolder $rd)
  {       
    return 'Input';
  }
}
?>

Agavi's default behaviour when a form fails validation is to render the corresponding Error View. Adding a handleError() method to the Action with the name of a different View overrides this behaviour. The above code tells Agavi to re-render the ContactInputView if any of the form fields fail validation.

Now, go back to the "Contact Us" form in your Web browser and try submitting it with empty or invalid data. Instead of the ContactErrorView, Agavi should re-render the ContactInputView with error messages below the invalid fields, as in Figure 3:

Figure 3. The WASP contact form, with input errors highlighted
Screen capture of the WASP contact form, with input errors highlighted

This is the handiwork of the AgaviFormPopulationFilter. When one or more of a form's input fields fail validation, this tool takes care of automatically displaying error messages for the invalid fields. You'll notice that it also takes care of putting valid input values back into the form so the user doesn't need to re-type them. The error messages themselves are retrieved from the validators (remember the <error> blocks I pointed out on the previous page?).

The AgaviFormPopulationFilter is enabled by default in an Agavi application. You can turn it off, or change its behaviour, by adjusting the parameters in $WASP_ROOT/app/config/global_filters.xml. Here's an example of what the default configuration looks like:

<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/filters/1.0">

  <ae:configuration context="web">
    <filters>
      <filter name="FormPopulationFilter" class="AgaviFormPopulationFilter">
        
        <!-- only run for request method "write" (=POST on web) 
        by default (can be changed at runtime, of course) -->
        <!-- if you omit this, it will never run -->
        <ae:parameter name="methods">
          <ae:parameter>write</ae:parameter>
        </ae:parameter>
        
        <!-- only run for output type "html" (so it doesn't break on, 
         say, JSON data) -->
        <!-- if you omit this, it will run for all output types -->
        <ae:parameter name="output_types">
          <ae:parameter>html</ae:parameter>
        </ae:parameter>
        
        <!-- error message insertion rules -->
        <!-- they are run in sequence; once the first one matched, 
         execution stops -->
        <!--
          errors that belong to more than one field (e.g. date validator) 
          can be handled using "multi_field_error_messages"
          "normal" errors are handled through "field_error_messages"
          errors that yield no match and those that have no corresponding 
          field are inserted using rules defined in "error_messages".
        -->
        
        <!-- for all field error messages. -->
        <ae:parameter name="field_error_messages">
          <!-- ${htmlnsPrefix} is either empty (for HTML) or something like 
          "html:" for XHTML documents with xmlns="..." notation. Always use this, 
          makes your code more bullet proof. XPath needs the namespaces when the document 
          is namespaced -->
          
          <!-- all input fields that are not checkboxes or radios, and 
          all textareas -->
          <ae:parameter name="self::${htmlnsPrefix}input[not(@type='checkbox' 
           or @type='radio')] | self::${htmlnsPrefix}textarea">
            <!-- if this rule matched, then the node found by the rule is our 
             starting point for inserting the error message(s). -->
            
            <!-- can be any of "before", "after" or "child" (to insert as prev, 
            next sibling or last child) -->
            <ae:parameter name="location">after</ae:parameter>
            <!-- a container groups all errors for one element. ${errorMessages} 
            is a string containing all errors (see below) -->
            <ae:parameter name="container">
             <![CDATA[<div class="errors">${errorMessages}</div>]]>
            </ae:parameter>
            <!-- this defines the HTML for each individual error message; 
            those are then put into the container. ${errorMessage} is the 
            error message string -->
            <ae:parameter name="markup">
             <![CDATA[<p class="error">${errorMessage}</p>]]>
            </ae:parameter>
          </ae:parameter>
          
          <!-- all other inputs - note how we select the parent element and 
          insert ourselves as last child of it -->
          <ae:parameter name="parent::*">
            <ae:parameter name="location">child</ae:parameter>
            <ae:parameter name="container">
             <![CDATA[<div class="errors">${errorMessages}</div>]]>
            </ae:parameter>
            <ae:parameter name="markup">
             <![CDATA[<p class="error">${errorMessage}</p>]]>
            </ae:parameter>
          </ae:parameter>
        </ae:parameter>
        
        <!--
        <ae:parameter name="multi_field_error_messages">
        </ae:parameter>
        -->
        
        <!-- everything that did not match any of the rules above, 
        or errors that do not belong to a field -->
        <ae:parameter name="error_messages">
          <!-- insert before the element -->
          <!-- that can be an input, or a form, if the error does not belong 
          to a field or didn't match anywhere else -->
          <ae:parameter name="self::*">
            <ae:parameter name="location">before</ae:parameter>
            <!-- no container here! we just insert paragraph elements -->
            <ae:parameter name="markup">
             <![CDATA[<p class="error">${errorMessage}</p>]]>
            </ae:parameter>
          </ae:parameter>
        </ae:parameter>        
      </filter>

      ...
    </filters>
  </ae:configuration>
</ae:configurations>

Since the AgaviFormPopulationFilter takes care of errors in input validation, you can now also update $WASP_ROOT/app/modules/Default/templates/ContactError.php to state:

There was an error sending your message. Please try again later.

Integrating Agavi with Doctrine

Now that you know how forms work in Agavi, look at something a little more complicated. In the previous example, the Action simply converted the user's input into an e-mail message and sent it out using PHP's mail() function. However, another common requirement is to read or write user input to a data store, either a file or a database table. Since Agavi comes with connectors for most common database systems, this is fairly easy to accomplish.

Once you've got a database, you also need Models to interact with it. Models make it possible to manage, manipulate, and perform calculations on data. While you can write your own Models, an easier way is to auto-generate them using an object relational mapper such as Doctrine or Propel. In this article series, you'll use Doctrine, which is quite popular for its flexibility and ease of use.

To illustrate this, create some new Actions, allowing sellers to list their used vehicles for sale in the WASP application, and buyers to view these listings and send purchase inquiries to the dealership. Submitted listings will be stored in a MySQL database, and will become visible and searchable on the WASP site after being approved by a moderator.

Before you can write any Action code to perform the above tasks, you need to get Doctrine and Agavi talking to each other. The following steps describe the process:

Step 1: Create the application database

To begin, drop to your MySQL command prompt, and create a new, empty database for the application:

shell>mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 59
Server version: 5.1.28-rc-community MySQL Community Server (GPL)
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

mysql> CREATE DATABASE wasp;
Query OK, 1 row affected (0.00 sec)

While you're at it, define a user account with access privileges to only this database. This is a good security practice, as it ensures that your other databases remain secure even if this user account is compromised.

mysql> GRANT ALL ON wasp.* TO wasp@localhost IDENTIFIED BY 'wasp';
Query OK, 1 row affected (0.00 sec)

Now, create some tables to store vehicle listings, as follows:

mysql> USE wasp;
Database changed

mysql> CREATE TABLE IF NOT EXISTS `listing` (
    ->   `RecordID` int(10) unsigned NOT NULL AUTO_INCREMENT,
    ->   `RecordDate` date NOT NULL,
    ->   `OwnerName` varchar(255) NOT NULL,
    ->   `OwnerTel` varchar(25) DEFAULT NULL,
    ->   `OwnerEmail` text NOT NULL,
    ->   `VehicleManufacturerID` int(11) NOT NULL,
    ->   `VehicleModel` varchar(255) NOT NULL,
    ->   `VehicleYear` year(4) NOT NULL,
    ->   `VehicleColor` varchar(30) NOT NULL,
    ->   `VehicleMileage` int(11) NOT NULL,
    ->   `VehicleIsFirstOwned` tinyint(1) NOT NULL,
    ->   `VehicleAccessoryBit` int(11) NOT NULL,
    ->   `VehicleIsCertified` tinyint(1) NOT NULL,
    ->   `VehicleCertificationDate` date DEFAULT NULL,
    ->   `VehicleSalePriceMin` int(11) NOT NULL,
    ->   `VehicleSalePriceMax` int(11) NOT NULL,
    ->   `VehicleSalePriceIsNegotiable` tinyint(1) NOT NULL DEFAULT '0',
    ->   `Note` text,
    ->   `OwnerCity` varchar(255) NOT NULL,
    ->   `OwnerCountryID` int(11) NOT NULL,
    ->   `DisplayStatus` tinyint(1) NOT NULL,
    ->   `DisplayUntilDate` date DEFAULT NULL,
    ->   PRIMARY KEY (`RecordID`)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.13 sec)

This is the main data store for the application. It contains details of vehicles listed for sale, including such information as the vehicle's make and model, year of manufacture, color, current mileage, location, and sale price. Each vehicle record also includes two administrative fields, DisplayStatus and DisplayUntilDate, which control whether the listing appears on the WASP site and for how long.

Note that this table uses foreign key references in two fields, VehicleManufacturerID and OwnerCountryID, which specify the vehicle's manufacturer and the owner's current country respectively. Go ahead and create the source tables for these key references:

mysql> CREATE TABLE IF NOT EXISTS `manufacturer` (
    ->   `ManufacturerID` int(11) NOT NULL AUTO_INCREMENT,
    ->   `ManufacturerName` varchar(255) NOT NULL,
    ->   PRIMARY KEY (`ManufacturerID`)
    -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)

mysql> CREATE TABLE IF NOT EXISTS `country` (
    ->   `CountryID` int(11) NOT NULL AUTO_INCREMENT,
    ->   `CountryName` varchar(255) NOT NULL,
    ->   PRIMARY KEY (`CountryID`)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)

While you're at it, populate these source tables with some example records as well:

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`) 
      VALUES(1, 'Ferrari');
Query OK, 1 row affected (0.06 sec)

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`) 
      VALUES(2, 'Porsche');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `manufacturer` (`ManufacturerID`, `ManufacturerName`) 
      VALUES(3, 'BMW');
Query OK, 1 row affected (0.00 sec)


mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(1, 'United States');
Query OK, 1 row affected (0.05 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(2, 'United Kingdom');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(3, 'India');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(4, 'Singapore');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(5, 'Germany');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(6, 'France');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(7, 'Italy');
Query OK, 1 row affected (0.00 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(8, 'Spain');
Query OK, 1 row affected (0.02 sec)

mysql> INSERT INTO `country` (`CountryID`, `CountryName`) 
      VALUES(9, 'Hungary');
Query OK, 1 row affected (0.00 sec)

Step 2: Download and add Doctrine libraries to the application

Once the database is set up, the next step is to download the Doctrine libraries and add them to your application. To install Doctrine, visit its home page, download and uncompress the source code archive, and copy the contents of the archive's lib/ directory to $WASP_ROOT/libs/doctrine/. Find links to the Doctrine Web site in Resources. This article uses Doctrine V. 1.1.

shell> cd /usr/local/apache/htdocs/wasp/libs
shell> mkdir doctrine
shell> cp -R /tmp/Doctrine-1.1.1/lib/* doctrine/

Step 3: Create and add Doctrine models to the application

The next step is to generate Doctrine models for your application. Doctrine can do this for you automatically, through its generateModelsFromDb() method. First, create a temporary directory for the output models:

shell> cd /tmp
shell> mkdir models

Then, create a simple PHP script that uses Doctrine to auto-generate models for the database objects created earlier. Listing 8 is an example of one such script, /tmp/doctrine-gen.php (for details on how this works, refer to the Doctrine manual, linked in Resources):

Listing 8. A PHP script to generate Doctrine models
<?php
// include main Doctrine class file
// change this per your system
include '/usr/local/apache/htdocs/wasp/libs/doctrine/Doctrine.php';
spl_autoload_register(array('Doctrine', 'autoload'));

// create Doctrine manager
$manager = Doctrine_Manager::getInstance();

// create database connection
$conn = Doctrine_Manager::connection('mysql://wasp:wasp@localhost/wasp', 'doctrine');

// auto-generate models
Doctrine::generateModelsFromDb('models', array('doctrine'), 
 array('generateTableClasses' => true));
?>

Execute this script using the PHP interpreter from the command line:

shell> php doctrine-gen.php

Doctrine start to generate models corresponding to your database tables. Once the script has executed, look inside /tmp/models/ and /tmp/models/generated/, and you should see something like Figure 4:

Figure 4. Auto-generated Doctrine models
Screen capture with list of auto-generated Doctrine models

The files shown in Figure 4 are classes representing your database objects. The classes inside the /tmp/models/generated/ directory are base classes generated by Doctrine, while those inside /tmp/models are child classes that you can use to add extra functionality to the base classes. To illustrate, look inside /tmp/models/generated/BaseListing.php, and you'll see a Doctrine object whose properties correspond to fields of the MySQL database table:

<?php

/**
 * BaseListing
 * 
 * This class has been auto-generated by the Doctrine ORM Framework
 * 
 * @property integer $RecordID
 * @property date $RecordDate
 * @property string $OwnerName
 * @property string $OwnerTel
 * @property string $OwnerEmail
 * @property integer $VehicleManufacturerID
 * @property string $VehicleModel
 * @property integer $VehicleYear
 * @property string $VehicleColor
 * @property integer $VehicleMileage
 * @property integer $VehicleIsFirstOwned
 * @property integer $VehicleAccessoryBit
 * @property integer $VehicleIsCertified
 * @property date $VehicleCertificationDate
 * @property integer $VehicleSalePriceMin
 * @property integer $VehicleSalePriceMax
 * @property integer $VehicleSalePriceIsNegotiable
 * @property string $Note
 * @property string $OwnerCity
 * @property integer $OwnerCountryID
 * @property integer $DisplayStatus
 * @property date $DisplayUntilDate
 * 
 * @package    ##PACKAGE##
 * @subpackage ##SUBPACKAGE##
 * @author     ##NAME## <##EMAIL##>
 * @version    SVN: $Id: Builder.php 5441 2009-01-30 22:58:43Z jwage $
 */
abstract class BaseListing extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->setTableName('listing');
        $this->hasColumn('RecordID', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'unsigned' => 1, 
         'primary' => true, 'autoincrement' => true));
        $this->hasColumn('RecordDate', 'date', null, 
         array('type' => 'date', 'notnull' => true));
        $this->hasColumn('OwnerName', 'string', 255, 
         array('type' => 'string', 'length' => 255, 'notnull' => true));
        $this->hasColumn('OwnerTel', 'string', 25, 
         array('type' => 'string', 'length' => 25));
        $this->hasColumn('OwnerEmail', 'string', null, 
         array('type' => 'string', 'notnull' => true));
        $this->hasColumn('VehicleManufacturerID', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleModel', 'string', 255, 
         array('type' => 'string', 'length' => 255, 'notnull' => true));
        $this->hasColumn('VehicleYear', 'integer', null, 
         array('type' => 'integer', 'notnull' => true));
        $this->hasColumn('VehicleColor', 'string', 30, 
         array('type' => 'string', 'length' => 30, 'notnull' => true));
        $this->hasColumn('VehicleMileage', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleIsFirstOwned', 'integer', 1, 
         array('type' => 'integer', 'length' => 1, 'notnull' => true));
        $this->hasColumn('VehicleAccessoryBit', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleIsCertified', 'integer', 1, 
         array('type' => 'integer', 'length' => 1, 'notnull' => true));
        $this->hasColumn('VehicleCertificationDate', 'date', null, 
         array('type' => 'date'));
        $this->hasColumn('VehicleSalePriceMin', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleSalePriceMax', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('VehicleSalePriceIsNegotiable', 'integer', 1, 
         array('type' => 'integer', 'length' => 1, 'default' => '0', 'notnull' => true));
        $this->hasColumn('Note', 'string', null, array('type' => 'string'));
        $this->hasColumn('OwnerCity', 'string', 255, 
         array('type' => 'string', 'length' => 255, 'notnull' => true));
        $this->hasColumn('OwnerCountryID', 'integer', 4, 
         array('type' => 'integer', 'length' => 4, 'notnull' => true));
        $this->hasColumn('DisplayStatus', 'integer', 1, 
         array('type' => 'integer', 'length' => 1, 'notnull' => true));
        $this->hasColumn('DisplayUntilDate', 'date', null, 
         array('type' => 'date'));
    }

}
?>

This BaseListing class is extended by the Listing class at /tmp/models/Listing.php, which currently looks like this:

<?php

/**
 * Listing
 * 
 * This class has been auto-generated by the Doctrine ORM Framework
 * 
 * @package    ##PACKAGE##
 * @subpackage ##SUBPACKAGE##
 * @author     ##NAME## <##EMAIL##>
 * @version    SVN: $Id: Builder.php 5441 2009-01-30 22:58:43Z jwage $
 */
class Listing extends BaseListing
{

}
?>

This child class, currently empty, is the appropriate place to add any custom methods or properties that you wish. Child classes are also the place to define relationships between models—a task that must be accomplished manually. To illustrate, update the empty Listing class above with the following code (Listing 9):

Listing 9. The extended Listing model
<?php
class Listing extends BaseListing
{
    public function setUp()
    {
        $this->hasOne('Manufacturer', array(
                'local' => 'VehicleManufacturerID',
                'foreign' => 'ManufacturerID'
            )
        );
        $this->hasOne('Country', array(
                'local' => 'OwnerCountryID',
                'foreign' => 'CountryID'
            )
        );
    }
} 
?>

The code in Listing 9 specifies that every Listing has one Manufacturer and one Country.

You should also specify the reverse relationship in the Manufacturer and Country models, as below (Listings 10 and 11):

Listing 10. The extended Country model
<?php
class Country extends BaseCountry
{
    public function setUp()
    {
        $this->hasMany('Listing', array(
                'local' => 'CountryID',
                'foreign' => 'OwnerCountryID'
            )
        );
    }
}
?>
Listing 11. The extended Manufacturer model
<?php
class Manufacturer extends BaseManufacturer
{
    public function setUp()
    {
        $this->hasMany('Listing', array(
                'local' => 'ManufacturerID',
                'foreign' => 'VehicleManufacturerID'
            )
        );
    }
}
?>

Find a link to the Doctrine manual, which explains the internals of Doctrine models and model relationships in greater detail, in Resources.

To add these generated models to your Agavi application, copy them to a directory named $WASP_ROOT/app/lib/doctrine under the application root:

shell> cd /usr/local/apache/htdocs/wasp/app/lib
shell> mkdir doctrine
shell> cp /tmp/models/* doctrine/
shell> cp /tmp/models/generated/* doctrine/

At the end of this step, the Doctrine libraries are installed to $WASP_ROOT/app/libs/doctrine, while the models are installed to $WASP_ROOT/app/lib/doctrine.

Step 4: Configure Agavi to work with Doctrine

The final step (steps, actually) relate to telling Agavi know about your Doctrine models, and configuring your application to use Agavi's Doctrine adapter for database queries. This involves a number of steps:

To auto-load the main Doctrine class, edit $WASP_ROOT/app/config/autoload.xml and add an entry for it, as in Listing 12:

Listing 12. Agavi configuration to auto-load Doctrine
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns="http://agavi.org/agavi/config/parts/autoload/1.0" 
 xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 parent="%core.system_config_dir%/autoload.xml">
  <ae:configuration>
    
    ...
    <autoload name="Doctrine">
     %core.app_dir%/../libs/doctrine/Doctrine.php
    </autoload>
    
  </ae:configuration>
</ae:configurations>

Edit the main application configuration file at $WASP_ROOT/app/config/settings.xml and enable database support in your Agavi application, as in Listing 13:

Listing 13. Agavi configuration to enable database support
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/settings/1.0">
  <ae:configuration>
    ...
    <settings>
      <setting name="app_name">WASP</setting>

      <setting name="available">true</setting>
      <setting name="debug">false</setting>

      <setting name="use_database">true</setting>
      <setting name="use_logging">false</setting>
      <setting name="use_security">true</setting>
      <setting name="use_translation">false</setting>
    </settings>

  </ae:configuration>

  ...
</ae:configurations>

Edit the application's database configuration file at $WASP_ROOT/app/config/databases.xml and configure Doctrine as the default database adapter. Set the DSN for the MySQL database, and also configure the path ($WASP_ROOT/app/lib/doctrine) to the Doctrine models installed in Step 3 (Listing 14). This ensures that Agavi will automatically load the models as needed.

Listing 14. Agavi configuration for the Doctrine DSN
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/databases/1.0">
  
  <ae:configuration>
    <databases default="doctrine">
      
      <database name="doctrine" class="AgaviDoctrineDatabase">
        <ae:parameter name="dsn">mysql://wasp:wasp@localhost/wasp</ae:parameter>
        <ae:parameter name="load_models">%core.lib_dir%/doctrine</ae:parameter>
      </database>
      
    </databases>
  </ae:configuration>
  
</ae:configurations>

Retrieving database records

Now that the foundations for communication between Agavi, Doctrine, and MySQL are in place, you can write a ViewAction that will retrieve and display individual listings from the MySQL database. First, manually populate the listing table with a couple of example records; this will make it easier to test your Action in its initial stages of development:

mysql> INSERT INTO listing (RecordID, RecordDate, OwnerName, OwnerTel, 
      OwnerEmail, VehicleManufacturerID, VehicleModel, VehicleYear, VehicleColor, 
      VehicleMileage, VehicleIsFirstOwned, VehicleAccessoryBit, VehicleIsCertified, 
      VehicleCertificationDate, VehicleSalePriceMin, VehicleSalePriceMax, 
      VehicleSalePriceIsNegotiable, Note, OwnerCity, OwnerCountryID, DisplayStatus, 
      DisplayUntilDate) VALUES  (1, '2009-06-08', 'John Doe', '00123456789876', 
      'john@wasp.example.com', 2, 'Boxster', 2005, 'Yellow', 15457, 1, 23, 1, 
      '2008-01-01', 35000, 40000, 1, 'Well cared for. In good shape, no scratches 
      or bumps. Has prepaid annual service contract till 2009.', 'London', 2, 
      1, '2009-10-15');
Query OK, 1 row affected (0.05 sec)

mysql> INSERT INTO listing (RecordID, RecordDate, OwnerName, OwnerTel, 
      OwnerEmail, VehicleManufacturerID, VehicleModel, VehicleYear, VehicleColor, 
      VehicleMileage, VehicleIsFirstOwned, VehicleAccessoryBit, VehicleIsCertified, 
      VehicleCertificationDate, VehicleSalePriceMin, VehicleSalePriceMax, 
      VehicleSalePriceIsNegotiable, Note, OwnerCity, OwnerCountryID, DisplayStatus, 
      DisplayUntilDate) VALUES (2, '2009-06-08', 'Jane Doe', '00987654321236', 
      'jane@wasp.example.com', 2, '911 Turbo', 2003, 'Black', 17890, 1, 23, 1, 
      '2008-06-19', 17000, 25000, 1, '', 'Cambridge', 2, 1, '2009-10-15');
Query OK, 1 row affected (0.00 sec)

Now, add the necessary functionality to WASP by following the usual process, as below:

Step 1: Create placeholder classes

Vehicle listings can rightly be considered a functionally-independent component of the WASP application and as such, place Actions and Views relating to this component in a separate module. Fire up your Agavi build script and create a new module, as below:

shell> agavi module-create
...
Module name: Listing

While you're at it, add a new DisplayAction to handle the display of individual listings. To link this Action with two Views, DisplayErrorView and DisplaySuccessView, supply the following values when prompted:

shell> agavi action-wizard
...
Module name: Listing
Action name: Display
Space-separated list of views to create for Display [Success]: Error Success

Agavi will now generate the necessary class files and place them in the correct locations.

Step 2: Define routes

Add a new route that references your newly-created Action, as in (Listing 15):

Listing 15. The Listing/DisplayAction route definition
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 xmlns="http://agavi.org/agavi/config/parts/routing/1.0">
  <ae:configuration>
    <routes>
      ...                  
      <!-- action for listing pages "/listing" -->
      <route name="listing" pattern="^/listing" module="Listing">
        <route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
      </route>
      
    </routes>
  </ae:configuration>
</ae:configurations>

This route definition expects an id variable to be included as part of the URL GET request, indicated by the use of a capture group in the route definition. This variable represents the unique identifier for the vehicle listing, corresponding to the listing.RecordID primary key field in the MySQL database. Remember that you'll need to add this variable to the DisplayAction's validator for it to get through Agavi's input validation filter.

This is also the first example you've seen of a nested route. In a nested route definition, the inner route inherits the pattern matched by the outer route, and can then further modify or add to this pattern. This feature is extremely convenient when implementing CRUD functionality, where URLs typically have a common base but different suffixes, as below:

/object/display/23
/object/add
/object/edit/23
/object/delete/23

Using the route definition above, URLs containing the pattern /listing will first be matched by the outer route. Agavi will then examine the rest of the pattern and, depending on what it contains, decide which of the child routes offers the best match and direct the request to that route's Action. Of course, the definition above only contains a single child route at the moment...but have a little patience, you'll add to it very soon.

Step 3: Define validation rules

Since only one input variable is passed to the DisplayAction, validation is pretty simple—all you need is an AgaviNumberValidator (Listing 16):

Listing 16. The Listing/DisplayAction validator
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Listing/config/validators.xml"
>
  <ae:configuration>
    
    <validators method="read">
      <validator class="number">
        <arguments>
          <argument>id</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="type">int</ae:parameter>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="min">1</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators>
        
  </ae:configuration>
</ae:configurations>

Step 4: Write Action code

With routing and validation handled, the next step is to specify the View for the DisplayAction. Since the DisplayAction will only handle GET requests, you must specify an executeRead() method with the name of the View to be generated. Listing 17 shows what the Action looks like:

Listing 17. The Listing/DisplayAction definition
<?php
class Listing_DisplayAction extends WASPListingBaseAction
{

  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function executeRead(AgaviRequestDataHolder $rd)
  {
    return 'Success';
  }
}
?>

Step 5: Write View code

Now you can get to the meat of this section: setting up the DisplaySuccessView to display individual vehicle listings. Listing 18 shows what the View looks like:

Listing 18. The Listing/DisplaySuccessView definition
<?php
class Listing_DisplaySuccessView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    $this->setAttribute('_title', 'View Listing');
    $id = $rd->getParameter('id');
    $q = Doctrine_Query::create()
          ->from('Listing l')
          ->leftJoin('l.Manufacturer m')
          ->leftJoin('l.Country c')
          ->where('l.RecordID = ?', $id);
      $result = $q->fetchArray();
      if (count($result) == 1) {
        $this->setAttribute('listing', $result[0]);
        return 'Success';
      } else {
        return $this->createForwardContainer(
         AgaviConfig::get('actions.error404_module'), 
         AgaviConfig::get('actions.error404_action'));
      }
  }
}
?>

The first few lines of the executeHtml() method set up the View template, and retrieve the value of the input variable $_GET['id']. This value is then interpolated into a Doctrine query that attempts to find a matching listing in the database (for details on the syntax of the Doctrine query, refer to Resources for a link to the corresponding manual page). If the query returns a single record as the result, this result is assigned to the template variable $t['listing'] as an associative array. If no matches, or multiple matches, are found, the View automatically forwards to the application's default Error404 action.

It's also a good idea at this time to set up the DisplayErrorView (Listing 19) to specify what behaviour should occur when input validation fails.

Listing 19. The Listing/DisplayErrorView definition
<?php

class Listing_DisplayErrorView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    return $this->createForwardContainer(
     AgaviConfig::get('actions.error404_module'), 
     AgaviConfig::get('actions.error404_action'));
  }
}

?>

Pretty simple—you just forward to the default Error404 action again.

Finally, set up the DisplaySuccess template to actually display the information retrieved from the database (Listing 20):

Listing 20. The Listing/DisplaySuccess template
<h3>FOR SALE: <?php printf('%d %s %s (%s)', 
 $t['listing']['VehicleYear'], $t['listing']['Manufacturer']['ManufacturerName'], 
 ucwords(strtolower($t['listing']['VehicleModel'])), 
 ucwords(strtolower($t['listing']['VehicleColor']))); ?></h3>

  <div id="container">    
    <div id="specs">
      <table cellspacing="5">
        <tr>
          <td class="key">Listing ID: </td>
          <td class="value"><?php echo $t['listing']['RecordID']; ?></td>
        </tr>
        <tr>
          <td class="key">Year of manufacture: </td>
          <td class="value"><?php echo 
           $t['listing']['VehicleYear']; ?></td>
        </tr>
        <tr>
          <td class="key">Color: </td>
          <td class="value"><?php echo 
            $t['listing']['VehicleColor']; ?></td>
        </tr>
        <tr>
          <td class="key">Mileage: </td>
          <td class="value"><?php echo 
           $t['listing']['VehicleMileage']; ?></td>
        </tr>  
        <tr>
          <td class="key">Ownership: </td>
          <td class="value"><?php echo 
           ($t['listing']['VehicleIsFirstOwned'] == 1) ? 'First owner' : 
           'Multiple owners'; ?></td>
        </tr>  
        <tr>
          <td class="key">Certification: </td>
          <td class="value"><?php echo 
           ($t['listing']['VehicleIsCertified'] == 1) ? 'Certified, as of ' 
           . date('d M Y', strtotime($t['listing']['VehicleCertificationDate'])) 
           : 'Not certified'; ?></td>
        </tr>
        <tr>
          <td class="key">Accessories: </td>
          <td class="value">
          <?php echo ($t['listing']['VehicleAccessoryBit'] == 0) ? 
           'None <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 1) ? 
           'Power steering <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 2) ? 
           'Power windows <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 4) ? 
           'Audio system <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 8) ? 
           'Video system <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 16) ? 
           'Keyless entry system <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 32) ? 
           'GPS <br/>' : null; ?>
          <?php echo ($t['listing']['VehicleAccessoryBit'] & 64) ? 
           'Alloy wheels <br/>' : null; ?>
          </td>
        </tr>  
        <tr>
          <td class="key">Location: </td>
          <td class="value"><?php echo 
           $t['listing']['OwnerCity']; ?>,   
          <?php echo $t['listing']['Country']['CountryName']; ?></td>  
        </tr>  
        <tr>
          <td class="key">Sale price: </td>
          <td class="value"> $<?php echo 
           $t['listing']['VehicleSalePriceMin']; ?> - $<?php echo 
           $t['listing']['VehicleSalePriceMax']; ?> <?php echo 
           ($t['listing']['VehicleSalePriceIsNegotiable'] == 1) ? '(negotiable)' 
           : null; ?></td>  
        </tr>  
        <tr>
          <td class="key">Description: </td>
          <td class="value"><?php echo 
           $t['listing']['Note']; ?></td>
        </tr>
      </table>
  </div>
</div>

This template reads the various elements of the database record as keys of the $t['listing'] associative array (remember the call to setAttribute() in the DisplaySuccessView?) and presents this information as a neatly-formatted HTML table. To see it in action, open your Web browser and try to access the two example records you added to the MySQL database earlier, by visiting either http://wasp.localhost/listing/display/1 or http://wasp.localhost/listing/display/2. You should see something like Figure 5.

Figure 5. A vehicle listing
Screen capture of a sample vehicle listing

Notice that if you try passing the URL an invalid or missing ID, Agavi will forward you to the default "Page not found" error page, as in Figure 6. That's your DisplayErrorView in action (pardon the pun!)

Figure 6. The error page generated on an invalid listing ID
Screen capture of the error page generated on an invalid listing ID

Conclusion

And that's about it for this second article. Over the last few pages, I took you deeper into the world of Agavi, explaining how to accept and validate user input submitted through Web forms and introducing you to Agavi's very cool form population filter. I also showed you how to enable database access in an Agavi application, by creating a MySQL database, using Doctrine ORM to generate models, and using these models to connect to, and execute queries, on the database.

Your example application is now a little smarter than before: it has a contact form, it knows how to send e-mail, and it can pull vehicle listings from a MySQL database. However, there isn't yet a proper interface for users to directly add their own listings to the database. This function, together with a few others, will be discussed in detail in the third part of this series.

See Download for all the code implemented in this article. I recommend you get it, start to play with it, and maybe try your hand at adding new things to it. I guarantee you won't break anything, and it will definitely add to your learning. Until next time...happy experimenting!


Download

DescriptionNameSize
Archive of the WASP app with functions to datewasp-02.zip3797KB

Resources

Learn

Get products and technologies

Discuss

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into XML on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=XML, Open source, Web development
ArticleID=416377
ArticleTitle=Introduction to MVC programming with Agavi, Part 2: Add forms and database support with Agavi and Doctrine
publish-date=10272009