Resource (REST) programming model and conventions

IBM® WebSphere® sMash simplifies the task of creating applications using the Representational State Transfer (REST) architectural style.

RESTful design

RESTful designs often make use of collections. A collection is a simple model for manipulating a set of resources. Collections have member items that you can add, remove, update and delete. You can also get a list of members in the collection. For example:

HTTP Method    URI                   Description
-----------    ---                   ----------------
GET            /people               list members
POST           /people               create member
GET            /people/1             retrieve member
PUT            /people/1             update member
DELETE         /people/1             delete member

Simple convention for resources

WebSphere sMash supports the collection model natively within the <apphome>/app/resources virtual directory. Each script within the resources directory represents a resource handler, which implements the collection and member operations.

Resource handlers are accessed via a simple URL convention, based upon the following pattern:

/resources/<collection name>[/<member identifier>[/<path info>]]

For example, consider a request to /resources/people/1 that has the collection name "people" and member identifier "1". The collection name identifies the resource handler. In this case <apphome>/app/resources/people.groovy, and the value of member identifier is provided as a request parameter with name <collection name>Id. In this case, zget("/request/params/peopleId") == 1. The resource handler is a WebSphere sMash event handler, designed to handle resource CRUD events.

HTTP methods are mapped into collection and member events as shown in the following table:

Resource GET PUT POST DELETE
Collection list putCollection create deleteCollection
Member retrieve update postMember delete

Following is a more detailed example of request patterns and corresponding events and parsed data for handlers written in Groovy. The mappings for PHP handlers are detailed here.

Groovy Resource Handler Mappings

HTTP Method URI ...invokes method in app/resources/people.groovy ...with event data
GET /resources/people onList()  
POST /resources/people onCreate()  
GET /resources/people/1/accounts onRetrieve() zget("/request/params/peopleId")==1
zget("/event/pathInfo")==/accounts
PUT /resources/people/1 onUpdate() zget("/request/params/peopleId")==1
DELETE /resources/people/1 onDelete() zget("/request/params/peopleId")==1

Note: Many HTTP servers redirect URIs that do not contain a trailing slash (i.e. "/resources/people" from the example above) to the URI with a trailing slash appended. WebSphere sMash does not redirect RESTful resources that lack a trailing slash in order to maintain a consistent URI space and for readability. Addressing a RESTful resource with a trailing slash will result in a 404 error.


Sample collection handler

Showing a Groovy implementation:

<apphome>/app/resources/people.groovy
def onList() {
    // Get configured DataManager for data access
    def data = zero.data.groovy.Manager.create('peopleDB')

    def result = data.queryArray('SELECT * FROM people')

    // Serialize list to JavaScript Object Notation format
    request.view = 'JSON'
    request.json.output = result
      
    render()
}

def onCreate() {
    // Convert entity to JSON object
    def emp = zero.json.Json.decode(request.input[])

    def data = zero.data.groovy.Manager.create('peopleDB')

    def memberId = data.insert(
                      """INSERT INTO people (firstname, lastname)
                         VALUES ($emp.firstname, $emp.lastname)""", ['id'])

    // Set a Location header with URI to the new record
    locationUri = getRequestedUri(false) + '/' + memberId
    request.headers.out.Location = locationUri
    request.status = HttpURLConnection.HTTP_NO_CONTENT
}

def onRetrieve() {
    // Member id is parsed from the path
    String id = request.params.peopleId[]

    def data = zero.data.groovy.Manager.create('peopleDB')
    def person = data.queryFirst("SELECT * FROM people WHERE id=$id")

    if(person!= null) {
        // Use ViewEngine JSON rendering
        request.view='JSON'
        request.json.output = person
     
        render()
    
    } else {
        // Error handling
        request.status = HttpURLConnection.HTTP_NOT_FOUND
        request.error.message = "username $username not found."
        request.view = 'error'
        render()
    }
}
 
def onUpdate() {
    def emp = zero.json.Json.decode(request.input[])

    def data = zero.data.groovy.Manager.create('personDB')

    data.update("UPDATE employees "
        + "SET firstname=$emp.firstname, lastname=$emp.lastname "
        + "WHERE id=$emp.id")

    request.status = HttpURLConnection.HTTP_NO_CONTENT
}
 
def onDelete() {
    def id = request.params.peopleId[]
    def data = zero.data.groovy.Manager.create('personDB')

    data.update("DELETE FROM people WHERE id=$id");

    request.status = HttpURLConnection.HTTP_NO_CONTENT
}

PHP Resource Handler Mappings

PHP allows a resource handler to scripted as a regular script or through a set of functions defined in a class.

Simple Script dispatch

Using this approach the script is executed for a resource request and it is up to the developer to code for the different methods or events. Since the script is executed for both collection and member resources, distinguishing a GET method for a collection (List) from a GET method for an item (Retrieve) can be done using the value of /event/_name key from the global context.

Values of <resource>Id and pathInfo are added to the request parameters and event zone, respectively, as shown in the following table. Also, for each URI, the script people.php is executed.

Method URI ...with GC data
GET /resources/people zget('/event/_name') = 'list'
POST /resources/people zget('/event/_name') = 'create'
PUT /resources/people zget('/event/_name') = 'putCollection'
DELETE /resources/people zget('/event/_name') = 'deleteCollection'
GET /resources/people/1/acc zget('/event/_name') = 'retrieve'
zget('/request/params/peopleId') = 1
zget('/event/pathInfo') = /acc
PUT /resources/people/1 zget('/event/_name') = 'update'
zget('/request/params/peopleId') = 1
POST /resources/people/1 zget('/event/_name') = 'postMember'
zget('/request/params/peopleId') = 1
DELETE /resources/people/1 zget('/event/_name') = 'delete'
zget('/request/params/peopleId') = 1

The code sample below shows how this can be combined with the /request/method key from the global context.

<appRoot>/app/resources/people.php
<?php

switch (zget('/request/method')) {
        case 'GET':
        if (zget('/event/_name') == 'list') {
                // Perform a list specific operation on the collection
        } else {
                // /event/_name = "retrieve"
                // Retrieve the resource
        }
        break;
        case 'POST':
        // do POST specific handling
        break;
        case 'DELETE':
        // do DELETE specific handling
        break;
        case 'PUT':
        // do PUT specific handling
        break;
}

?>

Method dispatch

The script developer can delegate the method-specific handling to WebSphere sMash by defining a class with the resource name and specifying the operations on the resource by defining the required functions in the class based on the HTTP method and URI as shown in the following table.

Values of <resource>Id and pathInfo are added to the request parameters and event zone, respectively, as shown in the following table.

Method URI ...invokes handler in app/resources/employees.php ...with event data
GET /resources/employees Employees::onList()  
POST /resources/employees Employees::onCreate()  
PUT /resources/employees Employees::onPutCollection()  
DELETE /resources/employees Employees::onDeleteCollection()  
GET /resources/employees/1/acc Employees::onRetrieve() zget('/request/params/peopleId') = 1
zget('/event/pathInfo') = /acc
PUT /resources/employees/1 Employees::onUpdate() zget('/request/params/peopleId') = 1
POST /resources/employees/1 Employees::onPostMember() zget('/request/params/peopleId') = 1
DELETE /resources/employees/1 Employees::onDelete() zget('/request/params/peopleId') = 1

The code sample below shows PHP Method based dispatch. It is based on the resource handler in the zero:zero.phpemployee.demo sample application. Error handling code has been omitted in the interest of brevity but is present in the sample application.

<appRoot>/app/resources/employees.php
<?php
// Initialization common to all operations
// Get configured (as 'employee_db' in zero.config) DataManager for data access
$dataManager = data_manager('employee_db');
class Employees {
	function onList() {
		global $dataManager;
		// Retrieve employee records via Query Zero
		$employeeRecords = dataExec($dataManager, "SELECT * FROM employees");
                // Use the zero global context to render the data as JSON
		zput('/request/view', 'JSON');
		zput('/request/json/output', $employeeRecords);
		render_view();
	}
	function onCreate() {
		global $dataManager;
		// Convert the raw JSON stream in to a PHP array
		$er = json_decode($HTTP_RAW_POST_DATA);
		$result = dataExec($dataManager, "INSERT INTO employees (username, firstname, lastname, location, phonenumber) ". 
		 	"VALUES (?, ?, ?, ?, ?)", array($er['username'], $er['firstname'], $er['lastname'], $er['location'], $er['phonenumber']));
		// verify that an entry was inserted
		// Set a Location header with URI to the new record
		zput('/request/status', 201);
		$locationUri = zget('/request/path') . "/" . $er["username"];
		zput('/request/headers/out/Location', $locationUri);
		zput('/request/headers/out/Content-Type', 'text/json');  			
  	 	echo json_encode($er);
	}
	function onRetrieve() {
		// Get configured DataManager for data access
		global $dataManager;
		// Zero puts 'itemId' when accessing restful resources 
		$username = zget("/request/params/employeesId");
		// Retrieve employee record via Query Zero
		$employeeRecord = dataQueryFirst($dataManager, "SELECT * FROM employees WHERE username=?", array($username));
	  	// JSON encode employee record
		zput('/request/headers/out/Content-Type', 'text/json');  			
	 	echo json_encode($employeeRecord);
	}
	function onUpdate() {
		// Get configured DataManager for data access
		global $dataManager;
		$employeeId = zget('/request/params/employeesId');
		// By convention, Zero places posted text/json into the GC as a JSON object
		$er = json_decode($HTTP_RAW_POST_DATA);
		$result = dataExec($dataManager, "UPDATE employees SET firstname=?, lastname=?, ".
                            "location=?, phonenumber=? WHERE username=?",
                            array($er['firstname'], $er['lastname'], $er['location'], $er['phonenumber'], $employeeId));
		zput("/request/status", 204);
	}
	function onDelete() {
		// Get configured DataManager for data access
		global $dataManager;
		$username = zget("/request/params/employeesId");
		// Delete employee record via Query Zero
		$result = dataExec($dataManager, "DELETE FROM employees WHERE username=?", array($username));
		zput("/request/status", 204);
	}
}
?>

The outer scope script is always invoked prior to the method dispatch. This means that any code outside the class definition (including that in any includes) will be executed for all operations. It is recommended that only common code applicable to all operations be placed outside the class definition.

The class is instantiated prior to method dispatch. If a constructor which takes no arguments is present then it is called.

Note that the class does not actually need to be defined in the PHP file specified in the HTTP request, it can instead be defined in an includeed PHP script.

Note that PHP class and function names are case-insensitive so whilst it is good practice to use names such as onList in fact onlist would also match.

As WebSphere sMash does not buffer output, any PHP code that generates output would effectively commit that response and headers to the client. Subsequent header or status manipulation would be ignored.

Version 1.1.30763