Building REST services with JSON

In the recipe Consuming a JSON service, we learnt how lightweight and convenient the JSON format can be for exchanging data. What happens if we not only want to expose data using JSON, but also allow the possibility to modify it? This is one of the reasons why the REST architecture exists. REST stands for Representational State Transfer, and is no more than a set of principles that guide the concepts that describe its proper implementation.

One of these main principles is that the client-server communication that is part of a REST request should be stateless. This means that no context exists in the server between requests from a specific client. All the information required to perform an operation is part of the request.

In this recipe, we will learn how to add REST services to an application, using JSON as their exchange format. These services will allow any foreign application to get data from a post, create new posts, or delete existing posts.

Getting ready

To go through this recipe we need sample data to work with. Follow the Getting ready section of the Creating an RSS feed recipe.

Create the Post model in a file named post.php and place it in your app/models folder, with the following contents. With the validation option, required, we are telling CakePHP that these fields should always be present when creating or modifying records:

<?php
class Post extends AppModel {
public $validate = array(
'title' => array('required'=>true, 'rule'=>'notEmpty'),
'body' => array('required'=>true, 'rule'=>'notEmpty')
);
}
?>

Let us add actions for creating, editing, and deleting posts. Edit your app/controllers/posts_controller.php file and add the following methods to the PostsController class:

public function add() {
$this->setAction('edit'),
}
public function edit($id=null) {
if (!empty($this->data)) {
if (!empty($id)) {
$this->Post->id = $id;
} else {
$this->Post->create();
}
if ($this->Post->save($this->data)) {
$this->Session->setFlash('Post created successfully'),
$this->redirect(array('action'=>'index'));
} else {
$this->Session->setFlash('Please correct the errors marked below'),
}
} elseif (!empty($id)) {
$this->data = $this->Post->find('first', array(
'conditions' => array('Post.id' => $id)
));
if (empty($this->data)) {
$this->cakeError('error404'),
}
}
$this->set(compact('id'));
}
public function delete($id) {
$post = $this->Post->find('first', array(
'conditions' => array('Post.id' => $id)
));
if (empty($post)) {
$this->cakeError('error404'),
}
if (!empty($this->data)) {
if ($this->Post->delete($id)) {
$this->Session->setFlash('Post deleted successfully'),
$this->redirect(array('action'=>'index'));
} else {
$this->Session->setFlash('Could not delete post'),
}
}
$this->set(compact('post'));
}

We now need to add their respective views. Create a file named edit.ctp and place it in your app/views/posts folder, with the following contents:

<?php
echo $this->Form->create();
echo $this->Form->inputs(array(
'title',
'body'
));
echo $this->Form->end('Save'),
?>

Create a file named delete.ctp and place it in your app/views/posts folder, with the following contents:

<p>Click the <strong>Delete</strong> button to delete
the post <?php echo $post['Post']['title']; ?></p>
<?php
echo $this->Form->create(array('url'=>array('action'=>'delete', $post['Post']['id'])));
echo $this->Form->hidden('Post.id', array('value'=>$post['Post']['id']));
echo $this->Form->end('Delete'),
?>

Modify the app/views/posts/index.ctp to add links to these actions by changing the whole view to the following:

<h1>Posts</h1>
<?php if (!empty($posts)) { ?>
<ul>
<?php foreach($posts as $post) { ?>
<li>
<?php echo $this->Html->link($post['Post']['title'], array(
'action'=>'view',
$post['Post']['id']
)); ?>
:
<?php echo $this->Html->link('Edit', array(
'action'=>'edit',
$post['Post']['id']
)); ?>
-
<?php echo $this->Html->link('Delete', array(
'action'=>'delete',
$post['Post']['id']
)); ?>
</li>
<?php } ?>
</ul>
<?php } ?>
<?php echo $this->Html->link('Create new Post', array('action'=>'add')); ?>

How to do it...

  1. Edit your app/config/routes.php file and add the following statement at the end:
    Router::parseExtensions('json'),
    
  2. Edit your app/controllers/posts_controller.php file and add the following property to the PostsController class:
    public $components = array('RequestHandler'),
    
  3. Create a folder named json in your app/views/layouts folder, and inside the json folder, create a file named default.ctp, with the following contents:
    <?php
    echo $content_for_layout;
    ?>
    
  4. Create a folder named json in your app/views/posts folder, and inside the json folder, create a file named index.ctp, with the following contents:
    <?php
    foreach($posts as $i => $post) {
    $post['Post']['url'] = $this->Html->url(array(
    'action'=>'view',
    $post['Post']['id']
    ), true);
    $posts[$i] = $post;
    }
    echo json_encode($posts);
    ?>
    
  5. Edit your app/controllers/posts_controller.php file and add the following method to the end of the PostsController class:
    protected function _isJSON() {
    return $this->RequestHandler->ext == 'json';
    }
    
  6. Edit the PostsController::index() method and make the following changes:
    public function index() {
    if ($this->_isJSON() && !$this->RequestHandler->isGet()) {
    $this->redirect(null, 400);
    }
    $posts = $this->Post->find('all'),
    $this->set(compact('posts'));
    }
    
  7. Add the following methods to the beginning of the PostsController class below the declaration of the components property:
    public function beforeFilter() {
    parent::beforeFilter();
    if (
    $this->_isJSON() &&
    !$this->RequestHandler->isGet()
    ) {
    if (empty($this->data) && !empty($_POST)) {
    $this->data[$this->modelClass] = $_POST;
    }
    }
    }
    public function beforeRender() {
    parent::beforeRender();
    if ($this->_isJSON()) {
    Configure::write('debug', 0);
    $this->disableCache();
    }
    }
    
  8. Edit the PostsController::edit() method and make the following changes:
    public function edit($id=null) {
    if ($this->_isJSON() && !$this->RequestHandler->isPost()) {
    $this->redirect(null, 400);
    }
    if (!empty($this->data)) {
    if (!empty($id)) {
    $this->Post->id = $id;
    } else {
    $this->Post->create();
    }
    if ($this->Post->save($this->data)) {
    $this->Session->setFlash('Post created successfully'),
    if ($this->_isJSON()) {
    $this->redirect(null, 200);
    } else {
    $this->redirect(array('action'=>'index'));
    }
    } else {
    if ($this->_isJSON()) {
    $this->redirect(null, 403);
    } else {
    $this->Session->setFlash('Please correct the errors marked below'),
    }
    }
    } elseif (!empty($id)) {
    $this->data = $this->Post->find('first', array(
    'conditions' => array('Post.id' => $id)
    ));
    if (empty($this->data)) {
    if ($this->_isJSON()) {
    $this->redirect(null, 404);
    }
    $this->cakeError('error404'),
    }
    }
    $this->set(compact('id'));
    }
    
  9. Edit the PostsController::delete() method and make the following changes:
    public function delete($id) {
    if ($this->_isJSON() && !$this->RequestHandler->isDelete()) {
    $this->redirect(null, 400);
    }
    $post = $this->Post->find('first', array(
    'conditions' => array('Post.id' => $id)
    ));
    if (empty($post)) {
    if ($this->_isJSON()) {
    $this->redirect(null, 404);
    }
    $this->cakeError('error404'),
    }
    if (!empty($this->data) || $this->RequestHandler->isDelete()) {
    if ($this->Post->delete($id)) {
    $this->Session->setFlash('Post deleted successfully'),
    if ($this->_isJSON()) {
    $this->redirect(null, 200);
    } else {
    $this->redirect(array('action'=>'index'));
    }
    } else {
    if ($this->_isJSON()) {
    $this->redirect(null, 403);
    } else {
    $this->Session->setFlash('Could not delete post'),
    }
    }
    }
    $this->set(compact('post'));
    }
    

To test these services, we are going to create a small CakePHP shell that will create a new post, edit the created post, delete it, and show the list of posts throughout the process. Create a file named consume.php and place it in your app/vendors/shells folder, with the following contents:

<?php
App::import('Core', 'HttpSocket'),
class ConsumeShell extends Shell {
protected static $baseUrl;
protected static $httpSocket;
public function main() {
if (empty($this->args) || count($this->args) != 1) {
$this->err('USAGE: cake consume <baseUrl>'),
$this->_stop();
}
self::$baseUrl = $this->args[0];
$this->test();
}
protected function test() {
$this->request('/posts/add.json', 'POST', array(
'title' => 'New Post',
'body' => 'Body for my new post'
));
$lastId = $this->listPosts();
$this->hr();
$this->request('/posts/edit/'.$lastId.'.json', 'POST', array(
'title' => 'New Post Title',
'body' => 'New body for my new post'
));
$this->listPosts();
$this->hr();
$this->request('/posts/delete/'.$lastId.'.json', 'DELETE'),
$this->listPosts();
}
protected function request($url, $method='GET', $data=null) {
if (!isset(self::$httpSocket)) {
self::$httpSocket = new HttpSocket();
} else {
self::$httpSocket->reset();
}
$body = self::$httpSocket->request(array(
'method' => $method,
'uri' => self::$baseUrl . '/' . $url,
'body' => $data
));
if ($body === false || self::$httpSocket->response['status']['code'] != 200) {
$error = 'ERROR while performing '.$method.' to '.$url;
if ($body !== false) {
$error = '[' . self::$httpSocket->response['status']['code'] . '] ' . $error;
}
$this->err($error);
$this->_stop();
}
return $body;
}
protected function listPosts() {
$response = json_decode($this->request('/posts.json'));
$lastId = null;
foreach($response as $item) {
$lastId = $item->Post->id;
$this->out($item->Post->title . ': ' . $item->Post->url);
}
return $lastId;
}
}
?>

To run this shell script, invoke it with one argument: the base URL of your application. So change http://localhost below to suit your application's URL:

  • If you are on a GNU Linux / Mac / Unix system:
    ../cake/console/cake consume http://localhost
    
  • If you are on Microsoft Windows:
    ..cakeconsolecake.bat consume http://localhost
    

The output should be similar to that shown in the following screenshot:

How to do it...

We can see that the first list of posts shows our newly created post entitled New Post. The second list shows how we successfully changed its title to New Post Title, and the third list shows how we deleted the post.

How it works...

Similarly to what was described in the Creating an RSS feed recipe, we started by specifying json as a valid extension and added the RequestHandler component to our list of components.

Unlike the rss and xml extensions, CakePHP does not provide a default layout for json, so we need to create one. Through the beforeRender callback, we turn debugging off, and we disable caching when a JSON request is made, to avoid any information that would break the JSON syntax and prevent client browsers from caching JSON requests.

Note

When a JSON request is made to a controller that uses the RequestHandler component, the component will automatically set the content type of the response to application/json.

Once we have our layout, we are ready to start implementing our JSON views. In this recipe, we only implement index() as a JSON action that returns JSON data through a view. All the other actions—add(), edit(), and delete()—will simply use HTTP status codes to communicate with the client. The JSON index.ctp view will simply add the full URL for each post, and echo the whole data structure as a JSON-formatted string using json_encode().

As we will be changing some of the controller logic depending on the type of access (JSON versus normal access), we add a method named _isJSON() to our controller. This method uses the ext property of the RequestHandler component, which is set to the extension with which our action is requested. If no extension is used, and then it defaults to html. Using this property, we can check when a request is made using the json extension.

With _isJSON(), we can also add some extra checks to our methods, to make sure they are requested the proper way. For our index action, we make sure that if the request is made with JSON, we only allow GET requests to go through. If the request was made with any other method, for example, with POST, then we return an HTTP status of 400 (Bad Request), and we exit the application.

Note

When no data needs to be sent back to the client, HTTP status codes are a great way to inform if a REST request has succeeded or failed.

To help users of our REST requests, we should allow them to POST data without having to know how the data needs to be formatted for CakePHP to process it automatically. Therefore, we override the beforeFilter callback, so if a request is made with JSON that is not a GET request, and if CakePHP did not find any data properly formatted (when data was indeed posted), then we set what was posted as the controller data. This way, when creating or modifying posts, client code can simply use title to refer to the post title field, rather than having to use data[Post][title] as the name for the field.

We then proceed to make the necessary modifications to the edit() method. We start by making sure that we were accessing with the proper method (POST), and we change how we report success or failure: with an HTTP status of 200 (OK) when the post is saved, 403 (Forbidden) if the post cannot be saved, or 404 (Not Found) if trying to edit a post that does not exist.

The modifications to the delete() method are almost identical to the ones made to the edit() method. The two main differences are that the expected method is DELETED, and that we don't enforce data to be posted when being accessed through JSON.

To test the code in this recipe, we built a shell script to consume our REST services. This script uses the HttpSocket class to fetch the content. In this shell script, we built a generic request() function that takes a URL, a method (we use GET, POST, and DELETE), and an optional array of data to post.

We use the request() method to create a new post (notice how we specify the values for the title and body fields), get the list of posts that should include our newly created post, modify the created post, and finally delete it.

See also

  • Creating an RSS feed
  • Adding authentication to REST services
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.168.203