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.

Ready to get started?