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