Programming RESTful Endpoints in Drupal

Submitted by Steven on Thu, 04/10/2014 - 16:30
API

Expanding on Drupal to accept data from other sources is key to building a viable business solution. Most modern applications integrate data from sources other than user forms. One standard approach to integrating data from outside sources is to use a RESTful endpoint to accept JSON data. There are several contributed options to accepting JSON POST, but you may find a custom option best fits your needs. If you have a need to expand your Drupal platform by programming a custom RESTful endpoint this post will help get you started.

Contributed Options

The Drupal community has contributed several modules for processing RESTful POSTs. Perhaps the most popular, the Services Module, supports multiple formats including SOAP, JSON, REST, XMLRPC and AMF. Services operates on predefined endpoints and can be extended with support from modules like the Services Views module and Services Rules module. One disadvantage with using Services is the lack of backwards compatibility between versions; with a history of breaking upgrade paths between major point releases.

Another option is the RESTful Web Services module. This module only focuses on RESTful services. Instead of providing endpoints it accepts HTTP header content type request and returns JSON or XML when requested. The RESTful Web Services module does not currently have other supporting modules, but should be very forward compatible with Drupal with its ground up entity support.

A Web Services initiative to provide a core RESTful solution has been under development for Drupal 8. At the time of this writing Drupal 8 is in alpha stage and does not have a release date scheduled. With stability in question and a lack of contributed module available using Drupal 8 in your project is probably not a current option.

Custom Option

You may also choose to develop a custom module and write your own handling of a RESTful POST. Some reasons for developing a custom approach is to control consistency between module upgrades and core upgrades. Not only is consistency within Drupal an important factor, but maintaining consistency between applications outside Drupal may also weigh heavily in your decision process. This post will document how to program your own API to accept JSON via POST. How to define your own URI structure for flexibility and upgradeability, how to return proper header response codes and how to save the data as a Drupal object.

Define URI

Establishing a URI structure requires creating a hook_menu() entry defining the path to accept POST. Consider planning for versioning of the API and provide a declaration for the object type. In the example below we define our version and object type in our URI path as version “v1” with an object type of “json”. If/when we make changes to the API we can provide a new version number and point any application to the new version as they update. We also have the opportunity to add new object types and define a new URI for their POST.

function example_menu() {
 $items = array();
 $items['api/v1/post/json'] = array(
   'page callback' => 'example_json_accept',
   'access arguments' => array('access content'),
   'type' => MENU_CALLBACK,
 );
 return $items;
}

*note: Consider accepting arguments in your URI in order to provide a more dynamic API that can serve all your POST needs.

In the menu array above we not only define our path, version and object type, we also define the function called when the URL is requested. Our function will be responsible for validating the request, accepting a PHP input stream, returning status codes, decoding the JSON and processing the data into Drupal before returning a response code of success.

Validation

For minimum validation consider validating the request as a POST, authenticating a specific user, or key and performing data validation for data type and data structure (field and object validation).

When authenticating a user, or key, one option is to pass the information via the header userpwd.

Curl request with userpwd:

curl --user username:password -X POST -d @file.txt --header "Content-Type: application/json" 

http://yourapp.com/api/v1/post/json/[/codefilter_code]

In order to check for the username and password from the example above use PHP’s $_SERVER['PHP_AUTH_USER'] and $_SERVER['PHP_AUTH_PWD']. If you are only validating on a key you can pass the key as the username and validate the username in your application.

Response codes:
When validating request consider returning a proper status code for post method failure, not authorized and incorrect data to inform the application attempting to post the reason for failure.

// Validate the request is a post and return proper response code on failure.
 if ($_SERVER['REQUEST_METHOD'] != 'POST'){
   header('HTTP/1.1 405 Not Post');
}
// Authenticate the user, or key and return proper response code on failure.
elseif ($_SERVER['PHP_AUTH_USER'] != 'trusted user') {
 header('HTTP/1.1 401 Not Authorized');
}
 
// When data type and field validation fails return a 204.
elseif ($validation == FALSE) {
 header('HTTP/1.1 204 Incorrect Data');
}

Processing the JSON

PHP provides a method to obtain stream inputs by using file_get_contents and requesting “php://input”. File_get_contents streams the input and processes it into a string. Once the input is returned as a string use Drupal’s json_decode to return the string as an associative array.

 

$received_json = file_get_contents("php://input",  TRUE);
$json = drupal_json_decode($received_json, TRUE);

 

Note: Consider detecting if json_decode returns null or false.

Once validation has passed and your JSON is available, iterate and save the object to Drupal.

foreach($received->doc as $value) {
   $item = (object) NULL;
   $item ->type = “example”;
   $item->title =$value[‘title’];
        …
  // Prepare object for saving to Drupal.
  node_object_prepare($item);
 
  // Save the prepared object to Drupal.
   node_save($item);
 }

After completing the payload a notice of success to inform the application it’s POST was a success.

// Set header to indicate the item was saved.
header('HTTP/1.1 201 Created');

Knowing how to process a POST request helps developers to evaluate when to use a contributed module, or when the requirements call for a custom module. Programming your own RESTful endpoint is simple, is light on code count and provides flexibility.