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
smime.p7s
Description: S/MIME cryptographic signature
_______________________________________________ users mailing list [email protected] http://lists.agavi.org/mailman/listinfo/users
