Introduction to MVC programming with Agavi, Part 3: Add authentication and administrative functions with Agavi

Learn to build scalable Web applications with the Agavi framework

Continue to build the Web Automobile Sales Platform by adding the ability to add, delete, and update the automobile records in Part 3 of a five-part series. You will also see how to separate user functions from administrative functions with authentication.

Share:

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 11 August 2009)

Also available in Chinese Russian Japanese Vietnamese Portuguese

Introduction

Frequently used acronyms

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

The second part of this article series took you deeper into the world of Agavi, explaining how to deal with user input submitted through Web forms, and how to enable database access in your application with a little help from MySQL and Doctrine. It also expanded your knowledge of Agavi's MVC implementation by adding Models to the mix, and using these Models to read vehicle listings from the application database.

However, knowing how to read records from a database is only half the story. The other half involves writing new records, or modifying existing ones. And that's where this article comes in. Over the next few sections, I'll help you make the Web Automobile Sales Platform (WASP) example application even smarter, as you add the ability for users to create, edit and delete records through a Web interface. I'll also discuss the basics of Agavi's security framework, and show you how to restrict certain functions to authenticated users only. So come on in, and get started!


Adding database records

First up, Figure 1 has a quick reminder of what the WASP database looks like:

Figure 1. The WASP database
Diagram that lists attributes for the listing, manufacturer, and country database fields of the WASP database

The previous article (Part 2) in this series ended with the creation of a DisplayAction, which would read and display individual vehicle listings from the database. The listings themselves were created manually, using raw SQL at the MySQL command prompt. However, one of the business goals of the WASP application is to enable sellers themselves to add listings to the database, where they can be reviewed and decided upon by moderators. This business goal naturally leads to the following functional requirements:

  • An interface for sellers to upload vehicle listings;
  • An interface for WASP administrators to review, approve, or delete uploaded listings;
  • A security and access control model that distinguishes between these two classes of users.

To implement these requirements, first build a CreateAction that allows sellers to add new listings to the database through a Web form. Fire up your Agavi build script and initialize the Action and three Views in the Listing module, as below:

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

You should also add a new route for this CreateAction to the application routing table (Listing 1):

Listing 1. The Listing/CreateAction 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=".create" pattern="^/create$" action="Create" />
        <route name=".display" pattern="^/display/(id:\d+)$" action="Display" />
      </route>
            
    </routes>
  </ae:configuration>
</ae:configurations>

The default behavior of the CreateAction is to display a Web form whose fields correspond to the database fields shown in Figure 1. To do this, specify in the CreateAction that the CreateInputView displays by default on all GET requests by defining the getDefaultViewName() method (Listing 2):

Listing 2. The Listing/CreateAction definition
<?php
class Listing_CreateAction extends WASPListingBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }
}
?>

Update the corresponding CreateInput template file with the necessary Web form (Listing 3). You can find the CSS rules and JavaScript code for this form in the code archive that accompanies this article (see Download.

Listing 3. The Listing/CreateInput template
<script src="/js/form.js"></script>
<h3>Add Listing</h3>
<form action="<?php echo $ro->gen(null); ?>" method="post">
  <fieldset>
    <legend>Owner Information</legend>
  	<label for="OwnerName" class="required">Name:</label>
  	<input id="OwnerName" type="text" name="OwnerName" />
  	<p/>
  	<label for="OwnerTel">Telephone number:</label>
  	<input id="OwnerTel" type="text" name="OwnerTel" />
  	<p/>
  	<label for="OwnerEmail" class="required">Email address:</label>
  	<input id="OwnerEmail" type="text" name="OwnerEmail" />
  	<p/>
  	<label for="OwnerCity" class="required">City:</label>
  	<input id="OwnerCity" type="text" name="OwnerCity" />
  	<p/>
  	<label for="OwnerCountryID" class="required">Country:</label>
  	<select id="OwnerCountryID" name="OwnerCountryID">
  	<?php foreach ($t['countries'] as $c): ?>
  	<?php echo "<option value=\"$c[CountryID]\">" . $c['CountryName'] . 
  	 "</option>"; ?>
  	<?php endforeach; ?>
  	</select>
  	<p/>
	</fieldset>
	
  <fieldset>	
    <legend>Vehicle Information</legend>
  	<label for="VehicleManufacturerID" class="required">Manufacturer:</label>
  	<select id="VehicleManufacturerID" name="VehicleManufacturerID">
  	<?php foreach ($t['manufacturers'] as $m): ?>
  	<?php echo "<option value=\"$m[ManufacturerID]\">" . $m['ManufacturerName'] . 
  	 "</option>"; ?>
  	<?php endforeach; ?>
  	</select>
  	<p/>
  	
	  <label for="VehicleModel" class="required">Model:</label>
	  <input id="VehicleModel" type="text" name="VehicleModel" />
  	<p/>
  	
  	<label for="VehicleYear" class="required">Year of manufacture:</label>
  	<input id="VehicleYear" type="text" name="VehicleYear" size="4" 
  	 style="width:80px" />
  	<p/>
  	
  	<label for="VehicleColor" class="required">Color:</label>
  	<input id="VehicleColor" type="text" name="VehicleColor" />
  	<p/>
  	
  	<label for="VehicleMileage" class="required">Mileage:</label>
  	<input id="VehicleMileage" type="text" name="VehicleMileage" size="6" 
  	 style="width:100px" />
  	<p/>
  	
  	<label for="VehicleAccessoryBit" style="height:130px">Accessories:</label>
  	<input id="VehicleAccessoryBit_1" type="checkbox" name="VehicleAccessoryBit[]" 
  	 value="1" style="width:2px" />Power steering
  	<br/>
  	<input id="VehicleAccessoryBit_2" type="checkbox" name="VehicleAccessoryBit[]" 
  	 value="2" style="width:2px" />Power windows
  	<br/>
  	<input id="VehicleAccessoryBit_4" type="checkbox" name="VehicleAccessoryBit[]" 
  	 value="4" style="width:2px" />Audio system
  	<br/>
  	<input id="VehicleAccessoryBit_8" type="checkbox" name="VehicleAccessoryBit[]" 
  	 value="8" style="width:2px" />Video system
  	<br/>
  	<input id="VehicleAccessoryBit_16" type="checkbox" name="VehicleAccessoryBit[]" 
  	 value="16" style="width:2px" />Keyless entry system
  	<br/>
  	<input id="VehicleAccessoryBit_32" type="checkbox" name="VehicleAccessoryBit[]" 
  	 value="32" style="width:2px" />GPS
  	<br/>
  	<input id="VehicleAccessoryBit_64" type="checkbox" name="VehicleAccessoryBit[]" 
  	 value="64" style="width:2px" />Alloy wheels
  	<p/>
  	
  	<label for="data[VehicleIsFirstOwned]">Ownership:</label>
  	<input id="data[VehicleIsFirstOwned]" type="checkbox" 
  	 name="data[VehicleIsFirstOwned]" value="1" style="width:2px" />First owner
  	<p/>
  	
  	<label for="VehicleIsCertified">Certification:</label>
  	<input id="VehicleIsCertified" type="checkbox" name="VehicleIsCertified" 
  	 value="1" style="width:2px" 
  	  onClick="javascript:handleInputDisplayOnCheck('VehicleIsCertified', 
  	  'divVehicleCertificationDate')"/>Fully certified
  	<p/>
  	
  	<div id="divVehicleCertificationDate" style="display:none">
    	<label for="VehicleCertificationDate" class="required">
    	 Certificate issued in:</label>
    	<select id="VehicleCertificationDate_mm" name="VehicleCertificationDate_mm">
    	<?php for ($x=1; $x<=12; $x++): ?>
    	<?php echo "<option value=\"$x\">" . 
    	 date("F", mktime(null, null, null, $x, 1)) . "</option>"; ?>
    	<?php endfor; ?>
    	</select>
    	<select id="VehicleCertificationDate_yyyy" 
    	 name="VehicleCertificationDate_yyyy">
    	<?php for ($x=1990; $x<=date('Y'); $x++): ?>
    	<?php echo "<option value=\"$x\">$x</option>"; ?>
    	<?php endfor; ?>
    	</select>
    	<p/>
  	</div>
  	
  	<label for="VehicleSalePriceMin" class="required">Sale price (min):
  	 </label>
  	<input id="VehicleSalePriceMin" type="text" name="VehicleSalePriceMin" 
  	 size="6" style="width:100px" />
  	<p/>

  	<label for="VehicleSalePriceMax" class="required">Sale price (max):
  	 </label>
  	<input id="VehicleSalePriceMax" type="text" name="VehicleSalePriceMax" 
  	 size="6" style="width:100px" />
  	<p/>
  	
  	<label for="VehicleSalePriceIsNegotiable"> </label>
  	<input id="VehicleSalePriceIsNegotiable" type="checkbox" 
  	 name="VehicleSalePriceIsNegotiable" value="1" style="width:2px" />Negotiable
  	<p/>
  	
  	<label for="Note">Description:</label>
  	<textarea id="Note" name="Note" style="width:300px; height:200px"
  	 ></textarea>
  	<p/>
	</fieldset>
	
	<input type="submit" name="submit" class="submit" value="Submit Listing" />
</form>

<script>
handleInputDisplayOnCheck('VehicleIsCertified', 'divVehicleCertificationDate');
</script>

Notice that two of the fields in this form, Country and Manufacturer, are selection lists whose values come from the country and manufacturer tables respectively. To retrieve these values and make them available as template variables, edit the base view for the Listing module at $WASP_ROOT/app/modules/Listing/lib/view/WASPListingBaseView.class.php, and add the following setInputViewAttributes() method to it (Listing 4):

Listing 4. The Listing/WASPListingBaseView definition
<?php
/**
 * The base view from which all Listing module views inherit.
 */
class WASPListingBaseView extends WASPBaseView
{
  // set values from selection lists
  function setInputViewAttributes() {
    $q = Doctrine_Query::create()
          ->from('Country c');    
    $this->setAttribute('countries', $q->fetchArray());
    
    $q = Doctrine_Query::create()
          ->from('Manufacturer m');
    $this->setAttribute('manufacturers', $q->fetchArray());
  }  
}
?>

Adding the setInputViewAttributes() method to the WASPListingBaseView makes it available in all views that extend it, and reduces the need to copy this code into each individual view.

Then, edit the CreateInputView and invoke this method in its executeHtml() method (Listing 5):

Listing 5. The Listing/CreateInputView definition
<?php
class Listing_CreateInputView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    $this->setInputViewAttributes(); 
  }
}
?>

If you visit the URL http://wasp.localhost/listing/create in your Web browser, you should see a form similar to Figure 2.

Figure 2. A Web form to add new listings
Screen capture of a Web form to add new listings with fields to enter owner and vehicle information

When the user submits the form, the client browser sends the input data to the server in a POST transaction. Assuming the input passes validation, Agavi will invoke the CreateAction's executeWrite() method which has to process the input and save it to the database as a new record. Listing 6 illustrates the code that performs this task:

Listing 6. The Listing/CreateAction definition
<?php
class Listing_CreateAction extends WASPListingBaseAction
{
  ...
  public function executeWrite(AgaviRequestDataHolder $rd)
  {    
    try {
      // initialize object
      $listing = new Listing();
      
      // populate with validated input
      $listing->fromArray($rd->getParameters());      
      $listing->VehicleAccessoryBit = 
       array_sum($rd->getParameter('VehicleAccessoryBit'));

      // set some values manually
      $listing->RecordDate = date('Y-m-d', mktime());
      $listing->DisplayStatus = 0;
      
      // save record and get record ID
      $listing->save();
      $id = $listing->RecordID;      
      return 'Success';
    } catch (Exception $e) {    
      $this->setAttribute('error', $e->getMessage());  
      return 'Error';
    }
  }
}
?>

The method in Listing 6 initializes a new instance of the Listing model, and populates it using the input submitted through the Web form. Any necessary custom adjustments to the input data—for example, adding the bits that make up the listing.VehicleAccessoryBit bitmask, or setting the listing's display status to hidden—are performed at this point as well, and the record is then saved to the database by calling the model's save() method, which formulates and executes the necessary INSERT query.

Assuming the record is saved successfully, the CreateAction will render the CreateSuccessView. Listing 7 has the code for the CreateSuccess template:

Listing 7. The Listing/CreateSuccess template
<h3>Add Listing</h3>
Your submission has been accepted.
<p/>
A moderator will review it shortly and, if approved, it will be added 
to the public database within the next 48 hours.

In the event of validation errors, or if the record cannot be saved to the database for any reason, the CreateAction will render the CreateErrorView. Depending on the cause of the error, this CreateErrorView will either re-render the CreateInput template with the erroneous fields highlighted, or render the CreateError template and it the error message in a template variable.

Listing 8 has the code for the CreateErrorView:

Listing 8. The Listing/CreateErrorView definition
<?php
class Listing_CreateErrorView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);

    // if validation errors, render input template
    if ($this->container->getValidationManager()->getReport()->count()) {
      $this->setInputViewAttributes(); 
      $this->getLayer('content')->setTemplate('CreateInput');   
    }   
  }
}
?>

And Listing 9 has the code for the CreateError template:

Listing 9. The Listing/CreateError template
<h3>Add Listing</h3>
An error occurred while processing your submission. Please try again later.
<br/><br/>
<code style="color: red">
<?php echo $t['error']; ?>
</code>

As you follow along, you'll have noticed that the above approach to handling input validation errors is different from that shown to you earlier (refer to the ContactAction definition in "Using the form population filter" in Part 2 of this series). In the earlier approach, the Action's handleError() method was overridden to directly render the corresponding InputView; here, the Action's handleError() method is left untouched and it is the ErrorView which decides whether to render the Input template or the Error template.

This difference might seem subtle, but it is, in fact, an extremely important one. While the first method is convenient, it falls flat when working with different output types. For example, when working with HTML output, you might want to redisplay the input form; however, when working with SOAP output, you'd probably want to display an error message instead. The second method (which is the method recommended by Agavi core developers) allows this, because the decision of how to handle an error is left to the ErrorView rather than to the Action. And ErrorViews can handle multiple output types, while Actions cannot.

While on the subject, it's worth noting that although the examples in this series use try-catch() blocks to trap and handle exceptions, this isn't strictly necessary. Agavi will automatically handle any uncaught exceptions, as it captures them and returns a generic '500 Internal Server Error' page to the client. Catching and handling exceptions on a per-Action basis is a lot more work, but, on the other hand, it allows you much more control over the error page returned by the client. Which method should you use? It really depends on the requirements of your application, but once you select a method, stick to it, because you'll generate inconsistencies if you switch over mid-stream.

You'll also have noticed that in the preceding explanation, I skipped over one important component: the input validator. The reason for this is that, since the Web form in Listing 3 is significantly longer and more complex than the ones you've seen to date, the input validators that accompany it deserve a more in-depth discussion as well. And that comes next.


Using complex input validators

Look at the Web form in Listing 3, and you'll quickly realize that this form requires a number of different validators for its input. To validate string fields, such as the fields for Model and Description, use an AgaviStringValidator, as Listing 10 illustrates:

Listing 10. A string validation example
<validator class="string">
  <arguments>
    <argument>VehicleModel</argument>
  </arguments>
  <errors>
    <error>ERROR: Vehicle model is missing or invalid</error>
  </errors>
  <ae:parameters>
    <ae:parameter name="required">true</ae:parameter>
  </ae:parameters>
</validator>

<validator class="string">
  <arguments>
    <argument>Note</argument>
  </arguments>
  <ae:parameters>
    <ae:parameter name="required">false</ae:parameter>
  </ae:parameters>
</validator>

You can validate numeric fields using an AgaviNumberValidator, as Listing 11 illustrates:

Listing 11. A number validation example
<validator class="number">
  <arguments>
    <argument>VehicleMileage</argument>
  </arguments>
  <errors>
    <error>ERROR: Vehicle mileage is missing or invalid</error>
  </errors>
  <ae:parameters>
    <ae:parameter name="type">int</ae:parameter>
    <ae:parameter name="min">1</ae:parameter>
    <ae:parameter name="max">99999</ae:parameter>
    <ae:parameter name="required">true</ae:parameter>
  </ae:parameters>        
</validator>      

<validator class="number">
  <arguments>
    <argument>VehicleSalePriceMin</argument>
  </arguments>
  <errors>
    <error>ERROR: Vehicle sale price (min) is missing or invalid</error>
  </errors>
  <ae:parameters>
    <ae:parameter name="type">int</ae:parameter>
    <ae:parameter name="min">0</ae:parameter>
    <ae:parameter name="required">true</ae:parameter>
  </ae:parameters>
</validator>

The AgaviNumberValidator is also useful for input that needs to be restricted to a specific range. For example, MySQL's YEAR data type currently only allows values in the range 1901-2155, and so input intended for that field needs to comply with this restriction (Listing 12):

Listing 12. A year validation example
<validator class="number">
  <arguments>
    <argument>VehicleYear</argument>
  </arguments>
  <errors>
    <error for="required">ERROR: Vehicle year of manufacture is missing 
    </error>
    <error for="type">ERROR: Vehicle year of manufacture is invalid</error>
    <error for="min">ERROR: Vehicle year of manufacture is before 1900
    </error>
    <error for="max">ERROR: Vehicle year of manufacture is after 2020
    </error>
  </errors>
  <ae:parameters>
    <ae:parameter name="type">int</ae:parameter>
    <ae:parameter name="min">1901</ae:parameter>
    <ae:parameter name="max">2020</ae:parameter>
    <ae:parameter name="required">true</ae:parameter>
  </ae:parameters>
</validator>

To validate e-mail addresses, use an AgaviEmailValidator, as in Listing 13:

Listing 13. An e-mail address validation example
<validator class="email">
  <arguments>
    <argument>OwnerEmail</argument>
  </arguments>
  <errors>
    <error>ERROR: Email address is missing or invalid</error>
  </errors>
  <ae:parameters>
    <ae:parameter name="required">true</ae:parameter>
  </ae:parameters>
</validator>

Validate more complex strings, such as telephone numbers, using an AgaviRegexValidator (Listing 14):

Listing 14. A regular expression validation example
<validator class="regex">
  <arguments>
    <argument>OwnerTel</argument>
  </arguments>
  <errors>
    <error>ERROR: Number is missing or invalid</error>
  </errors>
  <ae:parameters>
    <ae:parameter name="required">false</ae:parameter>
    <ae:parameter name="pattern">#^[0-9]{6,25}$#</ae:parameter>
    <ae:parameter name="match">true</ae:parameter>
  </ae:parameters>
</validator>

Agavi also makes it possible to nest validators using AND or OR conditions. One useful application of this ability involves using the validator to automatically set default values for certain fields if they're not present in the POST submission. Checkbox fields are a good example of this: These fields are typically not included in the POST submission unless the user explicitly checks them. In these cases, an AgaviOrValidator can be used in combination with an AgaviSetValidator to assign these fields a default value and export them onwards to the Action.

Consider Listing 15, which illustrates by dynamically creating and setting the value of $_POST['VehicleIsFirstOwned'] to 0 if it doesn't already exist in the POST submission:

Listing 15. An example of setting default values
<validator class="or">
  <validator class="number">
    <arguments>
      <argument>VehicleIsFirstOwned</argument>
    </arguments>
    <errors>
      <error>ERROR: Vehicle ownership status is invalid</error>
    </errors>
    <ae:parameters>
      <ae:parameter name="required">false</ae:parameter>
      <ae:parameter name="min">0</ae:parameter>
      <ae:parameter name="max">1</ae:parameter>
    </ae:parameters>
  </validator>      

  <validator class="set">
    <ae:parameters>
      <ae:parameter name="export">VehicleIsFirstOwned</ae:parameter>
      <ae:parameter name="value">0</ae:parameter>
    </ae:parameters>
  </validator>      
</validator>

Agavi also comes with a very sophisticated validator for dates and times. The AgaviDateTimeValidator not only checks that a given date is valid; it also reformats the input value into a different date or time format. This is particularly useful when you deal with database DATETIME fields which expect date and time values in a specific format.

In Listing 16, the AgaviDateTimeValidator reads the vehicle certificate month and year from two separate form fields, verifies that they constitute a valid date, and then reformats them into YYYY-MM-DD format for insertion into a MySQL DATE field.

Listing 16. A date/time validation example
<validator class="datetime">
  <arguments>
    <argument name="AgaviDateDefinitions::MONTH">VehicleCertificationDate_mm
    </argument>
    <argument name="AgaviDateDefinitions::YEAR">VehicleCertificationDate_yyyy
    </argument>
  </arguments>
  <errors>
    <error>ERROR: Vehicle certification date is missing or invalid</error>
  </errors>
  <ae:parameters>
    <ae:parameter name="required">true</ae:parameter>
    <ae:parameter name="check">true</ae:parameter>
    <ae:parameter name="export">VehicleCertificationDate</ae:parameter>
    <ae:parameter name="cast_to">
      <ae:parameters>
          <ae:parameter name="type">date</ae:parameter>
          <ae:parameter name="format">yyyy-MM-dd</ae:parameter>
      </ae:parameters>
    </ae:parameter>
  </ae:parameters>
</validator>

Note that the AgaviDateTime validator requires the Agavi configuration variable use_translation to be set to true. You can set this variable in the $WASP_ROOT/app/config/settings.xml file.

For the complete set of CreateAction validators, refer to the code archive that accompanies this article (see Download).


Listing database records

Let's proceed to build the WASP administrative interface. To keep things simple, I'll assume that administrators only need to view listings, edit listings, and delete listings. These functions correspond to the AdminIndexAction, AdminEditAction, and AdminDeleteAction, all of which you'll build over the next few sections. Fire up the Agavi build script and add them as follows:

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

A small sidebar here: From the above, it's clear that I place the AdminIndexAction in the Listing module. I'll dog this with the other two administrative Actions discussed above as well. Why? Well, all these Actions are related to listing management; therefore, the Listing module seems a good home for them. The resulting file system layout under this perspective is:

app/modules/Listing/actions/DisplayAction.class.php
app/modules/Listing/actions/CreateAction.class.php
app/modules/Listing/actions/AdminIndexAction.class.php
app/modules/Listing/actions/AdminEditAction.class.php
app/modules/Listing/actions/AdminDeleteAction.class.php

However, as an alternative perspective— these three actions are intended for use by administrators only, so should be grouped into a separate module named, perhaps, Admin. Under this perspective, the resulting file system layout becomes:

app/modules/Listing/actions/DisplayAction.class.php
app/modules/Listing/actions/CreateAction.class.php
...
app/modules/Admin/actions/ListingIndexAction.class.php
app/modules/Admin/actions/ListingEditAction.class.php
app/modules/Admin/actions/ListingDeleteAction.class.php

Among users who migrate to Agavi from other frameworks, a key point of confusion involves figuring out which of the above two approaches is correct or best. But what's important to know, is that both classifications above (and a few others I haven't mentioned, but you're free to think of) are valid. Agavi doesn't lay down any special rules for the criteria to use when you organize your Actions into modules. Modules simply provide a convenient way to group related Actions together; they have no bearing on how those Actions are invoked and used. So, you're free to choose a classification that works for you and your application. Or, if you're not that organized, you can even dump all your Actions into the Default module and not worry about it at all!

Back to work now. Add routes for the three new Actions to Agavi's routing table (Listing 17):

Listing 17. The Listing/Admin* route definitions
<?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 admin listing pages "/admin/listing/*" -->
      <route name="admin.listing" pattern="^/admin/listing" module="Listing">
        <route name=".index" pattern="^/index$" action="AdminIndex" />
        <route name=".edit" pattern="^/edit/(id:\d+)$" action="AdminEdit" />
        <route name=".delete" pattern="^/delete$" action="AdminDelete" />
      </route>
                        
    </routes>
  </ae:configuration>
</ae:configurations>

The AdminIndexAction is the simplest of the bunch, because it only displays a list of all the records in the listing table and offers choices to edit and delete each one. Let's start with that one, by editing the Action's executeRead() method and popping in a Doctrine query to retrieve all listings in the database (Listing 18).

Listing 18. The Listing/AdminIndexAction definitions
<?php
class Listing_AdminIndexAction extends WASPListingBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function executeRead(AgaviRequestDataHolder $rd) 
  { 
    try {
      // get input variables
      $id = $rd->getParameter('id');

      // create query
      $q = Doctrine_Query::create()
            ->from('Listing l')
            ->leftJoin('l.Manufacturer m')
            ->leftJoin('l.Country c');
      $result = $q->fetchArray();
      
      // set view variables
      $this->setAttribute('records', $result);
      return 'Success';
    } catch (Exception $e) {
      $this->setAttribute('error', $e->getMessage());  
      return 'Error';
    }
  }
  
}
?>

Assuming no exceptions occur, the resulting recordset is transferred to the AdminIndexSuccess template through the template variable $t['records'] and displayed in a neat HTML table (Listing 19).

Listing 19. The Listing/AdminIndexSuccess template
<h3>View All Listings</h3>
<?php if (count($t['records']) == 0): ?>
No records available
<?php else: ?>

<div id="list">
  <form action="<?php echo $ro->gen('admin.listing.delete'); ?>" method="post" >
  <table cellspacing="5">
    <tr>
      <td class="key"></td>
      <td class="key"></td>
      <td class="key">Submission Date</td>
      <td class="key">Manufacturer</td>
      <td class="key">Model</td>
      <td class="key">Year</td>
      <td class="key">Mileage</td>
      <td class="key">Color</td>
      <td class="key"></td>
    </tr>  
    <?php foreach ($t['records'] as $record): ?>
    <tr>
      <td><input type="checkbox" name="id[]" 
       value="<?php echo $record['RecordID']; ?>" style="width:2px" />
      </td>
      <td><?php echo $record['RecordID']; ?></td>
      <td><?php echo date('d M Y', strtotime($record['RecordDate'])); ?>
      </td>
      <td><?php echo $record['Manufacturer']['ManufacturerName']; ?>
      </td>
      <td><?php echo $record['VehicleModel']; ?></td>
      <td><?php echo $record['VehicleYear']; ?></td>
      <td><?php echo $record['VehicleMileage']; ?></td>
      <td><?php echo $record['VehicleColor']; ?></td>
      <td><a href="<?php echo $ro->gen('admin.listing.edit', 
       array('id' => $record['RecordID'])); ?>">Edit</a></td>
    </tr>  
    <?php endforeach; ?>
    <tr>
      <td colspan="9"><input type="submit" name="submit" style="width:150px" 
       value="Delete Selected" /></td>
    </tr>    
  </table>  
  </form>
</div>
<?php endif; ?>

Notice also the use of the $ro->gen() method to dynamically generate routes for editing and deleting records, using the route names created earlier.

To see it in action, visit http://wasp.localhost/admin/listing/index in your Web browser to display a summary of the vehicle listings currently in the database. Figure 3 displays an example of what this view looks like:

Figure 3. A summary view of database records
Screen capture of a summary view of database records

No paging or sorting yet...but have patience, you'll get to that a little further along in this series!


Using a new master template

You'll notice from Figure 3 that the summary view generated by the AdminIndexAction has the same layout and appearance as the other public pages of the WASP application. This is not surprising, as all the views are using the same master template, located at $WASP_ROOT/app/templates/Master.php. However, customers often request a different look and feel for an application's administrative views, either for aesthetic purposes or to visually highlight to users that they've moved to a different section of the application.

It's not particularly difficult to do this with Agavi—all that's needed is to create a different master template, register it with Agavi, and then refer to this template in a view's setupHtml() method.

Step 1: Create a new master template

To begin, create a new master template at $WASP_ROOT/app/templates/AdminMaster.php, and fill it with the code in Listing 20:

Listing 20. The AdminMaster 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">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <base href="<?php echo $ro->getBaseHref(); ?>" />
    <link rel="stylesheet" type="text/css" href="/css/default.css" />
    <link rel="stylesheet" type="text/css" href="/css/admin.css" />
    <title><?php if(isset($t['_title'])) echo htmlspecialchars($t['_title']) . 
     ' - '; echo AgaviConfig::get('core.app_name'); ?></title>
  </head>
  <body>
    <!-- begin header -->
    <div id="header">
      <div id="logo">
        <img src="/images/logo-admin.jpg" />
      </div>
      <div id="menu">
      </div>
    </div>
    <!-- end header -->
    
    <!-- begin body -->
    <div id="body"> 
      <?php echo $inner; ?>
    </div>
    <!-- end body -->
    
    <!-- begin footer -->
    <div id="footer">
      <p>Powered by <a href="http://www.agavi.org/">Agavi</a>. 
      Licensed under <a href="http://www.creativecommons.org/">Creative Commons
      </a>.</p>
    </div>
    <!-- end footer -->
  </body>
</html>

While you're at it, also create a new CSS file with these additional rules (Listing 21) and save it to $WASP_ROOT/pub/css/admin.css:

Listing 21. The Listing/AdminMaster template stylesheet
#header {
  background: white;
  border-bottom: dashed 2px black;
}

#logo {
  padding-left: 10px;
}

#menu {
  background: white;
}

#menu a {
  color: black;
}

#footer {
  background: black;
}


#footer a {
  color: white;
}

#body form fieldset legend {
  color: black;
}

Step 2: Create and register a new layout

Next, tell Agavi about your new template. Register a new layout that uses it for the HTML output type in $WASP_ROOT/app/config/output_types.xml. Here's the addition you'll need to make:

<?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/output_types/1.0">
  
  <ae:configuration>    
    <output_types default="html">      
      <output_type name="html">
        
        ...   
        <layouts default="standard">
          <!-- standard layout with a content and a decorator layer -->
          <layout name="standard">
            <!-- content layer without further params. 
            this means the standard template is used, i.e. the one with the 
            same name as the current view -->
            <layer name="content" />
            <!-- decorator layer with the HTML skeleton, navigation etc; 
            set to a specific template here -->
            <layer name="decorator">
              <ae:parameter name="directory">%core.template_dir%</ae:parameter>
              <ae:parameter name="template">Master</ae:parameter>
            </layer>
          </layout>
          
          <layout name="admin">
            <layer name="content" />
            <layer name="decorator">
              <ae:parameter name="directory">%core.template_dir%</ae:parameter>
              <ae:parameter name="template">AdminMaster</ae:parameter>
            </layer>
          </layout>         
          ...

        </layouts>
        
        <ae:parameter name="http_headers">
          <ae:parameter name="Content-Type">text/html; charset=UTF-8
          </ae:parameter>
        </ae:parameter>
        
      </output_type>      
    </output_types>    
  </ae:configuration>
  ...
 
</ae:configurations>

Step 3: Use the new layout as needed

To use this new layout in a view, pass the layout name to the view's setupHtml() method as an additional argument. To illustrate, update the AdminIndexSuccessView so it looks like Listing 22:

Listing 22. The Listing/AdminIndexSuccessView definitions
<?php
class Listing_AdminIndexSuccessView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd, 'admin');
  }
}
?>

And now, when you revisit the listing summary page, you should see your new layout in action (Figure 4):

Figure 4. A revised summary view, with a new layout
Screen capture of a revised summary view, with a new layout

Deleting database records

You've already created the placeholder classes and routes, so all that's left is to get the AdminDeleteAction working. To begin, add a validator for the array of record IDs to pass to the AdminDeleteAction (Listing 23):

Listing 23. The Listing/AdminDeleteAction 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="write">
      <validator class="number">
        <arguments base="id[]">
          <argument />
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="min">1</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators>
    
  </ae:configuration>
</ae:configurations>

Then, update the AdminDeleteAction's executeWrite() method to read these record IDs and delete the corresponding records from the database (Listing 24):

Listing 24. The Listing/AdminDeleteAction definitions
<?php
class Listing_AdminDeleteAction extends WASPListingBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    try {
      // get record ids
      $ids = $rd->getParameter('id');
      
      foreach ($ids as $id) {
        // delete record from database
        $q = Doctrine_Query::create()
              ->delete('Listing')
              ->addWhere('RecordID = ?', $id);
        $result = $q->execute();        
      }
      
      return 'Success';     
    } catch (Exception $e) {
      $this->setAttribute('error', $e->getMessage());  
      return 'Error';
    }
  } 
}
?>

You'll also need to set up the AdminDeleteSuccess and AdminDeleteError templates, as in Listings 25 and 26:

Listing 25. The Listing/AdminDeleteSuccess template
<h3>Delete Listing</h3>
The selected record(s) were successfully deleted!
Listing 26. The Listing/AdminDeleteError template
<h3>Delete Listing</h3>
The selected record(s) could not be deleted. Please try again.
<br/><br/>
<code style="color: red">
<?php echo $t['error']; ?>
</code>

And you're done! View the summary page at http://wasp.localhost/admin/listing/index and see how it works. Select some records and click Delete Selected.


Editing database records

Editing an existing record is a slightly different operation than adding a new record. In the first instance, it's necessary to present the user with an input form whose fields are pre-filled with the contents of the existing record. Fortunately, Agavi's FormPopulationFilter comes to the rescue again, and makes this a fairly painless task.

To illustrate, open your AdminEditAction and update its executeRead() method to retrieve the selected record (using the validated record ID passed in the request URL), and then pass it on to the AdminEditView, as in Listing 27:

Listing 27. The Listing/AdminEditAction definition
<?php
class Listing_AdminEditAction extends WASPListingBaseAction
{
  public function getDefaultViewName()
  {
    return 'Input';
  }
  
  public function executeRead(AgaviRequestDataHolder $rd)
  {
    try {
      // get record ID
      $id = $rd->getParameter('id');
      
      // get record
      $q = Doctrine_Query::create()
            ->from('Listing l')
            ->leftJoin('l.Manufacturer m')
            ->leftJoin('l.Country c')
            ->where('l.RecordID = ?', $id);
      $result = $q->fetchArray();
      
      // if record exists, show input form
      // else generate 404 error page
      if (count($result) == 1) {
        $this->setAttribute('listing', $result[0]);
        return 'Input';
      } else {        
        return 'Redirect404';
      }
    } catch (Exception $e) {
      $this->setAttribute('error', $e->getMessage());  
      return 'Error';
    }
  } 
}
?>

Notice that the client is redirected to the AdminEditRedirect404View if no record matches the requested ID; this view merely forwards to Agavi's default Error404 view, which displays a "Page not found" error to the client.

Assuming that a record matching the requested ID is found, the AdminEditInputView will process the record and convert it into an associative array whose keys match the fields of the input form. A call to the FormPopulationFilter with this array as an argument is all that's needed to pre-fill each field with its corresponding value (Listing 28).

Listing 28. The Listing/AdminEditInputView definition
<?php
class Listing_AdminEditInputView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd, 'admin');

    $this->setInputViewAttributes();    
    
    // pre-populate form
    if ($this->getAttribute('listing')) {      
      $pre = new AgaviParameterHolder();
      
      $record = $this->getAttribute('listing');        
      foreach ($record as $k => $v) {
        $pre->setParameter("$k", $v);          
      }
      
      // special modification: VehicleAccessoryBit
      $allBits = array(1,2,4,8,16,32,64);    
      $selectedBits = array();
      foreach ($allBits as $bit) {
        ($record['VehicleAccessoryBit'] & $bit) ? $selectedBits[] = $bit : null; 
      }
      $pre->setParameter("VehicleAccessoryBit", $selectedBits);
         
      // special modification: VehicleCertificationDate
      $pre->setParameter("VehicleCertificationDate_mm", 
       date('n', strtotime($record['VehicleCertificationDate'])));
      $pre->setParameter("VehicleCertificationDate_yyyy", 
       date('Y', strtotime($record['VehicleCertificationDate'])));
      
      // special modification: DisplayUntilDate
      $pre->setParameter("DisplayUntilDate_dd", date('j', 
       strtotime($record['DisplayUntilDate'])));
      $pre->setParameter("DisplayUntilDate_mm", date('n', 
       strtotime($record['DisplayUntilDate'])));
      $pre->setParameter("DisplayUntilDate_yyyy", date('Y', 
       strtotime($record['DisplayUntilDate'])));
      
      // populate form
      $this->getContext()->getRequest()->setAttribute('populate', $pre, 
       'org.agavi.filter.FormPopulationFilter');
    }    
  }
}
?>

In case you wonder, the AdminEditInput template contains the same form that you saw in Listing 3, with the addition of the two special administrative fields for display status and display expiry date. Listing 29 has the additions:

Listing 29. The Listing/AdminEditInput template
<h3>Edit Listing</h3>
<form action="<?php echo $ro->gen(null); ?>" method="post">
  ...
<fieldset>  
    <legend>Listing Status</legend>
    <label for="DisplayStatus" class="required">Display status:</label>
    <select id="DisplayStatus" name="DisplayStatus" 
     onChange="javascript:handleInputDisplayOnSelect('DisplayStatus', 
     'divDisplayUntilDate', new Array('1'))">
      <option value="0">Hidden</option>
      <option value="1">Visible</option>
    </select>
    <p/>
    <div id="divDisplayUntilDate" style="display:none">
      <label for="DisplayUntilDate" class="required">Display until:</label>
      <select id="DisplayUntilDate_dd" name="DisplayUntilDate_dd">
        <?php for ($x=1; $x<=31; $x++): ?>
        <?php echo "<option value=\"$x\">$x</option>"; ?>
        <?php endfor; ?>
      </select>
      <select id="DisplayUntilDate_mm" name="DisplayUntilDate_mm">
        <?php for ($x=1; $x<=12; $x++): ?>
        <?php echo "<option value=\"$x\">" . 
         date("F", mktime(null, null, null, $x, 1)) . "</option>"; ?>
        <?php endfor; ?>
      </select>
      <select id="DisplayUntilDate_yyyy" name="DisplayUntilDate_yyyy">
        <?php for ($x=date('Y'); $x<=(date('Y')+5); $x++): ?>
        <?php echo "<option value=\"$x\">$x</option>"; ?>
        <?php endfor; ?>
      </select>
    </div>
  </fieldset
  ...
</form>

Listing 30 shows the corresponding validation rules:

Listing 30. The Listing/AdminEditAction 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>
    ...       
      <validator class="number">
        <arguments>
          <argument>DisplayStatus</argument>
        </arguments>
        <errors>
          <error>ERROR: Display status is missing or invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="min">0</ae:parameter>
          <ae:parameter name="max">1</ae:parameter>
        </ae:parameters>
      </validator>                      
      
      <validator class="datetime">
        <arguments>
          <argument name="AgaviDateDefinitions::DATE">DisplayUntilDate_dd
          </argument>
          <argument name="AgaviDateDefinitions::MONTH">DisplayUntilDate_mm
          </argument>
          <argument name="AgaviDateDefinitions::YEAR">DisplayUntilDate_yyyy
          </argument>
        </arguments>
        <errors>
          <error>ERROR: Display expiry date is missing or invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="check">true</ae:parameter>
          <ae:parameter name="export">DisplayUntilDate</ae:parameter>
          <ae:parameter name="cast_to">
            <ae:parameters>
                <ae:parameter name="type">date</ae:parameter>
                <ae:parameter name="format">yyyy-MM-dd</ae:parameter>
            </ae:parameters>
          </ae:parameter>
        </ae:parameters>
      </validator>                
            
    </validators>
    
  </ae:configuration>
</ae:configurations>

Figure 5 displays what the pre-filled input form looks like:

Figure 5. A Web form with pre-filled fields
Screen capture of a Web form with pre-filled fields

Once this form is submitted back to the AdminEditAction, the executeWrite() method creates and populates a Listing object with the submitted values, and then calling the save() method to formulate and execute the corresponding UPDATE query (Listing 31).

Listing 31. The Listing/AdminEditAction definition
<?php
class Listing_AdminEditAction extends WASPListingBaseAction
{
  ...
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    try {
      // initialize object
      $listing = new Listing();
      $listing->assignIdentifier($rd->getParameter('id'));
      
      // populate with validated input
      $listing->fromArray($rd->getParameters());      
      $listing->VehicleAccessoryBit = 
       array_sum($rd->getParameter('VehicleAccessoryBit'));
      
      // save updated record
      $listing->save();
      return 'Success';
    } catch (Exception $e) {
      $this->setAttribute('error', $e->getMessage());  
      return 'Error';     
    }
  }     
}
?>

Notice the call to assignIdentifier() in Listing 31. This is used to set the record's primary key and it serves as a signal to Doctrine that the record being processed already exists in the database. Therefore, Doctrine should use an UPDATE query instead of an INSERT query.

With this, WASP administrators have the ability to view, edit, and delete records. Or, to put it another way, your administration module is now fully functional! However, it doesn't have any security and, as a result, any WASP user can access it. You'll fix that next.


Adding user authentication

Agavi comes with a full-featured security framework that supports simple password-based authentication and more complex role-based access control (RBAC), and can also be easily extended to meet custom requirements. For your purposes, however, password-based authentication, which requires administrators to supply a valid password before they can access the administration module, should be enough to do the trick.

Access control is specified on a per-Action basis, through the Action's isSecure() method. If this method returns true, Agavi will check whether the current user is authenticated and, if not, will forward to the application's default login action (typically, Default/LoginAction). Then, the LoginAction obtains and verifies user credentials, and authenticates the user for subsequent requests.

The following steps discuss how you can secure the WASP administration module.

Step 1: Create the user database and model

Begin by creating a new MySQL table to hold administrator user names and passwords, as below:

mysql> CREATE TABLE IF NOT EXISTS `user` (
    -> RecordID int(11) NOT NULL AUTO_INCREMENT,
    -> Username varchar(25) CHARACTER SET utf8 NOT NULL,
    -> `Password` text CHARACTER SET utf8 NOT NULL,
    -> PRIMARY KEY (RecordID),
    -> UNIQUE KEY Username (Username)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
Query OK, 0 rows affected (0.13 sec)

Seed this table with a few accounts to get things rolling:

mysql> INSERT INTO user (Username, Password) VALUES('simon', PASSWORD('says'));
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO user (Username, Password) VALUES('marco', PASSWORD('polo'));
Query OK, 1 row affected (0.08 sec)

Then, use Doctrine to generate a User model for this table, using the process described in "Integrating Agavi with Doctrine" in Part 2 of this series. Add the resulting classes to your $WASP_ROOT/app/lib/doctrine/ directory:

shell> php doctrine-gen.php
shell> cd /usr/local/apache/htdocs/wasp/app/lib
shell> cp /tmp/models/User.php doctrine/
shell> cp /tmp/models/generated/BaseUser.php doctrine/

Step 2: Create placeholder classes

While every Agavi application includes a LoginAction by default, it doesn't include a LogoutAction. So, start your Agavi build script and add it:

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

You might also need to generate templates for the LoginInputView, LoginSuccessView, and LoginErrorView, as below:

shell> agavi template-create
Module name: Default
Template name: LoginSuccess

shell> agavi template-create
Module name: Default
Template name: LoginInput

shell> agavi template-create
Module name: Default
Template name: LoginError

Step 3: Define routes

Add routes for these Actions to the application's routing table (Listing 32):

Listing 32. The Default/LoginAction route definitions
<?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 admin login page "/admin/login" -->
      <route name="admin.login" pattern="^/admin/login$" 
       module="Default" action="Login" />
      
      <!-- action for admin logout pages "/admin/logout" -->
      <route name="admin.logout" pattern="^/admin/logout$" 
       module="Default" action="Logout" />
                                  
    </routes>
  </ae:configuration>
</ae:configurations>

Step 4: Write Action code

Add some code to the LoginAction that reads the credentials submitted by the user and checks them against the database (Listing 33). If valid, the Action uses the setAuthenticated() method to set an authentication flag and renders the LoginSuccess view; if not, it returns the LoginError view.

Listing 33. The Default/LoginAction definition
<?php
class Default_LoginAction extends WASPDefaultBaseAction
{

  public function getDefaultViewName()
  {
    return 'Input';
  }
  
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    try {
      // get input parameters
      $u = $rd->getParameter('username');
      $p = $rd->getParameter('password');
      
      // check user credentials
      $q = Doctrine_Query::create()
            ->from('User u')
            ->where('u.Username = ? AND u.Password = PASSWORD(?)', array($u,$p));
      $result = $q->fetchArray();
        
      // set authentication flag if valid
      if (count($result) == 1) {
        $this->getContext()->getUser()->setAuthenticated(true);
        return 'Success';
      } else {
        return 'Error';
      }
    } catch (Exception $e) {
        return 'Error';        
    }
  }
}
?>

The LogoutAction does the reverse: It unsets the user authentication flag and ends the user session. Listing 34 has the code:

Listing 34. The Default/LogoutAction definition
<?php
class Default_LogoutAction extends WASPDefaultBaseAction
{
  public function executeRead(AgaviRequestDataHolder $rd)
  {
    try {
      $this->getContext()->getUser()->setAuthenticated(false);
      return 'Success';
    } catch (Exception $e) {
      return 'Error';     
    }
  } 
}
?>

Step 5: Write View code

Among the various Views belonging to the LoginAction and LogoutAction, two in particular need attention: the LoginInputView and the LoginSuccessView.

The LoginInputView generates the login form that users will see when they try to access a restricted Action. It also stores the original request URL and redirects the user to that URL after successful login. According to the Agavi cookbook (look in Resources for a link), the easiest way to do this is to store the original request URL in the execution context, as shown in Listing 35:

Listing 35. The Default/LoginInputView definition
<?php
class Default_LoginInputView extends WASPDefaultBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    // get referrer URL and save it
    if($this->getContainer()->hasAttributeNamespace(
     'org.agavi.controller.forwards.login')) {
      $this->getContext()->getUser()->setAttribute('redirect', 
       $this->getContext()->getRequest()->getUrl(), 'org.agavi.WASP.login');
    } else {
      $this->getContext()->getUser()->removeAttribute('redirect', 
       'org.agavi.WASP.login');
    }   
    $this->setupHtml($rd, 'admin');
  }
}
?>

The LoginInput template generated by this view is pretty basic—a Web form with two fields. Listing 36 has the code, and Figure 6 demonstrates the form:

Listing 36. The Default/LoginInput template
<h3>Log In</h3>
<form action="<?php echo $ro->gen('admin.login'); ?>" method="post">
  <label for="username" class="required">Username:</label>
  <input id="username" type="text" name="username" style="width:150px" />
  <p/>
  <label for="password" class="required">Password:</label>
  <input id="password" type="password" name="password" style="width:150px" />
  <p/>
  <input type="submit" name="submit" class="submit" value="Log In" />
</form>
Figure 6. A login form
Screen capture of a login form

Listing 37 has the input validation rules for the above form:

Listing 37. The Default/LoginAction 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>username</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Username is missing</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
      
      <validator class="string">
        <arguments>
          <argument>password</argument>
        </arguments>
        <errors>
          <error for="required">ERROR: Password is missing</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
    </validators>
    
  </ae:configuration>
</ae:configurations>

Assuming a successful login, the LoginSuccessView retrieves the original URL request and redirects the client to it (Listing 38):

Listing 38. The Default/LoginSuccessView definition
<?php
class Default_LoginSuccessView extends WASPDefaultBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    // get original request URL
    // redirect 
    if($this->getContext()->getUser()->hasAttribute('redirect', 
     'org.agavi.WASP.login')) {
      $this->getResponse()->setRedirect($this->getContext()->
       getUser()->removeAttribute('redirect', 'org.agavi.WASP.login'));
      return true;
    }
    $this->setupHtml($rd, 'admin');
  }
}
?>

Step 6: Define the secure Actions

You're almost done—all that's left is to define which Actions require authentication. Open the AdminIndexAction, AdminDeleteAction, and AdminEditAction classes, and add an isSecure() method to each that returns true. Listing 39 has an example:

Listing 39. A secure Action
<?php
class Listing_AdminEditAction extends WASPListingBaseAction
{
  ...  
  final public function isSecure()
  {
    return true;
  }     
}
?>

And now, when you try to access any of the administrative routes—for example, the summary page at http://wasp.localhost/admin/listing/index—Agavi will first redirect you to a login form and only display the requested URL once you enter valid credentials. Try it for yourself and see!


Conclusion

That's about it for this third article. Over the last few sections, you significantly increased the weight of the WASP example application, by adding a full-fledged administration module and giving users a Web-based interface to add, edit, and delete vehicle listings in the database. That wasn't all—I also showed you how to define a separate layout for your administration module, and guided you through the basics of Agavi's user authentication and security model.

The Download section has all the code implemented in this article. I recommend that you get it, start to play with it, and maybe try to add 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-03.zip3,843KB

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=417778
ArticleTitle=Introduction to MVC programming with Agavi, Part 3: Add authentication and administrative functions with Agavi
publish-date=10272009