SOA And Drupal - The Services Module

Drupal Services module

Photo of Greg Harvey
Tue, 2008-08-19 11:21By greg

So, you want to create an SOA-based infrastructure for some or all of your business tools and web sites? Not sure where to start? I'll admit I have worked a little with Microsoft web services, but I am fairly ignorant of the open source alternatives out there for SOA. What I can tell you is I'm hugely impressed with the new Services module for Drupal. It is still in dev, but it's pretty cool! What it attempts to do is "black box" (in the words of friend and ex-colleague, Ray) the process of providing a web service. The Services module, as a piece of software, relies on at least two other modules to exist - a server and a service. On one side the selected server module parses requests made to the endpoint in to PHP variables and passes those variables back to the core Services module. On the other side it also receives return variables back from the Services module and builds the "wrapper" XML around the data you wish to return. Of course it does other stuff too, like formatting errors, building WSDLs (in the case of SOAP), etc. There are several servers available at the moment:

A service is where you, Joe Drupal Developer, come in. You make services. Service modules provide information to the Services module about the methods they wish to expose, define what variables (including type) they expect to receive and define what the Services module can expect them to return. And they do this through a rather neat (albeit, predictably named) hook called hook_service. This hook, you'll be pleased to note, is incredibly simple to use. It is simply an array (like so many things in Drupal) of parameters the Services module expects. Let's look at a very simple service I created for the Image module: /**
* Implementation of hook_service().
*/
function image_service_service() {
return array(
//image service definition
array(
'#method' => 'image.getImages',
'#callback' => 'image_service_get_images',
'#args' => array(
array(
'#name' => 'nid',
'#type' => 'int',
'#description' => t('A node id.'),
),
),
'#return' => 'struct',
'#help' => t('Returns the images attached to a node.')
),
);
}
?> Simple, huh? #method is the name of the method call you are exposing, #callback is the function which will take the provided arguments sent by the client and return the required data, #args is an array of arguments (parameters to be passed to the callback function) the method should expect to receive from a client, #return is simply information for the Services module as to what sort of data it should expect back ('struct' being a structured array) and '#help' is just a string of help text for other developers describing the operation of the service. That's it! We now have a service. Of course, it doesn't do anything yet, because the callback function does not exist, so let's write it: /**
* when given a node id returns any image data and images
* assigned to that node
*
* @param $nid
* integer representing the nid of an image node
*
* @return
* array containing images by size for this node
*/
function image_service_get_images($nid) {
if (module_exists('image')) {
$image_node = node_load($nid);
if ($image_node->type == 'image') {
foreach($image_node->images as $type => $path) {
$fid = db_result(db_query("SELECT fid FROM {image} WHERE nid = %d AND image_size = '%s'", $nid, $type));
$file = db_fetch_object(db_query("SELECT * FROM {files} WHERE fid = %d AND filename = '%s'", $fid, $type));
$fullpath = variable_get('drupal_fullpath', '/').$path;
$binaryfile = fopen($fullpath, 'rb');
$send[$type]['file'] = base64_encode(fread($binaryfile, filesize($fullpath)));
$send[$type]['filename'] = $file->filename;
$send[$type]['uid'] = $file->uid;
$send[$type]['filemime'] = $file->filemime;
$send[$type]['filesize'] = $file->filesize;
$send[$type]['status'] = $file->status;
$send[$type]['timestamp'] = $file->timestamp;
preg_match_all('/.*\/images\/(.*)/', $path, $matches);
$send[$type]['fullname'] = $matches[1][0];
fclose($binaryfile);
}
return $send;
} else {
return array(0 => 'node was not of type image');
}
} else {
return array(0 => 'image module not installed on remote server');
}
}
?> I'm not going to go in to the detail of that function, because it's pretty straight-forward and requires further explanation of how the Image module works, which I'm not going in to right now. What matters is the parameters of the function must match the array of arguments defined in hook_service and the returned variable must be of the type defined in the hook too. After that, what your function does could be *anything*. In this case, my function loads the node, checks it's an image, fetches all of the image variations and returns them, complete with base64 encoded binary file, in a structured array. What happens next? The array goes back to the Services module which passes it on to your installed server(s). They do their thing, wrap it in the correctly formed XML for the protocol they handle and pass the XML on to the requesting client. Job done! In about 40 lines of simple PHP you just wrote a web service. A web service in SOAP, XML-RPC, JSON, etc. - any protocol someone happens to have created a server for. And if there's a protocol you need which isn't on the list, make your own server! =) Now to take our service for a test drive. The endpoints for the various Services module servers always take the form of /services/server_module_name, so in my example, /services/xmlrpc. Here's a basic test script, assuming API keys are switched off, your web service is running on localhost and the node ID of our image node is 3: $request = xmlrpc_encode_request(
"image.getImages", array(
3,
)
);

$context = stream_context_create(
array(
'http' => array(
'method' => "POST",
'header' => "Content-Type: text/xml",
'content' => $request
)
)
);

//output our XML method call
print '

Sent

'. htmlspecialchars(print_r($request, true)) .'

';

$file = file_get_contents("http:// localhost /services/xmlrpc", false, $context);

$response = xmlrpc_decode($file);
if (xmlrpc_is_fault($response)) {
trigger_error("xmlrpc: $response[faultString] ($response[faultCode])");
} else {

//output the returned data
print '

Received

'. htmlspecialchars(print_r($response, true)) .'

';
}
?> The returned array should look something like this: Array
(
[thumbnail] => Array
(
[file] => /9j/4AAQSkZJRgABAQAAAQABAAD//... etc.
[filename] => thumbnail
[uid] => 1
[filemime] => image/jpeg
[filesize] => 2040
[status] => 1
[timestamp] => 1219063947
[fullname] => Sunset.thumbnail.jpg

)
[_original] => Array

(
[file] => /9j/4AAQSkZJRgABAQAAAQABAAD//... etc.
[filename] => _original
[uid] => 1
[filemime] => image/jpeg
[filesize] => 71189
[status] => 1
[timestamp] => 1219061774
[fullname] => Sunset.jpg
)

[preview] => Array

(

[file] => /9j/4AAQSkZJRgABAQAAAQABAAD//... etc.
[filename] => preview
[uid] => 1
[filemime] => image/jpeg
[filesize] => 31455
[status] => 1
[timestamp] => 1219063947
[fullname] => Sunset.preview.jpg
)
)
The recipient can now do as they will with the data. They have the encoded binary (just base64 decode to get it back and save it to disk) as well as all the meta data.
So the Services module is just the glue, taking instructions from the services and passing them to the servers. But it's a mighty elegant system and I am just loving using it.
By the way, it looks like all this stuff might be core for Drupal 7. How exciting. I need the bathroom.