Drupal 7 Tutorial: Customizing Services Input and Output format

Recently I was working with the Drupal services module, and ran into a few hang ups. For my project I needed a rest service that could export xml that matched a specific format, and consume xml and store it in the database for later processing. To do this I created a custom services resource, this was pretty straight forward. I found a few really great sites that explain how this is done. This one in particular is quite helpful. It was when I attempted to customize the output and input that I ran into some hang ups. When attempting to research more about how it is done, I found very little information posted anywhere. In this brief tutorial I will outline how I was able to achieve my goals, and hopefully save you some time.

At this point I had setup my custom resource, and had some sample data put into the callback for testing my xml output generation. For my example here lets say I needed to have an output that looked like this.

<?xml version="1.0" encoding="utf-8"?>
<Fruit_Basket custom_attribute="attribute_value">
   <fruits>
      <fruit>
         <name>apple</name>
      </fruit>
      <fruit>
         <name>banana</name>
      </fruit>
      <fruit>
         <name>kiwi</name>
      </fruit>
      <fruit>
         <name>orange</name>
      </fruit>
    </fruits>
</Fruit_Basket>

But when I would ping my end point, services would output this instead.

<?xml version="1.0" encoding="utf-8"?>
<result>
   <fruits>
      <item>
         <name>apple</name>
      </item>
      <item>
         <name>banana</name>
      </item>
      <item>
         <name>kiwi</name>
      </item>
      <item>
         <name>orange</name>
      </item>
   </fruits>
</result>

This schema was a bit different than what I required. The root node was hard coded to be set to "result", and the children of the node "fruits" all where named "item". This was going to be a pretty big problem for my needs. These nodes had to be named correct or the application consuming the data would not process them correctly. As well there was no support for xml attributes. You could not specify the attributes such as .

The fix for this is to call the hook that allows us to specify a new formatter for this content type.
I was able to accomplish this through the use of hook_rest_server_response_formatters_alter().

/**
* implementation of hook_rest_server_response_formatters_alter()
*/
function custom_services_resource_rest_server_response_formatters_alter(&$formatters) {
  //This will change the formatter for xml, to use our custom one so it renders correctly
  $formatters['xml']['view'] = 'ExampleRESTServerView';
}
 
class ExampleRESTServerView extends RESTServerView {
  public function render() {
    $doc = new DOMDocument('1.0', 'utf-8');
    $root = $doc->createElement('Fruit_Basket');
    $doc->appendChild($root);
 
    $this->xml_recurse($doc, $root, $this->model);
 
    return $doc->saveXML();
  }
 
  private function xml_recurse(&$doc, &$parent, $data) {
    if (is_object($data)) {
      $data = get_object_vars($data);
    }
 
    if (is_array($data)) {
      $assoc = FALSE || empty($data);
      $k = 'item';
      foreach ($data as $key => $value) {
        if ($key === "key") {
          $k = $value;
          $k = preg_replace('/[^A-Za-z0-9_]/', '_', $k);
          $k = preg_replace('/^([0-9]+)/', '_$1', $k);
          continue;
        }
        else if($key === "attrs") {
          foreach($value as $attr_name => $attr_value) {
            $parent->setAttribute($attr_name, $attr_value);
          }
          continue;
        }
        else if (is_numeric($key)) {
          $key = $k;
        }
        else {
          $assoc = TRUE;
          $key = preg_replace('/[^A-Za-z0-9_]/', '_', $key);
          $key = preg_replace('/^([0-9]+)/', '_$1', $key);
        }
 
        $element = $doc->createElement($key);
        $parent->appendChild($element);
        $this->xml_recurse($doc, $element, $value);
      }
    }
    elseif ($data !== NULL) {
      $parent->appendChild($doc->createTextNode($data));
    }
  }
}

This custom version of the xml formatter is a modified version of the original xml formatter. In this version I changed the render function to have a few new features. These include the ability to specify a element name when passing an array to be rendered. And the ability to specify xml attributes.

Looking at my sample data here shows how the data would need to be formatted.

function custom_services_resource_index() {
  $requests['attrs'] = array("custom_attribute" => "attribute_value");
  $requests['fruits'] = array("key" => "fruit", array("name" => "apple"), array("name" => "banana"), array("name" => "kiwi"), array("name" => "orange"));
  return (object)$requests;
}

I added special key values, "attrs" and "key". When the processing hits "attrs" It will treat these values as the key is the name of attribute, and the value is its value. So in the above example we would see this attribute custom_attribute="attribute_value". And the attribute would be assigned to the node that this key appears directly under. So it would look like .

And when the key named "key" is seem it lets us process the array of nodes with the specified element name.
In our example that gets us,

<fruit>
  <name>Apple</name>
</fruit>

Rather than

<item>
  <name>Apple</name>
<item>

Also note, if the array is passed here and no key value was specified, "item" would be defaulted to.
Using the custom formatter specified in the hook we are able to adjust the data that is exported letting us match the schema requirements.

Now what about when we are posting the xml response back to the server. I found issues with the default xml parser. If we were to take the xml from the output above, and just sent that back into the system. The default output that would be presented in the callback would be parsed from into an array that would look like this.

Array
(
    [fruits] => Array
        (
            [fruit] => Array
                (
                    [name] => orange
                )
        )
)

In our xml we had 4 fruits, but in this resulting array we ended up with just one. This is because the data was converted into an array and the keys of the same name collided and were essentially condensed into one. This gave us just the last value of the array.
The way around this was similar to how we handled the output, we needed to use a hook to specify a new xml parser. The hook to use this time was hook_rest_server_request_parsers_alter().

/**
* implementation of hook_rest_server_request_parsers_alter()
*/
function custom_services_resource_rest_server_request_parsers_alter(&$parsers) {
 
 //Our content type is text/xml this changes the parser to be our custom one
 $parsers['text/xml'] = "ExampleXMLResponseParser::parseXML";
}
 
class ExampleXMLResponseParser extends RESTServer {
  public static function parseXML($handle)  {
    //just want the straight xml from the request to be passed along
    $xml = self::contentFromStream($handle);
 
    // if $xml is Null then we expect errors
    if (!$xml) {
      // build an error message string
      $message = 'XML value was null';
 
      // throw an error
      services_error($message, 406);
    }
    return $xml;
  }
}

This tells the server fo change the parser of content type “text/xml” to use my custom parseXML function. Next I defined this class that would do the parsing. I used pretty close to the same as the original but told it just to pass along the xml it received. I needed to do this because the xml was going to be handled at a later time and I needed to just store it for now. You could change the parser to do special handling of the xml here if needed, or pass it along as the result and have the callback do the processing. Either way now the xml isn't being condensed into an array and losing quite a bit of the data a long the way.

And that is how you can hook in and change how services handles the processing of the output and parsing of the input data.

Download the code used in this tutorial right here.

You can get it almost working by using

class ExampleServicesFruitsFormatter implements ServicesFormatterInterface {

on line 172 of the .module file, and copying code from ServicesFormatter.inc file in servers/rest_server/includes/ folder in services module folder. Just find

class ServicesXMLFormatter implements ServicesFormatterInterface {

in that file and make changes to the download above where they diverge.

The only problem is that you still have 'result' in the root of the XML returned from the service. I tried overrding the 'formatter class' in custom_services_resource_index function, but that worked on ALL XML resources, not just the fruits.

Just FYI in case anyone sees this, for Services 3.4 and beyond, how does one create a custom XML resource without completely blotting over the XML for node/user/views etc?

Thanks! Even though it's not working with 3.4 without hacks, it is very helpful in beginning to understand how to customize Services.

Argh. My nine-year-old son was heavily distracting me as I wrote that. What I meant to say was that I tried to turn 'result' to 'fruits' in the root by adding to this function:

/**
 * implementation of hook_rest_server_response_formatters_alter()
 */
function custom_services_resource_rest_server_response_formatters_alter(&$formatters) {
  //This will change the formatter for xml, to use our custom one so it renders correctly
  $formatters['xml']['view'] = 'ExampleServicesFruitsFormatter';
  // I added the line below to take care of the 'result' element in root. 
  // I want it to say 'fruits'.
  $formatters['xml']['formatter class'] = 'ExampleServicesFruitsFormatter';
}

That second line does indeed work in changing the root element, but it also changes it to 'fruits' for every XML resource. I haven't had time to figure out how to change 'result' in only the custom resource. It is hard-coded in the XML formatter provided by services. I could probably do it with some kind of conditional in that alter hook, so that a non-'fruit' resource skips it.

Once I have it all working, I can share a patch to the above download for anyone who is interested. Thanks.

@Chris and @Peter - thanks for sharing this, it worked for me in D7. I am also looking for a way to change the XML formatter only for my custom resource and not for all (node/user/... etc). Did you find a way to do this yet? We need a way to detect which resource was just invoked.

Hey Farhad,

No, I have not yet tried, but the simplest way to do it wold be grabbing the path from the URL and doing conditionals against it. So, if your resource has a path of 'myresource' you could match against that to make sure only your custom resource is currently in use.

Try current_path(): https://api.drupal.org/api/drupal/includes!path.inc/function/current_pa…

This is not a bulletproof way of doing things, because what if someone creates an endpoint path that happens to match your default resource path? Still, this is how I would approach it until I think of something better. Best of luck!

-C

Thank you for this tutorial, its the only help I can find for changing the ITEM tag. All I need to do is change this item and /item tag. How should I use your code that I have downloaded? Do I install it just like a module?

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <cpp>, <java>, <php>. The supported tag styles are: <foo>, [foo].
  • Web page addresses and email addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.

About the Author