Creating a REST API with Agavi

Implement a REST API with the Agavi PHP framework

Agavi is an open-source, flexible, and scalable framework for application development. One of its key features is built-in support for REST routes, making it possible to quickly add a REST API for third-party development to an existing or new Web application. In this article, examine this feature in detail, and how to build a REST API with support for both XML and JSON formats.

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.



16 March 2010

Also available in Chinese Russian Japanese Portuguese

Introduction

These days, every Web application worth its salt has a REST API. Flickr has one; so do Google, Bit.ly, and NetFlix, as well as many other popular applications. As an architectural pattern, REST is popular because it intuitively maps existing HTTP verbs to common data operations, and because it offers a lightweight alternative to existing SOAP- and RPC-based architectures, with fewer data typing and conformance requirements. This has positive time and cost implications: REST-based APIs are typically faster to implement, and easier to develop against, than their SOAP- and RPC-based equivalents.

Frequently used acronyms

  • API: Application program interface
  • CRUD: Create Read Update Delete
  • DOM: Document Object Model
  • HTML: Hypertext Markup Language
  • HTTP: Hypertext Transfer Protocol
  • JSON: JavaScript Object Notation
  • MVC: Model-View-Controller
  • OOP: Object-oriented programming
  • ORM: Object-Relational Mapping
  • REST: Representational State Transfer
  • RPC: Remote procedure call
  • SQL: Structured Query Language
  • URL: Uniform Resource Locator
  • WSDL: Web Services Description Language
  • XML: Extensible Markup Language

In a previous set of articles, I introduced you to the Agavi MVC framework, illustrating you can use it to quickly and efficiently build a scalable Web application (see Resources for a link). Agavi has a number of good things going for it:

  • Sophisticated input filtering and validation
  • OOP-compliant architecture
  • Customized URL routing
  • Extensible role-based access control

It also offers two features that are invaluable for the REST API developer: built-in support for REST HTTP methods, and support for multiple output types such as XML and JSON.

This article will walk you through the process of building a simple REST API with Agavi. If you already have an Agavi-based application, this article will explain how to leverage existing framework conventions and expose your application innards to third-party developers. If you're creating a new REST-based application, this article will demonstrate how using Agavi can make the process simpler and more efficient.


Understanding REST

First up, a few words about REST, otherwise known as Representational State Transfer. REST differs from SOAP in that it is based on resources and actions, rather than on methods and data types. A resource is simply a URL referencing the object or entity on which the actions are to be performed—or example, /users or /photos—and an action is one of the four HTTP verbs—GET (retrieve), POST (create), PUT (update), and DELETE (remove).

To better understand this, consider a simple example. Suppose, you have a photo-sharing application and you need API methods for developers to remotely add new photos to, or retrieve existing photos from, the application data store. Under the SOAP approach, you typically have SOAP API methods like createPhoto() and getPhoto(), which receive XML-encoded requests containing photo parameters as input, take care of creating or retrieving photo records, and return XML-encoded responses indicating success or failure. The SOAP WSDL defines the request and response packet format, the data types of the various input parameters, and the range of possible response values.

The REST approach is significantly simpler. Under this approach, you expose a URL endpoint, such as /photos, and examine the HTTP method used to access this URL to understand the action required. So, for example, you POST an HTTP packet to /photos to create a new photo, or send a request to GET /photos to retrieve a list of available photos. This approach is much easier to understand, as it maps existing HTTP verbs to CRUD operations, and it's also lighter in terms of resource consumption, since there's no formal definition of data types of request/response headers needed.

The typical REST conventions for URL requests, and what they mean, are as follows:

  • GET /items: Retrieve a list of items
  • GET /items/123: Retrieve item #123
  • POST /items: Create a new item
  • PUT /items/123: Update item #123
  • DELETE /items/123: Remove item #123

Agavi comes with built-in support for these REST conventions. If you followed along with earlier articles in this series, you've already seen that the framework will automatically map GET and POST requests to an action's executeRead() and executeWrite() methods. In a similar vein, PUT and DELETE requests will also automatically map to an action's executeCreate() and executeRemove() methods. Defining a new REST API therefore becomes as simple as defining these action methods, filling them with code, and routing requests to them correctly. And that's exactly what you'll do in the remainder of this article.


Setting up the example application

Before you start to implement a REST API, a few notes. Throughout this article, I'll assume that you have a working (Apache, PHP, and MySQL) development environment, and that you're familiar with the basics of SQL and XML. I'll also assume that you're conversant with the basic principles of application development with Agavi, understand the interaction between actions, views, models and routes, and are familiar with using Doctrine models in an Agavi application. Finally, I'll assume that your Apache Web server is configured to support virtual hosting, URL rewriting, and PUT and DELETE requests. In case you're not familiar with these topics, you should read the introductory Agavi article series (see Resources for a link) before proceeding with the material in this article.

The example application in this case is a simple database of book titles and authors. The REST API will allow third-party developers to retrieve, add, delete, and update books in this database using normal REST conventions. The majority of this article will assume XML request and response bodies; however, there's also a section at the end that covers how to handle JSON requests and responses.

Step 1: Initialize a new application

To begin, first set up a simple Agavi application that will serve as a testbed for the development goals of this article. Use the Agavi build script to initialize a new project, accepting default values except where shown below:

shell> agavi project-wizard
Project name [New Agavi Project]: ExampleApp
Project prefix (used, for example, in the project base action) [Exampleapp]: ExampleApp
Should an Apache .htaccess file with rewrite rules be generated (y/n) [n]? y
...

While you're at it, define a new virtual host for your test application, such as http://example.localhost/, in your Apache configuration, and then point your browser to it. You should see the default Agavi welcome page, as in Figure 1.

Figure 1. The default Agavi welcome page
Screen capture of the default Agavi welcome page

Step 2: Add a new module and corresponding actions

For simplicity, I'll assume that all the actions you wish to protect are located in a module other than the Default module. Drop back to your command prompt and create a new Books module using the Agavi build script, as below:

shell> agavi module-wizard
Module name: Books
Space-separated list of actions to create for Books: Index Book
Space-separated list of views to create for Index [Success]: Success
Space-separated list of views to create for Book [Success]: Success Error
...

These are the actions to which you shortly will add REST methods.

At this point, you should also remove the Welcome module, as recommended by the Agavi documentation.

shell> rm -rf app/modules/Welcome
shell> rm -rf app/pub/welcome

Step 3: Update the application routing table

Next, update the application's routing table, at $ROOT/app/config/routing.xml, with routes corresponding to the standard REST routes discussed in the preceding section. Listing 1 has the necessary route definitions.

Listing 1. The REST 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>

      <!-- default action for "/" -->
      <route name="index" pattern="^/$" module="Default" action="Index" />

      <!-- REST-style routes -->
      <route name="books" pattern="^/books" module="Books">
        <route name=".index" pattern="^/$" action="Index" />
        <route name=".book" pattern="^/(id:\d+)$" action="Book" />
      </route>      

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

You should now be able to access the newly-minted actions using the routes in Listing 1. To verify this, try browsing to http://example.localhost/books/ and confirm that you are presented with a stub view similar to that in Figure 2.

Figure 2. An Agavi stub HTML view
Screen capture of an Agavi stub HTML view with title of Index

Step 4: Initialize the book database and model

The next step is to initialize the application database. So, create a new MySQL table to hold book records, as below:

mysql> CREATE TABLE IF NOT EXISTS book (
    ->   id int(11) NOT NULL AUTO_INCREMENT,
    ->   title varchar(255) NOT NULL,
    ->   author varchar(255) NOT NULL,
    ->   PRIMARY KEY (`id`)
    -> ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.13 sec)

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

mysql> INSERT INTO `book` (`id`, `title`, `author`) 
    -> VALUES (1, 'Wolf Hall', 'Hilary Mantel');
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO `book` (`id`, `title`, `author`) 
    -> VALUES (2, 'Prayers for Rain', 'Dennis Lehane');
Query OK, 1 row affected (0.08 sec)

Then, download the Doctrine object relational mapper (see Resources for a link) and add the Doctrine libraries to $ROOT/libs/doctrine. You must also update your application settings, in $ROOT/app/config/settings.xml, to activate database support, and then update your database configuration file, typically found at $ROOT/app/config/databases.xml, to use Agavi's Doctrine adapter. Listing 2 has an example of what this configuration might look like:

Listing 2. Doctrine ORM 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/databases/1.0">
  <ae:configuration>
    <databases default="doctrine">    
      <database name="doctrine" class="AgaviDoctrineDatabase">
        <ae:parameter name="dsn">mysql://user:pass@localhost/example
        </ae:parameter>
        <ae:parameter name="load_models">%core.lib_dir%/model
        </ae:parameter>
      </database>      
    </databases>
  </ae:configuration>
</ae:configurations>

At this point, you can use Doctrine to generate models for these tables. Remember to manually copy the resulting model classes to your $ROOT/app/lib/model/ directory.

shell> cp /tmp/models/Book.php app/lib/model/
shell> cp /tmp/models/generated/BaseBook.php app/lib/model/

The process of integrating Doctrine with Agavi and using it to generate models from database tables is discussed in detail in Part 3 of the introductory Agavi series (see the Resources of this article for a link).

Step 5: Define the XML output type

By default, Agavi is only configured for HTML output. Since this example REST API will initially support XML, it's necessary to define this output type, specify the relevant response headers and mark it as default. To do this, update the output type configuration file, at $ROOT/app/config/output_types.xml, with the code in Listing 3.

Listing 3. XML 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="xml">

      <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>

In case you're not able to get things working as above, remember that the steps above are described in more detail in Part 1 of the introductory Agavi series (see the Resources of this article for a link). Alternatively, you can download a complete code archive of the example application from Download.


Handling GET requests

The typical REST API must support two types of GET requests, the first for a list of resources (GET /books/) and the second for a specific resource (GET /books/123). Using the routing table discussed in the previous section, Agavi will automatically route these to the Books_IndexAction::executeRead() and Books_BookAction::executeRead() methods respectively.

The IndexAction's executeRead() method should respond to a GET /books/ request with a 200 (OK) status code and a list of all available book records. Listing 4 illustrates what the code looks like:

Listing 4. The IndexAction handler for GET requests
<?php
class Books_IndexAction extends ExampleAppBooksBaseAction
{ 
  public function executeRead(AgaviRequestDataHolder $rd)
  {
    $q = Doctrine_Query::create()
          ->from('Book b');
    $result = $q->execute(array(), Doctrine::HYDRATE_ARRAY); 
    $this->setAttribute('result', $result);
    return 'Success';
  }
}
?>

Listing 4 uses Doctrine to perform a query for the records in the books database table, setting the result as a view variable for the IndexSuccessView. The next step is to add an executeXml() method to the IndexSuccessView, to output this information as an XML document. Listing 5 has the code:

Listing 5. The IndexSuccess XML view
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{ 
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');    
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']); 
    }
    return $xml->asXml();
  }
}
?>

There is nothing very complicated here. The executeXml() method generates a new DOM document, creates the root element and then uses SimpleXML to build the rest of the XML tree, populating it with the information from the Doctrine result set.

To see this in action, use a Web browser to request the URL http://example.localhost/books. You should end up with something like Figure 3. (View a text-only version of Figure 3.)

Figure 3. The XML response to a GET request for all books
Screen capture of the XML response to a GET request for all books

In a similar vein, the BookAction's executeRead() method should respond to a GET /books/{id} request with an XML document containing the details of the requested book. Listing 6 illustrates what the code looks like:

Listing 6. The BookAction handler for GET requests
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
  public function executeRead(AgaviRequestDataHolder $rd)
  {
    $q = Doctrine_Query::create()
          ->from('Book b')
          ->addWhere('id = ?', $rd->getParameter('id'));
    $result = $q->execute(array(), Doctrine::HYDRATE_ARRAY); 
    if (count($result) == 0) {
      return 'Error';  
    }
    $this->setAttribute('result', $result);
    return 'Success';
  }
}
?>

Listing 7 has the corresponding validator definition, and Listing 8 has the corresponding BookSuccess view.

Listing 7. The BookAction 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%/Books/config/validators.xml"
>
  <ae:configuration>    
    <validators>
      <validator class="number">
        <arguments>
          <argument>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>
Listing 8. The BookSuccess XML view
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');    
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']);      
    }
    return $xml->asXml();
  }   
}
?>

To see this in action, use a Web browser to request the URL http://example.localhost/books/1. You should end up with something like Figure 4. (View a text-only version of Figure 4.)

Figure 4. The XML response to a GET request for an individual book
Screen capture of the XML response to a GET request for an individual book

In case the specified resource is not available, it's a good idea to return a 404 (Not Found) status code. This is easily done by having the BookErrorView redirect to the application's default Error404SuccessView, and updating that view with an executeXml() method that returns a 404 message body. Listing 9 has the code.

Listing 9. The Error404Success XML view
<?php
class Default_Error404SuccessView extends ExampleAppDefaultBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $this->getResponse()->setHttpStatusCode('404');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('error'); 
    $dom->appendChild($root);
    $message = $dom->createElement('message', '404 Not Found');
    $root->appendChild($message);
    return $dom->saveXml();
  }
}
?>

Handling POST requests

Handling POST requests is a little more involved. The usual REST convention is that a POST request creates a new resource, with the request body containing all the necessary inputs (in this case, the author and title) for the resource. Now, Agavi can automatically read and convert a URL-encoded request body into individual request parameters. However, when the request body contains an XML document, as occurs with POST and PUT requests, additional processing is needed to convert the XML data into request parameters, suitable for use within an action method.

Listing 10 has an example of one such XML document, representing a new book entry:

Listing 10. An XML document representing a new book entry
<book>
  <title>The Da Vinci Code</title>
  <author>Dan Brown</author>
</book>

The easiest way to do this is to subclass the AgaviWebRequest class to check the Content-Type header of the incoming request and, if the header indicates an XML request body, perform the necessary processing on the XML. Listing 11 has an example of what one such subclass might look like:

Listing 11. A custom HTTP request handler class
<?php
// credit: David Zuelke
class ExampleAppWebRequest extends AgaviWebRequest {
  public function initialize(AgaviContext $context, array $parameters = array()) {
    parent::initialize($context, $parameters);
    $rd = $this->getRequestData();
    if (stristr($rd->getHeader('Content-Type'), 'text/xml')) {
      switch ($_SERVER['REQUEST_METHOD']) {
        case 'PUT': {
          $xml = $rd->removeFile('put_file')->getContents();
          break;
        }
        case 'POST': {
          $xml = $rd->removeFile('post_file')->getContents();
          break;
        }
        default: {
          $xml = '';
        }
      }
      $rd->setParameters((array)simplexml_load_string($xml));
    }
  } 
}
?>

PUT and POST data, if not URL-encoded, will be stored in the request's put_file and post_file file variables. Listing 11 pulls this data out of the request and uses SimpleXML to convert it into an object, then casts this object into an array suitable for use with the AgaviRequestDataHolder's setParameters() method. This data can now be accessed in the usual manner from within action methods with the AgaviRequestDataHolder's getParameter() method.

You can save the updated class definition to $ROOT/app/lib/request/ExampleAppWebRequest.class.php, and then load it by adding it to $ROOT/app/config/autoload.xml. Listing 12 illustrates the addition to this file:

Listing 12. Agavi autoloader configuration
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations xmlns="http://agavi.org/agavi/config/parts/autoload/1.0" 
 xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" 
 parent="%core.system_config_dir%/autoload.xml">
  <ae:configuration>
    <autoload name="ExampleAppBaseAction">
     %core.lib_dir%/action/ExampleAppBaseAction.class.php</autoload>
    <autoload name="ExampleAppBaseModel">
    %core.lib_dir%/model/ExampleAppBaseModel.class.php</autoload>
    <autoload name="ExampleAppBaseView">
    %core.lib_dir%/view/ExampleAppBaseView.class.php</autoload>
    <autoload name="ExampleAppWebRequest">
    %core.lib_dir%/request/ExampleAppWebRequest.class.php</autoload>
    <autoload name="Doctrine">
    %core.app_dir%/../libs/doctrine/Doctrine.php</autoload>    
  </ae:configuration>
</ae:configurations>

That's not all. The usual REST convention is that a POST request creates a new resource, while a PUT request updates an existing resource. However, Agavi's default settings map POST requests to the executeWrite() method and PUT requests to the executeCreate() method. This can be somewhat confusing for REST warriors, and so it's usually a good idea to switch these mappings, by updating the $ROOT/app/config/factories.xml to reflect the new mapping. Listing 13 has the code:

Listing 13. Factory remapping of HTTP request methods to Agavi action methods
<?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/factories/1.0">
  <ae:configuration>  
    ...

    <request class="ExampleAppWebRequest">
      <ae:parameter name="method_names">
          <ae:parameter name="POST">create</ae:parameter>
          <ae:parameter name="GET">read</ae:parameter>
          <ae:parameter name="PUT">write</ae:parameter>
          <ae:parameter name="DELETE">remove</ae:parameter>
      </ae:parameter>
    </request>
  ... 
  </ae:configuration>

With all of this in place, it now becomes possible to define the Books_IndexAction::executeCreate() method to handle POST requests. Listing 14 has the code.

Listing 14. The IndexAction handler for POST requests
<?php
class Books_IndexAction extends ExampleAppBooksBaseAction
{
  public function executeCreate(AgaviRequestDataHolder $rd)
  {
    $book = new Book;
    $book->author = $rd->getParameter('author');
    $book->title = $rd->getParameter('title');
    $book->save();    
    $this->setAttribute('result', array($book->toArray()));
    return 'Success';
  }
}
?>

Remember to also update the action validator to allow these request variables (Listing 15):

Listing 15. The IndexAction 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%/Books/config/validators.xml"
>
  <ae:configuration>

    <validators method="create">
      <validator class="string">
        <arguments>
          <argument>author</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>    
      <validator class="string">
        <arguments>
          <argument>title</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators>

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

REST conventions dictate that the response to a successful POST should contain a 201 (Created) status code, a location header indicating the URL of the new resource, and a representation of the resource in the response body. All of this is accomplished with a small modification to the IndexSuccessView, as in Listing 16.

Listing 16. The revised IndexSuccess XML view
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    if ($this->getContext()->getRequest()->getMethod() == 'create') {
        $this->getResponse()->setHttpStatusCode('201');
        $this->getResponse()->setHttpHeader(
         'Location', 
         $this->getContext()->getRouting()->gen(
          'book', 
          array('id' => $result[0]['id']
        )));     
    }
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');    
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']);      
    }
    return $xml->asXml();
  }
}
?>

Handling PUT and DELETE requests

As noted previously, PUT requests are used to indicate a modification to an existing resource and, as such, include a resource identifier in the request string. A successful PUT implies that the existing resource has been replaced with the resource specified in the PUT request body. The response to a successful PUT can be either status code 200 (OK), with the response body containing a representation of the updated resource, or status code 204 (No Content) with an empty response body.

Listing 17 has the updated validator definition:

Listing 17. The BookAction 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%/Books/config/validators.xml"
>
  <ae:configuration>
    
    <validators>
      <validator class="number">
        <arguments>
          <argument>id</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
          <ae:parameter name="min">1</ae:parameter>
        </ae:parameters>
      </validator>    
    </validators>

    <validators method="write">
      <validator class="string">
        <arguments>
          <argument>author</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>    
      <validator class="string">
        <arguments>
          <argument>title</argument>
        </arguments>
        <ae:parameters>
          <ae:parameter name="required">true</ae:parameter>
        </ae:parameters>
      </validator>
    </validators>

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

Listing 18 has the code for the Books_BookAction::executeWrite() action method, which will handle PUT requests:

Listing 18. The BookAction handler for PUT requests
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
  public function executeWrite(AgaviRequestDataHolder $rd)
  {
    $book = Doctrine::getTable('book')->find($rd->getParameter('id'));
    if (!is_object($book)) {
      return 'Error';
    }
    $book->author = $rd->getParameter('author');
    $book->title = $rd->getParameter('title');
    $book->save();
    $this->setAttribute('result', array($book->toArray()));
    return 'Success';
  }
}
?>

In a similar vein, the executeRemove() method will handle DELETE requests, removing the specified resource from the data store. Listing 19 displays the code for this method:

Listing 19. The BookAction handler for DELETE requests
<?php
class Books_BookAction extends ExampleAppBooksBaseAction
{
  public function executeRemove(AgaviRequestDataHolder $rd)
  {
    $q = Doctrine_Query::create()
          ->delete('Book')
          ->addWhere('id = ?', $rd->getParameter('id'));
    $result = $q->execute();
    $this->setAttribute('result', null);
    return 'Success';
  }
}
?>

The response to a successful DELETE request can be a 200 (OK) with the status in the response body, or a 204 (No Content) with an empty response body. The latter is easily accomplished with a change to the BookSuccessView, as in Listing 20:

Listing 20. The BookSuccess XML view
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
  public function executeXml(AgaviRequestDataHolder $rd)
  {
    if ($this->getContext()->getRequest()->getMethod() == 'remove') {
      $this->getResponse()->setHttpStatusCode('204');
      return false;
    }
    $result = $this->getAttribute('result');
    $dom = new DOMDocument('1.0', 'utf-8');
    $root = $dom->createElement('result');
    $dom->appendChild($root);    
    $xml = simplexml_import_dom($dom);
    foreach ($result as $r) {
      $item = $xml->addChild('item');
      $item->addChild('id', $r['id']);
      $item->addChild('author', $r['author']);
      $item->addChild('title', $r['title']);
    }
    return $xml->asXml();
  }
}
?>

Adding JSON support

The previous sections have illustrated how to set up a simple XML-based REST API. However, JSON is increasingly popular as a data exchange format, and so it's often necessary to also support JSON request and response bodies in your REST API. Agavi's flexible output types make this a breeze to handle.

Step 1: Activate the JSON output type

To begin, add a handler for JSON requests to the routing table, as in Listing 21:

Listing 21. The JSON route handler
<?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 JSON requests -->
      <route name="json" pattern=".json$" cut="true" stop="false" 
       output_type="json" />
      ...
    </routes>
  </ae:configuration>
</ae:configurations>

This route, placed at the top of the routing table, matches all requests ending with the .json suffix and sets them up to use the JSON 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 .json suffix is removed from the request URL once a match occurs.
  • The stop attribute in a route definition indicates whether or not route processing should continue 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://example.localhost/books/1.json, it checks the routing table and finds an immediate match with the top-level catch-all route. Agavi then strips the .json suffix from the request URL and sets the output type for the request to JSON. It then continues checking the remainder of the request http://example.localhost/books/1 against the listed routes until it finds a match with the books.book route and invokes the BookAction. Once the BookAction has completed, Agavi will look for an executeJson() method in the view, execute this method and return its output to the client.

Next, define a new JSON output type (Listing 22):

Listing 22. The JSON 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="xml">      
      <output_type name="json">
        <ae:parameter name="http_headers">
          <ae:parameter name="Content-Type">application/json</ae:parameter>
        </ae:parameter>
      </output_type>
      ...
    </output_types>    
  </ae:configuration>  
</ae:configurations>

Step 2: Extract parameters from JSON Web requests

As with XML, Agavi will not automatically convert JSON packets into request parameters. So, update the custom ExampleAppWebRequest class to handle this task, keying on the Content-Type header and using PHP's json_decode() function to extract JSON values into a PHP array that can be passed to the AgaviRequestDataHolder's setParameters() method. Listing 23 has the code:

Listing 23. The revised Web request handler
<?php
class ExampleAppWebRequest extends AgaviWebRequest {
  public function initialize(AgaviContext $context, array $parameters = array()) {
    parent::initialize($context, $parameters);
    $rd = $this->getRequestData();
    // handle XML requests
    if (stristr($rd->getHeader('Content-Type'), 'text/xml')) {
      switch ($_SERVER['REQUEST_METHOD']) {
        case 'PUT': {
          $xml = $rd->removeFile('put_file')->getContents();
          break;
        }
        case 'POST': {
          $xml = $rd->removeFile('post_file')->getContents();
          break;
        }
        default: {
          $xml = '';
        }
      }
      $rd->setParameters((array)simplexml_load_string($xml));
    }
    // handle JSON requests
    if (stristr($rd->getHeader('Content-Type'), 'application/json')) {
      switch ($_SERVER['REQUEST_METHOD']) {
        case 'PUT': {
          $json = $rd->removeFile('put_file')->getContents();
          break;
        }
        case 'POST': {
          $json = $rd->removeFile('post_file')->getContents();
          break;
        }
        default: {
          $json = '{}';
        }
      }
      $rd->setParameters(json_decode($json, true));
    } 
  } 
}
?>

Step 3: Update application views

The final step is to update the various application views to support JSON output. To do this, attach executeJson() methods to the IndexSuccessView (Listing 24) and BookSuccessView (Listing 25):

Listing 24. The IndexSuccess JSON view
<?php
class Books_IndexSuccessView extends ExampleAppBooksBaseView
{ 
  public function executeJson(AgaviRequestDataHolder $rd)
  {
    $result = $this->getAttribute('result');
    if ($this->getContext()->getRequest()->getMethod() == 'create') {
      $this->getResponse()->setHttpStatusCode('201');
      $this->getResponse()->setHttpHeader('Location', 
       $this->getContext()->getRouting()->gen(
        'book', 
        array('id' => $result[0]['id']
      ))); 
    }
    return json_encode($result);
  }
}
?>
Listing 25. The BookSuccess JSON view
<?php
class Books_BookSuccessView extends ExampleAppBooksBaseView
{
  public function executeJson(AgaviRequestDataHolder $rd)
  {
    if ($this->getContext()->getRequest()->getMethod() == 'remove') {
      $this->getResponse()->setHttpStatusCode('204');     
      return false;
    }
    return json_encode($this->getAttribute('result'));
  }
}
?>

To see this in action, point your Web browser to http://example.localhost/books/.json or http://example.localhost/books/1.json, and you should receive a JSON-encoded response packet with the corresponding data. Figure 5 has an example of this packet, as viewed in the Firebug debugger. (View a text-only version of Figure 5.)

Figure 5. The JSON response to a GET request for all books
Screen capture of the JSON response to a GET request for all books

It's important to note that the JSON support described above was activated simply by making changes in the various views, with the action code remaining untouched. By allowing developers to decide how different output types should be handled 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

Agavi provides a fully realized framework for building a REST API, making it easy for application developers to allow third-party access to application functions using a lightweight, intuitive architectural pattern. Agavi's built-in support for REST routes, as well as its ability to quickly support new output types, makes it ideal for rapid API development and deployment. The nature of Agavi's MVC implementation also means that you can add a REST API to an application at any time during the implementation phase, or even after the application has been deployed, with minimal impact on existing business logic.

See Download for has all the code implemented in this article, together with a simple jQuery-based test script that you can use to perform GET, POST, PUT and DELETE requests on the example API. I recommend you get the code, start playing with it, and maybe try your hand at adding new things to it. I guarantee you won't break anything, and it will definitely add to your learning. Have fun!


Download

DescriptionNameSize
Article examplesexample-app-rest.zip3775KB

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
ArticleID=474167
ArticleTitle=Creating a REST API with Agavi
publish-date=03162010