I'd make it

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

Mind the trailing slash in /books/.

You should also consider using HTTP Accept headers rather than .xml and .json.

Furthermore, it might really be a good idea to swap the method mappings for PUT and POST...


On 23.02.2010, at 10:24, [email protected] wrote:

Hi all,

I have put together a simple REST API that supports both XML and JSON
request/response bodies. The code for this is inline with this email.
I tested this with jQuery and it seems to be fine, but I suspect the
internals could do with some improvements.

I would be very interested in any comments to make this better.

I also plan to turn this into a tutorial that will get published
online, so am keen that it conforms to Agavi best practices, for the
benefit of other developers.

routing.xml
-------------
<routes>                          
 <!-- handler for .xml requests -->
 <route name="json" pattern=".json$" cut="true" stop="false"
output_type="json" />
 <route name="xml" pattern=".xml$" cut="true" stop="false"
output_type="xml" />
                        
 <!-- REST-style routes -->
<route name="books" pattern="^/books$" module="Default" action="Books" />
 <route name="book" pattern="^/books/(id:\d+)$" module="Default"
action="Book" />
</routes>

BookAction
---------------

<?php

class Default_BookAction extends ExampleAppDefaultBaseAction
{
        public function getDefaultViewName()
        {
                return 'Success';
        }
        
        public function executeRead(AgaviRequestDataHolder $rd)
        {
   $q = Doctrine_Query::create()
         ->from('Book b')
         ->addWhere('id = ?', $rd->getParameter('id'));
   $result = $q->execute(array(), Doctrine::HYDRATE_ARRAY);
   $this->setAttribute('result', $result);
   $this->setAttribute('op', 'get');
   return 'Success';
 }

        public function executeCreate(AgaviRequestDataHolder $rd)
        {
   $book = Doctrine::getTable('book')->find($rd->getParameter('id'));
   $book->author = $rd->getParameter('author');
   $book->title = $rd->getParameter('title');
   $book->save();
   $this->setAttribute('result', array($book->toArray()));
   $this->setAttribute('op', 'put');
   return 'Success';
 }

        public function executeRemove(AgaviRequestDataHolder $rd)
        {
   $q = Doctrine_Query::create()
         ->delete('Book')
         ->addWhere('id = ?', $rd->getParameter('id'));
   $result = $q->execute();
   $this->setAttribute('result', null);
   $this->setAttribute('op', 'delete');
   return 'Success';
 }

}

?>

BooksAction
---------------
<?php

class Default_BooksAction extends ExampleAppDefaultBaseAction
{
        public function getDefaultViewName()
        {
                return 'Success';
        }
        
        public function executeRead(AgaviRequestDataHolder $rd)
        {
   $q = Doctrine_Query::create()
         ->from('Book b');
   $result = $q->execute(array(), Doctrine::HYDRATE_ARRAY);
   $this->setAttribute('result', $result);
   $this->setAttribute('op', 'get');
   return 'Success';
 }

        public function executeWrite(AgaviRequestDataHolder $rd)
        {
   $book = new Book;
   $book->author = $rd->getParameter('author');
   $book->title = $rd->getParameter('title');
   $book->save();
   $this->setAttribute('result', array($book->toArray()));
   $this->setAttribute('op', 'post');
   return 'Success';
        
 }
}

?>

BookSuccessView
----------------------
<?php

class Default_BookSuccessView extends ExampleAppDefaultBaseView
{
        public function executeHtml(AgaviRequestDataHolder $rd)
        {
                $this->setupHtml($rd);
        }
        
        public function executeJson(AgaviRequestDataHolder $rd)
        {
        if ($this->getAttribute('op') == 'delete') {
                $this->getResponse()->setHttpStatusCode('204');           
        }
   return json_encode($this->getAttribute('result'));
        }


        public function executeXml(AgaviRequestDataHolder $rd)
        {
        $result = $this->getAttribute('result');
        if ($this->getAttribute('op') == 'delete') {
                $this->getResponse()->setHttpStatusCode('204');
        }
        if ($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();
        }
        }               
}

?>

BooksSuccessView
------------------------
<?php

class Default_BooksSuccessView extends ExampleAppDefaultBaseView
{
        public function executeHtml(AgaviRequestDataHolder $rd)
        {
                $this->setupHtml($rd);
        }
        
        public function executeJson(AgaviRequestDataHolder $rd)
        {
        $result = $this->getAttribute('result');
        if ($this->getAttribute('op') == 'post') {
                $this->getResponse()->setHttpStatusCode('201');
                $this->getResponse()->setHttpHeader('Location',
$this->getContext()->getRouting()->gen('book', array('id' =>
$result[0]['id'])));
        }
   return json_encode($result);
        }

        public function executeXml(AgaviRequestDataHolder $rd)
        {
        $result = $this->getAttribute('result');
        if ($this->getAttribute('op') == 'post') {
                $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();
        }
                        
}

?>

Custom WebRequest class (per David's suggestion)
-----------------------------------------------------------------
<?php
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'), '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));
       }
       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));
       }

   }

}
?>

Questions to the list:

1. Would you recommend always remapping POST to "create" when building
REST APIs with Agavi?

2. Is there any suggestion to improve the routing configuration for
the /books route?

2a. Currently the REST endpoint is /books.xml or /books.json depending
on the format required. How could I alter this so that the router
looks for a ?format=xml|json parameter and sets the output type
accordingly?

3. For DELETE operations using the XML API, is it correct to return a
204 response code and no content at all? Or should I return a 200 code
and an XML document with a <status>OK</status> type of body?

4. I'm setting an attribute in each execute() method indicating the
HTTP request method. I then check this in the view to decide whether
to send a 201 or 204 status code to the client. I'm pretty sure
there's a better way to do this...

5. In case of errors in a POST/PUT/DELETE operation, what response
should I send back for (a) JSON and (b) XML requests?

Many thanks for your comments and advice,

Vikram

_______________________________________________
users mailing list
[email protected]
http://lists.agavi.org/mailman/listinfo/users


Attachment: smime.p7s
Description: S/MIME cryptographic signature

_______________________________________________
users mailing list
[email protected]
http://lists.agavi.org/mailman/listinfo/users

Reply via email to