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

Reply via email to