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.