We'll implement a simple service for notes that's exposed using the REST Server. Then we'll implement a simple JavaScript client for note-taking. This post is pretty much written like extended code comments and the full code and article text is available here. Note: this page contains a bug fix in the JavaScript testing logic for Create-POST and Update-Put that is not in that link. Feel free to fork this repo and flesh out the text if you want to.

Agenda

  • Getting the necessary modules
  • Implementing a note service (resource)
  • Creating a endpoint
  • Writing a simple JavaScript client

Getting the necessary modules

First off we have to download the necessary modules of Drupal.org.

Services

Download the 6.x-3.x or 7.x-3.x version from the Services project page at Drupal.org.

Chaos tool suite

The only dependency for services. Provides the framework for the endpoint definitions so that they can be exported and defined in both code and the database. Maybe further along the road it'll be used for a plugin system for the servers and authentication mechanisms.

REST Server

My server implementation of choice and what we'll be using to test our service. This is included in the Services download.

Autoload

Autoload is required by the REST Server for Drupal core 6.x. The Autoload functionality was moved into the Drupal 7 core.

The Autoload module is a utility module. It allows other modules to leverage PHP 5's class autoloading capabilities in a unified fashion.

Implementing a note service (resource)

Create a noteresource module that will contain our service implementation. The info file could look something like this:

    name = Note Resource
    description = Sample resource implementation
    package = Notes example
    dependencies[] = services
    core = 6.x
    php = 5.2

To get a really simple example I'll create an api for storing notes. In a real world scenario notes would probably be stored as nodes, but to keep things simple we'll create our own table for storing notes.

    // noteresource.install
    /**
     * Implements hook_install().
     */
    function noteresource_install() {
      drupal_install_schema('noteresource');
    }

    /**
     * Implements hook_uninstall().
     */
    function noteresource_uninstall() {
      drupal_uninstall_schema('noteresource');
    }

    /**
     * Implements hook_schema().
     */
    function noteresource_schema() {
      return array(
        'note' => array(
          'description' => 'Stores information about notes',
          'fields' => array(
            'id' => array(
              'description' => 'The primary identifier for a note.',
              'type' => 'serial',
              'unsigned' => TRUE,
              'not null' => TRUE,
            ),
            'uid' => array(
              'description' => t('The user that created the note.'),
              'type' => 'int',
              'unsigned' => TRUE,
              'not null' => TRUE,
              'default' => 0,
            ),
            'created' => array(
              'description' => t('The timestamp for when the note was created.'),
              'type' => 'int',
              'unsigned' => TRUE,
              'not null' => TRUE,
              'default' => 0,
            ),
            'modified' => array(
              'description' => t('The timestamp for when the note was modified.'),
              'type' => 'int',
              'unsigned' => TRUE,
              'not null' => TRUE,
              'default' => 0,
            ),
            'subject' => array(
              'description' => t('The subject of the note'),
              'type' => 'varchar',
              'length' => 255,
              'not null' => TRUE,
            ),
            'note' => array(
              'description' => t('The note'),
              'type' => 'text',
              'size' => 'medium',
            ),
          ),
          'primary key' => array('id'),
        ),
      );
    }

The familiar stuff

Now lets implement some basic hooks and API methods. We need some permissions that'll be used to decide what our users can and cannot do:

    // noteresource.module
    /**
     * Implements hook_perm().
     */
    function noteresource_perm() {
      return array(
        'note resource create',
        'note resource view any note',
        'note resource view own notes',
        'note resource edit any note',
        'note resource edit own notes',
        'note resource delete any note',
        'note resource delete own notes',
      );
    }

Now for some Drupal API methods for the basic CRUD operations for our notes. These will be used by the functions that are used as callbacks for our resource. But it's always a good idea to supply functions like these so that other Drupal modules have a nice and clean interface to your module's data.

    // noteresource.module
    /**
     * Gets a note object by id.
     *
     * @param int $id
     * @return object
     */
    function noteresource_get_note($id) {
      return db_fetch_object(db_query("SELECT * FROM {note} WHERE id=:id", array(
        ':id' => $id,
      )));
    }

    /**
     * Writes a note to the database
     *
     * @param object $note
     * @return void
     */
    function noteresource_write_note($note) {
      $primary_key = !empty($note->id) ? array('id') : NULL;
      drupal_write_record('note', $note, $primary_key);
    }

    /**
     * Deletes a note from the database.
     *
     * @param int $id
     * @return void
     */
    function noteresource_delete_note($id) {
      db_query("DELETE FROM {note} WHERE id=:id", array(
        ':id' => $id,
      ));
    }

Defining our resource

All resources are defined through hook_services_resources(). The way resources are declared is quite similar to how the template and menu system works, it also bears a very close resemblance to how 2.x services are defined.

Notice how we define the basic CRUD methods here: create, retrieve, update, delete (and index). Most resources implement these methods, but it is also possible to implement actions, targeted actions and relationships. Those won't be covered here but their general nature is explained in the REST Server README.

All the methods have `'file' => array('type' => 'inc', 'module' => 'noteresource', 'name' => 'noteresource'),` specified, which tells services that it can find the callback function in the file noteresource.inc, which is where we will write them all.

    // noteresource.module
    /**
     * Implements hook_services_resources().
     */
    function noteresource_services_resources() {
      return array(
       'note' => array(
         'retrieve' => array(
           'help' => 'Retrieves a note',
           'file' => array('type' => 'inc', 'module' => 'noteresource', 'name' => 'noteresource'),
           'callback' => '_noteresource_retrieve',
           'access callback' => '_noteresource_access',
           'access arguments' => array('view'),
           'access arguments append' => TRUE,
           'args' => array(
             array(
               'name' => 'id',
               'type' => 'int',
               'description' => 'The id of the note to get',
               'source' => array('path' => '0'),
               'optional' => FALSE,
             ),
           ),
         ),
         'create' => array(
           'help' => 'Creates a note',
           'file' => array('type' => 'inc', 'module' => 'noteresource', 'name' => 'noteresource'),
           'callback' => '_noteresource_create',
           'access arguments' => array('note resource create'),
           'access arguments append' => FALSE,
           'args' => array(
             array(
               'name' => 'data',
               'type' => 'struct',
               'description' => 'The note object',
               'source' => 'data',
               'optional' => FALSE,
             ),
           ),
         ),
         'update' => array(
           'help' => 'Updates a note',
           'file' => array('type' => 'inc', 'module' => 'noteresource', 'name' => 'noteresource'),
           'callback' => '_noteresource_update',
           'access callback' => '_noteresource_access',
           'access arguments' => array('update'),
           'access arguments append' => TRUE,
           'args' => array(
             array(
               'name' => 'id',
               'type' => 'int',
               'description' => 'The id of the node to update',
               'source' => array('path' => '0'),
               'optional' => FALSE,
             ),
             array(
               'name' => 'data',
               'type' => 'struct',
               'description' => 'The note data object',
               'source' => 'data',
               'optional' => FALSE,
             ),
           ),
         ),
         'delete' => array(
           'help' => 'Deletes a note',
           'file' => array('type' => 'inc', 'module' => 'noteresource', 'name' => 'noteresource'),
           'callback' => '_noteresource_delete',
           'access callback' => '_noteresource_access',
           'access arguments' => array('delete'),
           'access arguments append' => TRUE,
           'args' => array(
             array(
               'name' => 'nid',
               'type' => 'int',
               'description' => 'The id of the note to delete',
               'source' => array('path' => '0'),
               'optional' => FALSE,
             ),
           ),
         ),
         'index' => array(
           'help' => 'Retrieves a listing of notes',
           'file' => array('type' => 'inc', 'module' => 'noteresource', 'name' => 'noteresource'),
           'callback' => '_noteresource_index',
           'access callback' => 'user_access',
           'access arguments' => array('access content'),
           'access arguments append' => FALSE,
           'args' => array(array(
               'name' => 'page',
               'type' => 'int',
               'description' => '',
               'source' => array(
                 'param' => 'page',
               ),
               'optional' => TRUE,
               'default value' => 0,
             ),
             array(
               'name' => 'parameters',
               'type' => 'array',
               'description' => '',
               'source' => 'param',
               'optional' => TRUE,
               'default value' => array(),
             ),
           ),
         ),
       ),
      );
    }

There is another alternative when defining services (which I personally prefer) but that will probably be covered in a later article. Take a look at http://github.com/hugowetterberg/services_oop if you're curious.

Implementing the callbacks

Create the file noteresource.inc which is where we told services that it could find our callbacks.

We'll start with the create-callback. The method will receive an object describing the note that is about to be saved. The attributes we want are subject and note and we'll throw an error if those are missing. We return the id of the created note, and it's uri so that the client knows how to access it. A get-request to the uri will return the full note.

    // noteresource.inc
    /**
     * Callback for creating note resources.
     *
     * @param object $data
     * @return object
     */
    function _noteresource_create($data) {
      global $user;

      unset($data->id);
      $data->uid = $user->uid;
      $data->created = time();
      $data->modified = time();

      if (!isset($data->subject)) {
        return services_error('Missing note attribute subject', 406);
      }

      if (!isset($data->note)) {
        return services_error('Missing note attribute note', 406);
      }

      noteresource_write_note($data);
      return (object)array(
        'id' => $data->id,
        'uri' => services_resource_uri(array('note', $data->id)),
      );
    }

The update callback works more or less the same, but we don't have to check that subject and note exists, there is no harm in allowing a client to just update the subject and leave the note alone.

    // noteresource.inc
    /**
     * Callback for updating note resources.
     *
     * @param int $id
     * @param object $data
     * @return object
     */
    function _noteresource_update($id, $data) {
      global $user;
      $note = noteresource_get_note($id);

      unset($data->created);
      $data->id = $id;
      $data->uid = $note->uid;
      $data->modified = time();

      noteresource_write_note($data);
      return (object)array(
        'id' => $id,
        'uri' => services_resource_uri(array('note', $id)),
      );
    }

The retrieve and delete callbacks are pretty trivial and probably don't need any further explanation.

    // noteresource.inc
    /**
     * Callback for retrieving note resources.
     *
     * @param int $id
     * @return object
     */
    function _noteresource_retrieve($id) {
      return noteresource_get_note($id);
    }

    /**
     * Callback for deleting note resources.
     *
     * @param int $id
     * @return object
     */
    function _noteresource_delete($id) {
      noteresource_delete_note($id);
      return (object)array(
        'id' => $id,
      );
    }

The index callback fetches a users notes and returns them all. We specified some arguments for this method that we don't use. They are mostly here to show that it would be a good idea to support paging and filtering of a index listing.

    // noteresource.inc
    /**
     * Callback for listing notes.
     *
     * @param int $page
     * @param array $parameters
     * @return array
     */
    function _noteresource_index($page, $parameters) {
      global $user;

      $notes = array();
      $res = db_query("SELECT * FROM {note} WHERE uid=:uid ORDER BY modified DESC", array(
        ':uid' => $user->uid,
      ));

      while ($note = db_fetch_object($res)) {
        $notes[] = $note;
      }

      return $notes;
    }

Access checking

Last but not least, we specified an access callback for all methods. This checks so that users don't oversteps their bounds and starts looking at other people's notes without having the proper permissions. This function should be in the main .module file.

    // noteresource.module
    /**
     * Access callback for the note resource.
     *
     * @param string $op
     *  The operation that's going to be performed.
     * @param array $args
     *  The arguments that will be passed to the callback.
     * @return bool
     *  Whether access is given or not.
     */
    function _noteresource_access($op, $args) {
      global $user;
      $access = FALSE;

      switch ($op) {
        case 'view':
          $note = noteresource_get_note($args[0]);
          $access = user_access('note resource view any note');
          $access = $access || $note->uid == $user->uid && user_access('note resource view own notes');
          break;
        case 'update':
          $note = noteresource_get_note($args[0]->id);
          $access = user_access('note resource edit any note');
          $access = $access || $note->uid == $user->uid && user_access('note resource edit own notes');
          break;
        case 'delete':
          $note = noteresource_get_note($args[0]);
          $access = user_access('note resource delete any note');
          $access = $access || $note->uid == $user->uid && user_access('note resource delete own notes');
          break;
      }

      return $access;
    }

As you can see neither the create nor the index function is represented here. That's because they both use user_access() directly. Unlike the other methods there are no considerations like note ownership to take into account. For creation the permission 'note resource create' is checked and for the index listing only 'access content' is needed.

Creating an endpoint

The endpoint can actually be created in two ways either through the admin interface or through code. The easiest option is most often to create the endpoint through the interface, and then export it and copy paste it into your module.

Go to admin/structure/services and click "Add endpoint". Name your endpoint "notes" and call it something nice, like "Note API". Choose "REST" as your server and place the endpoint at "js-api".

Save and click the Resources tab/local task and enable all methods for the note resource. Then save your changes.

You should now have a proper working endpoint that exposes your note API. The easiest way to check that everything's working properly is to add a dummy note to your table. Then try to access it on js-api/note/[id].yaml, where [id] is the id of the note you created (probably 1).

Writing a simple JavaScript client

We'll put our javascript client in a module named noteresourcejs. The info file could look something like this:

    name = Notes Javascript
    description = Sample endpoint definition and javascript client implementation
    package = Notes example

    core = 6.x
    php = 5.2

The javascript module will do two things: implement a javascript client; and provide the notes endpoint in code.

Defining the endpoint in code

First, we need to inform the Services module that our module implements it's API. We do this by implementing hook_ctools_plugin_api() as shown in the following:

/**
 * Implements hook_ctools_plugin_api().
 */
function noteresourcejs_ctools_plugin_api($owner, $api) {
  if ($owner == 'services' && $api == 'services') {
    return array(
      'version' => 3,
      'file' => 'notresourcejs.services.inc', // Optional parameter to indicate the file name to load.
      'path' => drupal_get_path('module', 'noteresourcejs') . '/includes', // If specifying the file key, path is required.
    );
  }
}

Go to admin/structure/services and select Export for your Notes API endpoint. The code shown should be copy-pasted in a hook named hook_default_services_endpoint(), followed by the return value of return array($endpoint) (which is not provided in the Export code) or as shown in the example below:

    // noteresourcejs.module
    /**
     * Implements hook_default_services_endpoint().
     */
    function noteresourcejs_default_services_endpoint() {
      $endpoints = array();

      $endpoint = new stdClass;
      $endpoint->disabled = FALSE; /* Edit this to true to make a default endpoint disabled initially */
      $endpoint->name = 'notes_js';
      $endpoint->title = 'Note API';
      $endpoint->server = 'rest_server';
      $endpoint->path = 'js-api';
      $endpoint->authentication = array();
      $endpoint->resources = array(
        'note' => array(
          'alias' => '',
          'operations' => array(
            'create' => array(
              'enabled' => 1,
            ),
            'retrieve' => array(
              'enabled' => 1,
            ),
            'update' => array(
              'enabled' => 1,
            ),
            'delete' => array(
              'enabled' => 1,
            ),
            'index' => array(
              'enabled' => 1,
            ),
          ),
        ),
      );
      $endpoints[] = $endpoint;

      return $endpoints;
    }

Notice that we don't return the endpoint as it is. But, as with views, we return an array containing the endpoint.

The client

Our client is quite trivial and will consist of one js file and one css file. I'm not going to write them both in their entirety here, but rather provide an excerpt that illustrates how you can communicate with a REST server using JavaScript. See [notes.js](services-3.x-sample/blob/master/js/notes.js) and [notes.css](services-3.x-sample/blob/master/css/notes.css) for the full versions.

    // js/notes.js (excerpt)
    // use an absolute URL path to prevent this from be local to the current page:
    var noteapi = {
      'apiPath': '/js-api/note'
    };

    // REST functions.
    noteapi.create = function(note, callback) {
      $.ajax({
         type: "POST",
         url: this.apiPath,
         data: JSON.stringify({note: note}),
         dataType: 'json',
         contentType: 'application/json',
         success: callback
       });
    };

    noteapi.retreive = function(id, callback) {
      $.ajax({
        type: "GET",
        url: this.apiPath + '/' + id,
        dataType: 'json',
        success: callback
      });
    };

    noteapi.update = function(note, callback) {
      $.ajax({
         type: "PUT",
         url: this.apiPath + '/' + note.id,
         data: JSON.stringify({note: note}),
         dataType: 'json',
         contentType: 'application/json',
         success: callback
       });
    };

    noteapi.del = function(id, callback) {
      $.ajax({
         type: "DELETE",
         url: this.apiPath + '/' + id,
         dataType: 'json',
         success: callback
       });
    };

    noteapi.index = function (callback) {
      $.getJSON(this.apiPath, callback);
    };

Notice how we don't need to do anything odd to talk with our server. Everything maps to http verbs and a url, so there is no need for special client libraries.

The js and css is added in hook_init(), and will therefore be loaded on all pages in our Drupal install.

    // noteresourcejs.module
    /**
     * Implements hook_init().
     */
    function noteresourcejs_init() {
      drupal_add_css(drupal_get_path('module', 'noteresourcejs') . '/css/notes.css');
      drupal_add_js(drupal_get_path('module', 'noteresourcejs') . '/js/notes.js');
    }

By Example: Resource, Path, Arguments, Oy Vey!

Consider your Services API endpoint like a menu route. If you want a resource path like:
https://example.com/json_field/json_field/123/asdf
The API endpoint is "json_field" (the first one) and is configured in the .services.inc, the second "json_field" is the resource, defined in hook_services_resources, and the arguments are 123 and asdf, defined in hook_services_resources. The payload is still $data, and is also defined in hook_services_resources. See example:

/**
 * Implements hook_services_resources().
 */
function json_field_services_resources() {
  /**
   * NOTE TO SELF:
   * When creating a new route, json_field.services.inc needs to be kept up to date.
   */
  return array(
    'json_field' => array(
      'update' => array(
        'help' => 'Update JSON Field',
        'file' => array('type' => 'inc', 'module' => 'json_field', 'name' => 'resources/update'),
        'callback' => 'json_field_resource_update',
        'access callback' => 'user_access',
        'access arguments' => array('access observer api'),
        'args' => array(
          array(
            'name' => 'id',
            'optional' => FALSE,
            'source' => array('path' => 0),
            'type' => 'string',
            'description' => 'The Entity ID whose field to update',
          ),
          array(
            'name' => 'field',
            'optional' => FALSE,
            'source' => array('path' => 1),
            'type' => 'string',
            'description' => 'The JSON field name to be updated',
          ),
          array(
            'name' => 'data',
            'optional' => FALSE,
            'source' => 'data',
            'type' => 'string',
            'description' => 'The complete, updated JSON array of values as {"1Z":1431983436,"1E":1432586713}.',
          ),
        ),
      ),
    ),
  );
}

The key is the 'source' in the 'args' array:
'source' => array('path' => 0),
"The source of this defined argument will be found in the 0th position in the path.
'source' => array('path' => 1),
"The source of this defined argument will be found in the 1st position in the path.
'source' => 'data',
"The source of this defined argument will be found as the payload (without a variable name)."
The callback function json_field_resource_update() defined in ./resources/update.inc in the json_field module folder will have the following parameter structure:
function json_field_resource_update($entity_id = NULL, $field_name = NULL, $data = array()) {

Caveats / Troubleshooting

Given two REST Services APIs, even path-spaced accordingly (ie. /apiv1 and /api), two resource names will conflict.
Ex. /api/groups and /apiv1/groups
Symptom: In the admin/structure/services/list/[api path]/resources config screen, one of the conflicting names may have "array()" instead of the endpoint description text. Odd behavior on the client end can be expected: ex. cannot authenticate to the new resource because the original one's authentication is actually not satisfied.

Comments

gapple’s picture

One gotcha that took me a while to figure out is that you must set the access callback value to a defined function. Unlike with hook_menu(), using a boolean value will not bypass the access check and instead results in a PHP error, as services passes the value straight through to call_user_func_array().

services.runtime.inc - line 135

  // Call default or custom access callback
  if (call_user_func_array($controller['access callback'], $access_arguments) != TRUE) {
    // return 401 error
  }

In addition, resource definitions are cached in the database, so if you're making modifications be sure to clear the cache or remove entries matching services:%:resources so that the new values in code are used.

texas-bronius’s picture

Following this comment on clearing cache, here's an example how to quickly clear Services routes cache (instead of the normal Drupal cache-clear that can take 20-40 seconds on some sites)
drush ev "cache_clear_all('services:', 'cache', TRUE);"
or use your endpoint name (like "mobile_v1"):
drush ev "cache_clear_all('services:mobile_v1:', 'cache', TRUE);"
An alternative is to visit your Services REST endpoint's Resources tab in the browser, and just click Save without any changes to the form.

--
http://drupaltees.com
80s themed Drupal T-Shirts

Anonymous’s picture

Does anyone have info on PUT and DELETE support in browsers? I can't find a place where the code is handling the ajax type and I thought the last two weren't widely supported.

gapple’s picture

Only GET and POST are supported for HTML forms, so other requests will always require a javascript handler.
The jQuery documentation for .ajax() simply notes "Other HTTP request methods, such as PUT and DELETE, can also be used here, but they are not supported by all browsers." The standard for XMLHttpRequest specifies that conforming browsers must support GET, POST, HEAD, PUT, DELETE, and OPTIONS.

This Stack Overflow question has some information on browser support:
One mentions this article that tested Firefox 3, Opera 9, and IE 7 for support of alternate request types, and all should support PUT or DELETE requests. FF may have had support as far back as a 1.x version.
Another answer notes that Safari added support in either version 3 or 4.

I was unable to find a definitive answer as to whether IE 6 supports PUT and DELETE requests through the XMLHttp ActiveX Object

ardnet’s picture

I followed the instructions based on above, and the same tutorial in here: https://github.com/hugowetterberg/services-3.x-sample/blob/master/creati...
I already downloaded all the required module, which is:
- Services 6.x-3.0-rc1
- Input stream 6.x-1.0
- Autoload 6.x-2.1
- Rest Server 6.x-2.0-beta3
- Chaos tool suite 6.x-1.x-dev
I already download, the of noteresource module from here: https://github.com/hugowetterberg/services-3.x-sample
I also already implemented the patch of issue in Services module in here: http://drupal.org/node/1153968

But still, when I try post something from that javascript form, it says from the Firebug:
POST http://localhost:8888/drupal62/js-api/note 500 Internal Server Error

Am I missing something here?
or is it because I used http://localhost:8888/drupal62? (which is I don't think so)
Should I put something in here: http://localhost:8888/drupal62/admin/build/services/list/notes/resources
Should I allow permissions for noteresouce module for all users (tick all checkboxes for anonymous and authenticated)?

Thanks in advance.

paloma.jimenez’s picture

ei, but does it work for you when consuming the service, just consulting a resource? i don't know how to make it run! i'm lost... i have a node (id = 1) and i try to get into the node by rest, using firefox... so i do try to get into http://localhost/drupal6/services/rest/node/1 but i what i get is a 404 error. I'm a real noob, sorry!

ardnet’s picture

Hi paloma.jimenez
Sorry just replied, yes, it is kinda works for me for consuming the services, I eventually found out how to do that, but not all though.
First of all, assuming that you already added endpoint, you need to enable some of the resources by clicking link Edit Resources, then you will see list of Resources there.

I only understand some part of the resources, for example: tick "node.retrieve"

Then in your URL, you type this: http://something.com/[your endpoint]/node/retrieve
after that you will see list of node display there as XML.

If you need to display only certain node, then do this: http://something.com/[your endpoint]/node/[nid]

Hopefully that giving you some light.

Cheers

creemej’s picture

I tried everything, but I can't get it working for Drupal 7.x

All these give me error: 404 Not Found in stead of a white screen or another expected result.

http://localhost/drupal/js-api/
http://localhost/drupal/js-api/note/
http://localhost/drupal/js-api/notes/
http://localhost/drupal/notes/
http://localhost/drupal/js-api/note/1.yaml
http://localhost/drupal/js-api/notes/1.yaml
http://localhost/drupal/notes/1.yaml

Where can I find help on this or find another example?

***

Sorry, it was working from the start!
But I did not have "Clean URL" activated on the test site.

http://localhost/drupal/?q=js-api gives the white screen and not the error 404.

Oh yeah, I removed the obsolete function "db_fetch_object"

//noteresource.inc
function _noteresource_index($page, $parameters) {
  global $user;
  $notes = array();
  $res = db_query("SELECT * FROM {note} WHERE uid=:uid ORDER BY modified DESC", array(':uid' => $user->uid));
  /*while ($note = db_fetch_object($res)) {
    $notes[] = $note;
  }*/
  foreach ($res as $note) {
     $notes[] = $note;
  }
  return $notes;
}

//noteresource.module
function noteresource_get_note($id) {
  //return db_fetch_object(db_query("SELECT * FROM {note} WHERE id=:id", array(':id' => $id)));
  $result = db_query("SELECT * FROM {note} WHERE id=:id", array(':id' => $id));
  return $result->fetchObject();  
}

Maybe, this can be of help for other dummies...

kuson’s picture

Dear Druplers,

Please refer to my comments in http://drupal.org/node/1246470 , if you want to get the above code working in Drupal 7.7x

Thx.

flacoman91’s picture

Thank you. your example helped me a bunch!

jjustman’s picture

When setting up my own first 3.x service, I kept getting errors like "cannot find controller". I found myself asking the question, "how does a certain request map into the service definition I set? It took me a bit of digging into the code to get into the right frame of mind to understand how Services is deciding where and how to dispatch requests. A good API documentation for the services_resources callback really seems to be in order. While I don't feel qualified for that yet, here is some of what I've learned.

The first thing to remember is that the REST module is using the REST assumption that certain HTTP methods correspond to certain things. As you can see in the REST server code there is a simple algorithm that determines where to dispatch a request:

  • First, the code counts the number of "path" elements, i.e. the part after the resource name. This is called the "path count.
  • If the path count is php-false (e.g. zero) and the http method is not POST, the request always goes to the 'index' action you have set in your resource definition.
  • If the path count is exactly one, or if the method is POST and the path count is zero, the action is selected based on the http request method:
        $action_mapping = array(
          'GET' => 'retrieve', 
          'POST' => 'create', 
          'PUT' => 'update', 
          'DELETE' => 'delete',
        );
    

For other numbers of arguments, I haven't tried the code, but you should look at it yourself as it has some specific assumptions. For example for GET requests with more than 1 path-part, the code will look for a 'relationships' part of the resource definition. This probably corresponds to some REST practice that I haven't figured out yet. For my purposes, I decided to make my multi-argument GET methods use the "param" argument type.

eli’s picture

Man, I wish I'd seen this comment before tracing through the code myself and coming to the same conclusion. This documentation is rather lacking if you want to go at all beyond the basic CRUD functions shown.

Relationships appear to be strictly for returning additional information about a specific object. For example, you can do something like node/[nid]/files to get the files associated with a node.

The missing piece for me is the array key called 'actions' -- this is where you can add things besides Create, Index, Retrieve, Delete. See services/resources/system_resource.inc for an example. Note that you can only POST to actions.

I get why it's set up this way, but the documentation could be a lot better and I really think there should be a way to offer arbitrary GET handlers. For example, there's no good reason from a RESTful standpoint for system.variable_get to require a POST.

Vivek Panicker’s picture

Thanks for the explanation!!! Your comment is useful even today!

bsenftner’s picture

I wrote up and posted working examples of the above, with the following modifications:

  • I removed any database logic so the example code is demonstrating how to implement REST API logic only,
  • I added logic for Relationships and Targeted Actions to demonstrate implementing those.

Services 3 examples
Cheers!

aaronaverill’s picture

The example create method contains some error handling that could use improvement:

if (!isset($data->subject)) {
  return services_error('Missing note attribute subject', 406);      
}

This will set the HTTP response to 406, but unfortunately the $message is dropped and the response body is blank. Not good.

Best practices dictate you should return some useful information in the message body about the error. You can accomplish this by setting the third param ($data) of the services_error method. Here is a snippet of code I use:

function _process_services_error($message, $code) {
  $data = array('error' => array('code' => $code, 'message' => $message));
  services_error($message, $code, $data);
}

replace the call to services_error in the example to _process_services_error and your service call will contain the more useful response body (Content-Type: application/json):

{"error":{"code":406,"message":"Missing note attribute subject"}}

miqmago’s picture

Great post!!

I tried using XML-RPC, because I need android functionality.
I'm getting always this error: "XML-RPC server accepts POST requests only."
I'm pretty sure I'm sending POST request to the server:
I tried with a js:

noteapi.index = function (callback) {
  jQuery.ajax({
    type: "POST",
    url: this.apiPath + '/index', 
    success: callback,
  });
};

Also with firefox poster, same result.
Also tried (http://drupal.org/node/1056524) activating language by url to avoid drupal redirection. Nothing happens but always the same message..

Is there any drupal redirection I'm missing? If yes, how could discover/avoid the redirection?
In firebug console only appears "POST http://localhost/drupaltest/public_html/en/js-api/note/index"

Please, need help!!

slewazimuth’s picture

The example worked for me perfectly. But then again, I'm running Drupal 7 and wrote out my example to take advantage of a lot of the simplified changes to the architecture. I tested mine first with json then urlencoded response formats running under FireFox POSTER and then used "drupal_http_request" function client side so I could work faster and not have to bother with javascript as I tend to like things quick, direct and dirt simple. I noticed that although it was easy to do the CRUD, Actions, Targeted Actions and Relationsip style resources for the REST server and the dev version of the server shows you categories nicely listed under the appropriate resource, the definition module doesn't yet support the dev version but only the last official release.

Cyberflyer’s picture

Am I missing something or does schema unininstall do the same thing as schema install?

// noteresource.install
    /**
     * Implements hook_install().
     */
    function noteresource_install() {
      drupal_install_schema('noteresource');
    }

    /**
     * Implements hook_uninstall().
     */
    function noteresource_uninstall() {
      drupal_install_schema('noteresource');
    }

Just askin'

Never Know When to Quit.

slewazimuth’s picture

A schema describes the data sctructure in a database, or in the case of this example the structure of a table. Whether you want to install or uninstall, you need to have the structure defined. When doing an "uninstall" if the schema itself is defined then removing the items from the table can be checked against the "schema", just as it is when its installed.

When using Drupal 7 its good to see exactly what is going on and why. For example when examining the data description for the "create" method. The "name" of the item is "data" and the type is shown as "struct' with the source being "data". That means the resource expects the passing of arguments to come from the body content inside a "stucture" called "data". Since one of my request parsing formats chosen was "application/x-www-form-urlencoded" that meant the body of the data would be expected to contain a structure named "data' which would have keys named "subject", "note" and so on. That meant I could read the data passed as an array or cast it to an object ie. $data['data']['subject'] or (object)$data['data'] so I could assign $data=(object)$data['data'] which would now make $data->note now give the correct value as per the example. If you're using drupal 7, don't just take my word for it. Check it out for yourself on the actual data. A lot of the information that RESTful methods expect to come passed in arguments in the body of the method call is actually originating in headers. In fact the Firefox plug-in/extension "POSTER" is actually passing its data as a Cookie in a Cookie Header. Once again, don't take my word for it, check out the actual header data and see for yourself when using POSTER.

aaronaverill’s picture

There is a small improvement needed in your code. If for example the user requests a resource by putting the accept type in the GET url:

...{etc}.../resource.json

Your service will return data whose URI doesn't include this extension (eg: .../resource/1 instead of /resource/1.json). It would be better to include this extension.

The problem code is here:

'uri' => services_resource_uri(array('note', $data->id)),

I have written a small function to fix this, and also handle if you want to pass back URL params (for example to allow the user to request pages of data in the list function):


function _get_resource_uri($path, $params = null) {
  $uri = services_resource_uri($path);
  $request = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
  $dot_pos = strrpos($request,'.');
  if (!($dot_pos === FALSE)) $uri .= substr($request,$dot_pos);
  if (isset($params) && count($params) > 0) $uri.= '?' . http_build_query($params, '', '&');
  return $uri;
}

A more robust version would check the actual request extension against valid service accept types (json,xml,etc). This is an exercise left to the reader.

sfyn’s picture

In the current implementation of the rest_server, this will make your example inoperable, you cannot have the endpoint name be the same as the resource name you intend to call with it.

metow’s picture

Allright now how do we set the response type to json? I'm retrieving the fid of a node (say 450) from the database and I want to return the response as:
{fid:450} for example.

Right now what I'm doing is:

return $fid;

which returns a string of "450"

so how do I use JSONparse for the response?

sfyn’s picture

drupal_json_encode

In 6 this is simpy known as drupal_json.

bechtold’s picture

With this declaration there is an error:
'file' => array('file' => 'inc', 'module' => 'noteresource'),
it must be:
'file' => array('type' => 'inc', 'module' => 'noteresource'),

fuzzy76’s picture

This page does not fit with the definition of hook_services_resources() at http://drupalcode.org/project/services.git/blob/refs/heads/7.x-3.x:/docs...

texas-bronius’s picture

This documentation is great! However, while it's titled "Services 3.x" and only Drupal 7 is supported anymore (and I think this document cites that), would it make sense that all code samples here in D6 should be stripped out or updated for D7 only? Ex. the .info and the db_fetch_object( .. ).

--
http://drupaltees.com
80s themed Drupal T-Shirts

dnlmzw’s picture

... and don't see it reflected when you run your code, then remember to update: Administration / Structure / Services / Resources, as the values are cached and does not get re-checked when you do the AJAX call to the endpoint. Took me quite a while to figure out, hope you find it useful.

texas-bronius’s picture

Exactly what I had to learn the hard way yesterday, Daniel. Thanks for posting here. I have not tried it but suppose that this might not be the case if the endpoint were defined in code: I am not that far along that I have spun cycles exploring that yet.

Anyone seen:
"CSRF validation failed"
as a POST response in a REST client? If this is core out of box Services REST server functionality (which it appears to be on this later version), it should be mentioned here as well.

To work within this secure framework, the client should first request a token by hitting the endpoint at/services/session/token to get the CSRF token and then include that token in its POST request with X-CSRF-Token in the header*. Drupal/Services is associating the session name/id with this token to prevent cross site attacks. This functionality came I think with v3, and I think it's core Services.

* Thanks Joe Tremblay for cracking this one!

--
http://drupaltees.com
80s themed Drupal T-Shirts

ellishettinga’s picture

In my module I have two .module files
If both .module files have a hook_default_services_endpoint implemented the last one executed overwrites the first.
I ended up combining the hook_default_services_endpoint code of both modules in one .module file using the module_exists() function to check if extra endpoints should be created.

texas-bronius’s picture

I didn't figure out the file/path bit of ctools plugin when implemented as shown in the example, but following this example worked right away: The linked example prescribes putting the .inc in the same folder as your custom module, so no need to find the winning recipe with file and path in hook_ctools_plugin_api.

--
http://drupaltees.com
80s themed Drupal T-Shirts

texas-bronius’s picture

Oddly, my new Services REST Server provided by code export registered just fine, but after clear-cache, it just wouldn't come up (hitting the endpoint in the browser gave a Drupal 404). I clicked Save on any of the Services configuration screens for this endpoint, and voila, hitting the endpoint as before instead gave the 'Services Endpoint "EXAMPLE" has been setup successfully.' as expected.

Side note: The endpoint showed as "Overridden," but Export showed no discernable differences. I selected "Revert," and it is both back in Default, and the API still works.

--
http://drupaltees.com
80s themed Drupal T-Shirts

Winfred_Ma’s picture

1. The story:
I download the resource package a few days ago. As my drupal version is 7.28 I found a few problems. Now I share it, Maybe it can help some latters.

2. The problems:
(1) About db_fetch_object().
The function was deprecated. You can use db_query()->fetchAll() replace it.
(2) About noteresource_perm().
The function was deprecated. You can use noteresource_permission() replace it. Following is the code.

            /**
             * Implements noteresource_permission().
            */
            function noteresource_permission() {
                return array(
                    'note resource create'=> array('title' => t('noteresource create'),'description' => t('create'),),
                    'note resource view any note'=> array('title' => t('noteresource view any note'),'description' => t('view any note'),),
                    'note resource view own notes'=> array('title' => t('noteresource view own notes'),'description' => t('view own notes'),),
                    'note resource edit any note'=> array('title' => t('noteresource edit any note'),'description' => t('edit any notes'),),
                    'note resource edit own notes'=> array('title' => t('noteresource edit own notes'),'description' => t('edit own notes'),),
                    'note resource delete any note'=> array('title' => t('noteresource delete any note'),'description' => t('delete any note'),),
                    'note resource delete own notes'=> array('title' => t('noteresource delete own notes'),'description' => t('delete own note'),),
                );
            }
        
texas-bronius’s picture

Took me a long time to crack this nut, and not sure where it belongs to document it, but I wanted to share that in order to provide a simple file upload, I found most joy by processing the file in my custom callback simply by calling _file_resource_create_raw() (provided by the module's services/resources/file_resource.inc (already included, no need to include separately).

So far what I think is working is:

  • Custom endpoint accepts multipart/form-data
  • Form submission is both a file upload field and any additional text fields
  • Call the file field 'files[]' (plural and with brackets)
  • API endpoint resource declares all form data fields but not file field
  • File field is handled by $_FILES php superglobal (check out _file_resource_create_raw() source)

Ex. Custom endpoint callback does what it needs to do as normal and also includes

$uploaded_file = _file_resource_create_raw();

What I struggled with forever was what to call the file field: Call it "files[]" not just "files". The brackets, for handling multiple file uploads, make _file_resource_create_raw() work and not break!
Bam!

--
http://drupaltees.com
80s themed Drupal T-Shirts

Fernly’s picture

If you followed this tutorial and your endpoints are not appearing on the services page, this is what made it happen:

In hook_default_services_endpoint(), add the following line:

  ...
  $endpoint->authentication = array();
  $endpoint->server_settings = array(); // This one appeared to be crucial.
  $endpoint->resources = array(
  ...

Note: I was declaring a soap_server service.

---
Vaerenbergh.com - A Drupal developer's web page

dzungpham0703’s picture

I apply some patch for Drupal 7.x but when access user own note it show the error:
"Access denied for user user_name". I don't know why. Permission "view own notes" enabled.

Update bacause sql query has been update by fecthAll(), the permission handle must change to:
$note[0]->uid == $user->uid and it work fine