Introduction to MVC programming with Agavi, Part 4: Create an Agavi search engine with multiple output types including XML, RSS, or SOAP

Learn to build scalable Web applications with the Agavi framework

Implement a simple search engine and add support for multiple output types such as XML, RSS, or SOAP for your sample Agavi program in Part 4. This five-part series is for the PHP developer interested in Agavi, a open-source, flexible, and scalable framework.

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

Also available in Russian Japanese Vietnamese Portuguese

Introduction

In Part 3 of this article series, you walked through one of the most common tasks you encounter as you build a Web-based application: implementing an administration module that allows administrators to perform CRUD operations through a Web interface. You also explored Agavi's security model, and built a login system for user authentication that protects access to application resources.

Now continue your Agavi education and add even more functionality to the sample WASP (Web Automotive Sales Platform) application. You'll implement a search engine that allows users to directly search the database for listings that match specific criteria. That's not all—Agavi provides a sophisticated framework for developers to easily add support for multiple output types such as XML, RSS, or SOAP to their application. In this article, learn about that support as you return XML-encoded results from your search engine with minimal programming. Let's get started!


Handling search criteria

Frequently used acronyms

  • API: Application program interface
  • CRUD: Create Read Update Delete
  • CVS: Concurrent Versions System
  • DOM: Document Object Model
  • HTML: Hypertext Markup Language
  • HTTP: Hypertext Transfer Protocol
  • MVC: Model-View-Controller
  • PDF: Portable Document Format
  • RSS: Really Simple Syndication
  • URL: Uniform Resource Locator
  • XML: Extensible Markup Language

From the work done to date, you can clearly see that the WASP application can accept vehicle listings submitted by sellers and save these to the database for approval. The administration module developed in Part 3 of this series allows administrators to review these submitted listings and approve them for display on the Web site. Administrators can also define the length of time that each listing is visible on the site.

To make it easier for potential buyers to find vehicles matching their criteria, it makes sense to now add a search function to the application. This search interface will receive specific criteria from buyers, scan approved listings for a match to these criteria, and display the results for further examination.

To begin, use the Agavi build script to add a new SearchAction to the Listing module:

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

Also update the application's routing table to add a new route for this Action, as in Listing 1:

Listing 1. The Listing/SearchAction 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 name=".search" pattern="^/search$" action="Search" />
      </route>
      ...
    </routes>
  </ae:configuration>
</ae:configurations>

The default behavior of this SearchAction is to display a search form, which allows users to enter various search criteria. The typical way to do this might be to use a SearchInputView for the form, and a SearchSuccessView for the results. However, you already know this technique...and I'd hate to be boring! So I'll do something slightly different and combine both the form and its results into a single SearchSuccessView, which will also be the default View for the SearchAction (Listing 2):

Listing 2. The Listing/SearchAction definition
<?php
class Listing_SearchAction extends WASPListingBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
}
?>

Now for the search form itself. Listing 3 has the code for the SearchSuccessView. Notice the use of the AgaviFormPopulationFilter in this View, to ensure that the search form is automatically repopulated with the criteria entered by the user.

Listing 3. The Listing/SearchSuccessView definition
<?php
class Listing_SearchSuccessView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    $this->getContext()->getRequest()->setAttribute('populate', 
     array('fsearch' => true), 'org.agavi.filter.FormPopulationFilter');    
  }
}
?>

Listing 4 shows the corresponding SearchSuccess template:

Listing 4. The Listing/SearchSuccess template
<h3>Search Listings</h3>
<form id="fsearch" action="<?php echo $ro->gen('listing.search'); ?>" 
 method="get">
  <fieldset>
    <legend>Criteria</legend> 
    Color: 
    <input id="color" type="text" name="color" style="width:120px" />
    
    Year: 
    <input id="year" type="text" name="year" size="4" style="width:100px" />
    
    Price: 
    <input id="price" type="text" name="price" style="width:140px" />
    
    <button id="search" type="submit"/>
  </fieldset>    
</form>

<h3>Search Results</h3>

<?php if (count($t['records']) == 0): ?>
No records available
<?php else: ?>
  <?php $x = 1; ?>
  <?php foreach ($t['records'] as $record): ?>
  <div>
    <strong><?php echo $x; ?>. 
    <a href="<?php echo $ro->gen('listing.display', 
     array('id' => $record['RecordID'])); ?>"><?php printf('%d %s %s (%s)',
     $record['VehicleYear'], $record['Manufacturer']['ManufacturerName'], 
     ucwords(strtolower($record['VehicleModel'])), 
     ucwords(strtolower($record['VehicleColor']))); ?></a>
    </strong>
    <br/>
    Mileage: <?php echo $record['VehicleMileage']; ?>     
    <br/> 
    Sale price: $<?php echo $record['VehicleSalePriceMin']; ?> - 
    $<?php echo $record['VehicleSalePriceMax']; ?> 
    <?php echo ($record['VehicleSalePriceIsNegotiable'] == 1) ? 
     '(negotiable)' : null; ?>
    <br/> 
    Location: <?php echo $record['OwnerCity']; ?>, 
     <?php echo $record['Country']['CountryName']; ?>
    <br/> 
    Submitted: <?php echo date('d M Y', strtotime($record['RecordDate'])); ?>
    <p/>
  </div>
  <?php $x++; ?>
  <?php endforeach; ?>
<?php endif; ?>

<p/>
<strong>
  <a href="<?php echo $ro->gen('listing.create'); ?>">
  Add a new listing</a>
</strong>

In Listing 4, you see two sections to the template. The first section contains a search form that buyers can use to enter their purchase criteria. Once these values are submitted and processed, the second section will display the matches found in the database. There's also a link to add a new listing.

To keep things simple, I limited the form to three criteria: color, price, and year of manufacture. Needless to say, these input values need to be validated before they can make it through to the SearchAction. Listing 5 has the necessary validation rules:

Listing 5. The Listing/SearchAction 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="string">
        <arguments>
          <argument>color</argument>
        </arguments>
        <errors>
          <error>ERROR: Vehicle color is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="required">false</ae:parameter>
        </ae:parameters>
      </validator>              
                  
      <validator class="number">
        <arguments>
          <argument>year</argument>
        </arguments>
        <errors>
          <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">false</ae:parameter>
        </ae:parameters>
      </validator>             
            
      <validator class="number">
        <arguments>
          <argument>price</argument>
        </arguments>
        <errors>
          <error>ERROR: Vehicle price is invalid</error>
        </errors>
        <ae:parameters>
          <ae:parameter name="type">int</ae:parameter>
          <ae:parameter name="min">0</ae:parameter>
          <ae:parameter name="required">false</ae:parameter>
        </ae:parameters>
      </validator>                            
                  
    </validators>
    
  </ae:configuration>
</ae:configurations>

You'll notice from Listing 4 that, in a break with what you've seen to date, the search form uses the GET method instead of the POST method. In general, search forms should use GET as a request method, because they don't change any data on the server. In contrast, the forms in Parts 1, 2, and 3 modified or added to data on the server and so the POST method was more appropriate for those. Using GET as the request method also makes it possible to derive a URL that always points to the most recent search results—a fact I'll make use of shortly!


Handling search results

Since the search form submits input using the GET method, it's necessary to update the SearchAction in Listing 2 with an additional executeRead() method that reads this input and incorporates it into a search query. Construct this query iteratively, depending on the search parameters submitted by the user, and restrict it to only those approved listings that are valid for display on the current date. Listing 6 has the necessary code.

Listing 6. The revised Listing/SearchAction definition
<?php
class Listing_SearchAction extends WASPListingBaseAction
{
  public function getDefaultViewName()
  {
    return 'Success';
  }
  
  public function executeRead(AgaviRequestDataHolder $rd) 
  {   
    try {
      // create base query
      $q = Doctrine_Query::create()
            ->from('Listing l')
            ->leftJoin('l.Manufacturer m')
            ->leftJoin('l.Country c')
            ->addWhere('l.DisplayStatus = 1')
            ->addWhere('l.DisplayUntilDate >= CURDATE()');
            
      // add criteria
      if ($rd->getParameter('color')) {
        $q->addWhere('l.VehicleColor LIKE ?', sprintf('%%%s%%', 
         $rd->getParameter('color')));  
      }            
      
      if ($rd->getParameter('year')) {
        $q->addWhere('l.VehicleYear = ?', $rd->getParameter('year'));  
      }            
      
      if ($rd->getParameter('price')) {
        $q->addWhere('? BETWEEN l.VehicleSalePriceMin AND l.VehicleSalePriceMax', 
         $rd->getParameter('price'));  
      }            

      $q->orderBy('l.RecordDate DESC');
      
      // execute query and assign results to template variable
      $results = $q->fetchArray();
      $this->setAttribute('records', $results);
      return 'Success';
    } catch (Exception $e) {
      $this->setAttribute('error', $e->getMessage());  
      return 'Error';
    }
  }
}
?>

It's important to note that if invoked without any criteria, the query in Listing 6 will return all approved and valid listings, ordered by date.

Errors, whether related to validation or query execution, are handled by the SearchErrorView using the technique outlined in the previous article—namely, that the SearchErrorView decides whether to render the SearchSuccess template or the SearchError template. Listing 7 has the code for the SearchErrorView, and Listing 8 has the code for the corresponding SearchError template.

Listing 7. The Listing/SearchErrorView definition
<?php
class Listing_SearchErrorView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    // if validation errors, render input template
    if ($this->container->getValidationManager()->getReport()->count()) {
      $this->getLayer('content')->setTemplate('SearchSuccess');   
      $this->getContext()->getRequest()->setAttribute('populate', 
       array('fsearch' => true), 'org.agavi.filter.FormPopulationFilter');    
    }   
  }
}
?>
Listing 8. The Listing/SearchError template
<h3>Search Listings</h3>
There was an error processing your submission. Please try again later.
<br/><br/>
<code style="color: red">
<?php echo $t['error']; ?>
</code>

From Listing 7, it should be clear that the AgaviFormPopulationFilter is explicitly attached to the search form by naming the form's id attribute ('fsearch') in the call to $this->getContext()->getRequest()->setAttribute. This ensures that error messages generated by the validator are correctly displayed in the form. As you've not done this before, you probably wonder why it's necessary. According to the Agavi development team, this step is necessary in this particular case because the AgaviFormPopulationFilter typically matches the current URL with the form's action URL to figure out which form to run on. However, since the search form in this case submits data using the GET method, the current URL will usually contain a list of additional search parameters. Thus, the AgaviFormPopulationFilter is unable to match it with the form's action URL. To solve this problem when you call the AgaviFormPopulationFilter, explicitly specify the form's id attribute .

To see the search function in action, browse to http://wasp.localhost/listings/search and enter your criteria into the search form. Once you submit the form, the page refreshes with a list of results matching your criteria. Figure 1 has an example of what you might see.

Figure 1. The WASP search form with search results
Screen capture of the WASP search form with two search results

Submit invalid input, and the AgaviFormPopulationFilter will return an error message, as in Figure 2.

Figure 2. The WASP search form with invalid input
Screen capture of the WASP search form with invalid input and resulting error messages

At this point, the WASP application has a functional search engine. Just one minor item is left: Set a link in the application main menu for users to access the most recent vehicle listings. As explained earlier (Listing 6), if you invoke the SearchAction without any criteria produces a result set that contains all approved and valid listings, ordered by date. So, you only need to edit the master template file at $WASP_ROOT/app/templates/Master.php and use Agavi's route generator on the "For Sale" link in the main menu, as in Listing 9:

Listing 9. The revised 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">

    ...
    <!-- begin header -->
    <div id="header">
      <div id="logo">
        <img src="/images/logo.jpg" />
      </div>
      <div id="menu">
        <ul>
          <li><a href="<?php echo $ro->gen('index'); ?>">
          Home</a></li>
          <li><a href="<?php echo $ro->gen('listing.search'); ?>">
          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>
    </div>
    <!-- end header -->
    ...

</html>

Understanding output types

Modern applications often require the presentation of the same data in a number of different formats. For example, the same result set might be presented as an HTML table, a CSV file, an XML document, or an RSS feed. Agavi makes this simple, as it allows each View to support multiple output types. Thus, you can present the results of the same Action in different ways and not duplicate any Action code.

Every Agavi application comes pre-configured to use the HTML output type by default. Look inside the WASP application's output type configuration file, at $WASP_ROOT/app/config/output_types.xml, and you'll see what this configuration looks like (Listing 10):

Listing 10. Agavi output type configuration
<?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">
        
        <renderers default="php">
          <renderer name="php" class="AgaviPhpRenderer">
            <ae:parameter name="assigns">
              <ae:parameter name="routing">ro</ae:parameter>
              <ae:parameter name="request">rq</ae:parameter>
              <ae:parameter name="controller">ct</ae:parameter>
              <ae:parameter name="user">us</ae:parameter>
              <ae:parameter name="translation_manager">tm</ae:parameter>
              <ae:parameter name="request_data">rd</ae:parameter>
            </ae:parameter>
            <ae:parameter name="default_extension">.php</ae:parameter>
            <!-- this changes the name of the variable with all template attributes 
            from the default $template to $t -->
            <ae:parameter name="var_name">t</ae:parameter>
          </renderer>
        </renderers>
        
        <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>         
          
          <!-- another example layout that has an intermediate wrapper layer in 
          between content and decorator -->
          <!-- it also shows how to use slots etc -->
          <layout name="wrapped">
            <!-- 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" />
            <layer name="wrapper">
              <!-- use CurrentView.wrapper.php instead of CurrentView.php as 
              the template for this one -->
              <ae:parameter name="extension">.wrapper.php</ae:parameter>
            </layer>
            <!-- 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>
              <!-- an example for a slot -->
              <slot name="nav" module="Default" action="Widgets.Navigation" />
            </layer>
          </layout>
          
          <!-- special layout for slots that only has a content layer to prevent 
          the obvious infinite loop that would otherwise occur if the decorator layer 
          has slots assigned in the layout; this is loaded automatically by 
          ProjectBaseView::setupHtml() in case the current container is run as a slot -->
          <layout name="simple">
            <layer name="content" />
          </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:configuration environment="production.*">
    <output_types default="html">
      
      <!-- use a different exception template in production envs -->
      <!-- others are defined in settings.xml -->
      <output_type name="html" 
       exception_template="%core.template_dir%/exceptions/web-html.php" />
      
    </output_types>
  </ae:configuration>
</ae:configurations>

When you introduce a new output type in your Agavi application, follow three basic steps:

Step 1: Define the output type, together with any headers, renderers, and layouts

Like routes, output type configuration is expressed in XML. Every output type definition has a unique name. An output type definition can also optionally include the HTTP headers to be sent to the requesting client when that output type is requested, a renderer (Agavi includes Smarty, eZ, and TAL renderers, in addition to basic PHP), and one or more layouts.

Step 2: Link routes with the new output type

You can mark up every route definition in the application's routing table with an output_type attribute, which specifies the output type to be used for that particular route. Here's an example:

<route name="show" pattern="^/show$" module="Default" action="Show" output_type="xml" />

Alternatively, you can set up a catch-all route that automatically sets the output type based on the request URL. This means that URL requests that end in .rss are automatically set to use the RSS output type, URL requests that end in .pdf are automatically set to use the PDF output type, and so on. Listing 12 shows an example of this approach.

Step 3: Add support for the new output type in Views

To serve a request for a View, Agavi executes the View's executeXXX() method, where XXX refers to the output type requested. So, Agavi will run executeHtml() to serve a request for an HTML resource, executeRss() to serve a request for an RSS resource, executeXml() to serve a request for an XML resource, and so on. This is also why each View class that you create to date has an executeHtml() method.


Presenting search results as XML

To better understand how output types are handled in Agavi, add XML support to the WASP search engine. To begin, update the output type configuration file at $WASP_ROOT/app/config/output_types.xml and define a new XML output type (Listing 11):

Listing 11. An XML output type 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/output_types/1.0">
  
  <ae:configuration>    
    <output_types default="html">      
      <output_type name="html">
        ...
      </output_type>
      
      <output_type name="xml">
        <ae:parameter name="http_headers">
          <ae:parameter name="Content-Type">text/xml; charset=UTF-8
          </ae:parameter>
        </ae:parameter>
      </output_type>               
    </output_types>    
  </ae:configuration>
  
</ae:configurations>

Then, update the application's routing table and set up a catch-all route for the new XML output type (Listing 12):

Listing 12. The route definition for the XML output type
<?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>
      <!-- handler for .xml requests -->
      <route name="xml" pattern=".xml$" cut="true" stop="false" output_type="xml" />    
      ...  
    </routes>
  </ae:configuration>
</ae:configurations>

This route, placed at the top of the routing table, matches all requests ending with the .xml suffix and sets them up to use the XML output type. That's not all it does though:

  • The cut attribute indicates whether to delete the matched substring segment from the request URL before proceeding. In this case, it's set to true so that the .xml suffix is removed from the request URL once a match occurs.
  • The stop attribute in a route definition indicates whether or not to continue route processing after the first match. In this case, it's set to false to ensure that the request continues down the list until the rest of the request URL is matched and the appropriate Action invoked.

The net effect of this configuration is that when Agavi receives a request for, say, http://wasp.localhost/listings/display/1.xml, it checks the routing table and finds an immediate match with the top-level catch-all route. Agavi then strips the .xml suffix from the request URL and sets the output type for the request to XML. It then continues checking the remainder of the request http://wasp.localhost/listings/display/1 against the listed routes until it finds a match with the listing.display route and invokes the Listing/DisplayAction. Once the DisplayAction has completed, Agavi will look for an executeXml() method in the selected DisplayView, execute this method, and return its output to the client.

The next step is to update the SearchSuccessView and add an executeXml() method that reads the result set generated by the SearchAction and turns it into well-formed XML. Listing 13 has the code:

Listing 13. The Listing/SearchSuccessView definition
<?php
class Listing_SearchSuccessView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    $this->getContext()->getRequest()->setAttribute('populate', 
     array('fsearch' => true), 'org.agavi.filter.FormPopulationFilter');    
  }
  
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    // get records
    $records = $this->getAttribute('records');
    
    // create document
    $dom = new DOMDocument('1.0', 'utf-8');

    // create root element
    $root = $dom->createElementNS('http://www.melonfire.com/agavi-wasp', 
     'wasp:results');    
    $dom->appendChild($root);
    
    // import to SimpleXML for easier manipulation
    $xml = simplexml_import_dom($dom);
    
    // add result count
    $xml->addChild('count', count($records));
        
    // add results
    foreach ($records as $record) {
      $listing = $xml->addChild('result');      
      $listing->addChild('id', $record['RecordID']);
      $listing->addChild('submissionDate', $record['RecordDate']);
      $listing->addChild('manufacturer', 
       $record['Manufacturer']['ManufacturerName']);
      $listing->addChild('model', ucwords(strtolower($record['VehicleModel'])));
      $listing->addChild('year', $record['VehicleYear']);
      $listing->addChild('color', $record['VehicleColor']);
      $listing->addChild('year', $record['VehicleYear']);
      $listing->addChild('mileage', $record['VehicleMileage']);
      $price = $listing->addChild('price');
      $price->addChild('min', $record['VehicleSalePriceMin']);
      $price->addChild('max', $record['VehicleSalePriceMax']);
      $location = $listing->addChild('location');
      $location->addChild('city', $record['OwnerCity']);
      $location->addChild('country', $record['Country']['CountryName']);        
    }
    
    // return output
    return $xml->asXML();    
  }   
}
?>

The executeXml() method in Listing 13 uses the DOM extension in PHP to generate the skeleton of an XML document. It then converts this to a SimpleXML object and fills in the skeleton by iterating over the result set returned by the SearchAction and representing each record as an XML node collection. The final XML document is then returned to the client through a call to asXML() method in SimpleXML.

Listing 14 has an example of the output XML generated by Listing 13:

Listing 14. An XML-encoded result set
<?xml version="1.0" encoding="utf-8"?>
<wasp:results xmlns:wasp="http://www.melonfire.com/agavi-wasp">
  <wasp:count>2</wasp:count>
  <wasp:result>
    <wasp:id>1</wasp:id>
    <wasp:submissionDate>2009-07-03</wasp:submissionDate>
    <wasp:manufacturer>Porsche</wasp:manufacturer>
    <wasp:model>Boxster</wasp:model>
    <wasp:year>2005</wasp:year>
    <wasp:color>Yellow</wasp:color>
    <wasp:year>2005</wasp:year>
    <wasp:mileage>15457</wasp:mileage>
    <wasp:price>
      <wasp:min>35000</wasp:min>
      <wasp:max>40000</wasp:max>
    </wasp:price>
    <wasp:location>
      <wasp:city>London</wasp:city>
      <wasp:country>United Kingdom</wasp:country>
    </wasp:location>
  </wasp:result>
  <wasp:result>
    <wasp:id>7</wasp:id>
    <wasp:submissionDate>2009-06-07</wasp:submissionDate>
    <wasp:manufacturer>Ferrari</wasp:manufacturer>
    <wasp:model>612 Scaglietti</wasp:model>
    <wasp:year>2003</wasp:year>
    <wasp:color>Yellow</wasp:color>
    <wasp:year>2003</wasp:year>
    <wasp:mileage>10974</wasp:mileage>
    <wasp:price>
      <wasp:min>125000</wasp:min>
      <wasp:max>200000</wasp:max>
    </wasp:price>
    <wasp:location>
      <wasp:city>London</wasp:city>
      <wasp:country>United Kingdom</wasp:country>
    </wasp:location>
  </wasp:result>
</wasp:results>

In a similar vein, update the SearchErrorView to return an XML-encoded error message in case of an error in input validation or query execution. Since generating an XML-encoded error message is a common task that you'll perform more than once, it makes sense to add this code to the WASPBaseView class, as in Listing 15, so that it can be used in all child classes that extend this View:

Listing 15. 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());
  }  
  
  // generate XML-encoded error message
  function getErrorXml($message) {
    $dom = new DOMDocument('1.0');
    $root = $dom->createElementNS('http://www.melonfire.com/agavi-wasp', 
     'wasp:error');    
    $dom->appendChild($root);        
    $xml = simplexml_import_dom($dom);
    $xml->addChild('message', $message);      
    return $xml->asXML();       
  }    
}
?>

Through the magic of inheritance, you can now call this method in your SearchErrorView (Listing 16) to handle both validation and application errors for the XML output type:

Listing 16. The Listing/SearchErrorView definition
<?php
class Listing_SearchErrorView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
    // if validation errors, render input template
    if ($this->container->getValidationManager()->getReport()->count()) {
      $this->getLayer('content')->setTemplate('SearchSuccess');   
      $this->getContext()->getRequest()->setAttribute('populate', 
       array('fsearch' => true), 'org.agavi.filter.FormPopulationFilter');    
    }   
  }
  
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    if ($this->container->getValidationManager()->getReport()->count()) {
      return $this->getErrorXml('Validation error');
    } else {
      return $this->getErrorXml('Application error');      
    }
  }   
}
?>

To see this in action, browse to http://wasp.localhost/listing/search.xml, and view something similar to Figure 3:

Figure 3. Search results in XML
Browser view of search results in XML format

You can attach search criteria to the request URL and your XML output will be appropriately filtered, just as the HTML version was. For example, try browsing to http://wasp.localhost/listing/search.xml?color=yellow&year=2005, and you will see a result set filtered by those criteria. Figure 4 illustrates:

Figure 4. Search results in XML
Browser view of search results in XML format, filtered for the color yellow and the year 2005

Presenting individual records as XML

You can just as easily present an XML view of individual vehicle listings, instead of search results. You only need to update the various DisplayViews with executeXml() methods that return XML output.

Before you begin, Listing 17 has a quick reminder of what the DisplayAction looks like (note that it's been updated since Part 2 of this series to only display approved and valid listings).

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

  public function getDefaultViewName()
  {
    return 'Success';
  }

  public function executeRead(AgaviRequestDataHolder $rd)
  {
    try {
      $id = $rd->getParameter('id');
      $q = Doctrine_Query::create()
            ->from('Listing l')
            ->leftJoin('l.Manufacturer m')
            ->leftJoin('l.Country c')
            ->where('l.RecordID = ?', $id)
            ->addWhere('l.DisplayStatus = 1')
            ->addWhere('l.DisplayUntilDate >= CURDATE()')
            ->orderBy('l.RecordDate DESC');
      $result = $q->fetchArray();
      if (count($result) == 1) {
        $this->setAttribute('listing', $result[0]);
        return 'Success';
      } else {
        return 'Redirect404';
      }
    } catch (Exception $e) {
      $this->setAttribute('error', $e->getMessage());  
      return 'Error';
    }
  }  
}

?>

Listing 18 updates the DisplaySuccessView and adds an executeXml() method that dynamically constructs an XML document representing the vehicle listing:

Listing 18. The Listing/DisplaySuccessView definition
<?php

class Listing_DisplaySuccessView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
  }
  
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    // get record
    $record = $this->getAttribute('listing');
    
    // create document
    $dom = new DOMDocument('1.0', 'utf-8');

    // create root element
    $root = $dom->createElementNS('http://www.melonfire.com/agavi-wasp', 
     'wasp:result');    
    $dom->appendChild($root);
    
    // import to SimpleXML for easier manipulation
    $xml = simplexml_import_dom($dom);
    
    // add nodes to XML output
    $xml->addChild('id', $record['RecordID']);
    $xml->addChild('submissionDate', $record['RecordDate']);
    $xml->addChild('manufacturer', $record['Manufacturer']['ManufacturerName']);
    $xml->addChild('model', ucwords(strtolower($record['VehicleModel'])));
    $xml->addChild('year', $record['VehicleYear']);
    $xml->addChild('color', strtolower($record['VehicleColor']));
    $xml->addChild('mileage', $record['VehicleMileage']);
    $xml->addChild('singleOwner', $record['VehicleIsFirstOwned']);
    $xml->addChild('certified', $record['VehicleIsCertified']);
    $xml->addChild('certifiedDate', $record['VehicleCertificationDate']);
    $xml->addChild('note', $record['Note']);
    
    $accessoryArr = array(
      '1'  => 'Power steering', 
      '2'  => 'Power windows', 
      '4'  => 'Audio system', 
      '8'  => 'Video system', 
      '16' => 'Keyless entry system',
      '32' => 'GPS',
      '64' => 'Alloy wheels'
    );
    $accessories = $xml->addChild('accessories');
    foreach ($accessoryArr as $k => $v) {
      if ($record['VehicleAccessoryBit'] & $k) {
        $accessories->addChild('item', $v);
      }
    }
    
    $price = $xml->addChild('price');
    $price->addChild('min', $record['VehicleSalePriceMin']);
    $price->addChild('max', $record['VehicleSalePriceMax']);
    $price->addChild('negotiable', $record['VehicleSalePriceIsNegotiable']);
    
    $location = $xml->addChild('location');
    $location->addChild('city', $record['OwnerCity']);
    $location->addChild('country', $record['Country']['CountryName']);        
    
    // return output
    return $xml->asXML();       
  }    
}
?>

Listing 19 illustrates an example of the resulting output:

Listing 19. An XML-encoded record
<?xml version="1.0" encoding="utf-8"?>
<wasp:result xmlns:wasp="http://www.melonfire.com/agavi-wasp">
  <wasp:id>1</wasp:id>
  <wasp:submissionDate>2009-07-03</wasp:submissionDate>
  <wasp:manufacturer>Porsche</wasp:manufacturer>
  <wasp:model>Boxster</wasp:model>
  <wasp:year>2005</wasp:year>
  <wasp:color>yellow</wasp:color>
  <wasp:mileage>15457</wasp:mileage>
  <wasp:singleOwner>1</wasp:singleOwner>
  <wasp:certified>1</wasp:certified>
  <wasp:certifiedDate>2008-01-01</wasp:certifiedDate>
  <wasp:note></wasp:note>
  <wasp:accessories>
    <wasp:item>Power steering</wasp:item>
    <wasp:item>Power windows</wasp:item>
    <wasp:item>Audio system</wasp:item>
    <wasp:item>Keyless entry system</wasp:item>
  </wasp:accessories>
  <wasp:price>
    <wasp:min>35000</wasp:min>
    <wasp:max>40000</wasp:max>
    <wasp:negotiable>1</wasp:negotiable>
  </wasp:price>
  <wasp:location>
    <wasp:city>London</wasp:city>
    <wasp:country>United Kingdom</wasp:country>
  </wasp:location>
</wasp:result>

Listing 20 has the revised DisplayErrorView:

Listing 20. The Listing/DisplayErrorView definition
<?php
class Listing_DisplayErrorView extends WASPListingBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $this->setupHtml($rd);
  }
  
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    if ($this->container->getValidationManager()->getReport()->count()) {
      return $this->getErrorXml('Validation error');
    } else {
      return $this->getErrorXml('Application error');      
    }
  }    
}
?>

To see this in action, browse to http://wasp.localhost/listing/display/1.xml, and you should see something like Figure 5:

Figure 5. An individual listing in XML
Browser view of an individual listing in XML format

Notice that throughout the last two examples, the code in the Action remained untouched. By allowing developers to decide how to handle different output types in the View, rather than in the Action, Agavi minimizes code duplication while still adhering to both MVC principles and the DRY (Don't Repeat Yourself) axiom.


Conclusion

At this point, the WASP example application is fully functional, with all main menu links active. Sellers can upload vehicle details and buyers can search for vehicles that match their criteria. With a usable administration module, administrators can edit, modify, and approve listings, and developers can use an XML interface to construct their own applications. All that's left are few more additions to make things a little more usable, and I'll cover those in the next (and final) article in this series.

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-04.zip3,852KB

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=422324
ArticleTitle=Introduction to MVC programming with Agavi, Part 4: Create an Agavi search engine with multiple output types including XML, RSS, or SOAP
publish-date=10272009