Astuce [D7] Créer un webservice REST

Le fonctionnement d'un webservice REST est très proche de celui d'une page classique. On souhaite accéder à une ressource, la modifier, la supprimer, ...

On y accède via une requête HTTP, à laquelle on fournit des paramètres et/ou des données.

Voici les étapes à suivre pour créer un webservice REST dans Drupal, communicant en JSON.

URI

Pour que Drupal gère l'URL du webservice, il faut implémenter le hook_menu() habituel :

// my_module.module
 
/**
 * Implements hook_menu().
 */
function my_module_menu()
{
  $items = array();
 
  // Webservice de lecture d'un article
  $items['api/article/%'] = array(
    'title' => t('Read article'),
    'page callback' => 'my_module_ws_article_read',
    'file' => 'my_module.ws.inc,'
    'page arguments' => array(2),
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
  // Webservice de mise à jour d'un article
  $items['api/article/%/update'] = array(
    'title' => t('Update article'),
    'page callback' => 'my_module_ws_article_update',
    'file' => 'my_module.ws.inc,'
    'page arguments' => array(2),
    'access arguments' => array('access content'),
    'type' => MENU_CALLBACK,
  );
 
  return $items;
}

Explications :

  • On souhaite que les URL de tous les webservices commencent par api/.
  • On définit 2 URL, une pour lire un article et l'autre pour le mettre à jour.
  • Les webservices seront implémentés dans les fonctions my_module_ws_article_read() et my_module_ws_article_update() du fichier my_module.ws.inc.
  • Les deux webservices devront recevoir un nid valide en paramètre dans l'URL.

Page callback

Webservice en lecture

Si le premier webservice doit retourner le nid, le titre, l'URL et le contenu de l'article, il pourrait s"implémenter ainsi :

// my_module.ws.inc
 
/**
 * Retourne un article au format JSON.
 *
 * @param int $nid Nid de l'article
 * @return string Le nid, le titre, l'URL et le contenu de l'article, au format JSON
 */
function my_module_ws_article_read($nid) {
 
  global $language;
 
  $data = array();
 
  // Requête autorisées
  drupal_add_http_header('Access-Control-Allow-Origin', "*");
  drupal_add_http_header('Access-Control-Allow-Methods', "GET, OPTIONS");
  drupal_add_http_header('Access-Control-Allow-Headers', "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Auth-Token");
 
  // Format du flux de sortie
  drupal_add_http_header('Content-Type', 'application/json');
 
  if ($_SERVER['REQUEST_METHOD'] == 'GET') {
 
    $article = node_load($nid);
 
    if (!empty($article) && $article->type == 'article') {
 
      $view = node_view($article);
 
      $title = (!empty($article->title_field[$language->language])) ?  $article->title_field[$language->language][0]['value'] : '';
      $content = (!empty($view['field_content'])) ? render($view['field_content'])) : '';
 
      $data = array(
        'nid' => intval($nid),
        'url_alias' => drupal_get_path_alias('node/'. $nid),
        'title' => $title,
        'content' => $content
      );
    } else {
 
      http_response_code('404');
      $data['error'] = 'Article introuvable (nid: ' . $nid . ').';
    }
 
    echo drupal_json_encode($data);
  }
}

Explications :

  • On définit le type de requête attendue par le webservice (requêtes GET ou OPTIONS depuis n'importe quel origine), ainsi que le format de sortie (ici JSON).
  • On récupère le nœud dont le nid est en paramètre. S'il est valide, on prépare le rendu de l'affichage par défaut du nœud.
  • On récupère le titre de l'article depuis le nœud, et le rendu de son champ content depuis la vue.
  • On stocke le tout dans un tableau.
  • On transforme le tableau en JSON que l'on affiche.

Remarque :

Dans cet exemple, le webservices accepte les requêtes de type OPTIONS. Cela peut être nécessaire lors de l'utilisation de certains framework (ex: Sencha). Pour ces requêtes, le contenu n'a pas besoin d'être renvoyé, d'où la condition ($_SERVER['REQUEST_METHOD'] == 'GET').

Si vous n'en avez pas besoin il est tout à fait possible de l'enlever.

Webservice en écriture

Voici le code du second webservice, pour mettre à jour le titre et le contenu de l'article. Pour simplifier, on considère que toute la mise à jour de l'article est déportée dans une fonction _my_module_article_update($article, $title, $content) :

// my_module.ws.inc
 
/**
 * Met à jour un article.
 *
 * @param int $nid Nid de l'article
 * @return string 'OK' si la mise à jour a réussi, un message d'erreur sinon.
 */
function my_module_ws_article_update($nid)
{
  $data = array();
 
  // Requête autorisées
  drupal_add_http_header('Access-Control-Allow-Origin', "*");
  drupal_add_http_header('Access-Control-Allow-Methods', "PUT, OPTIONS");
  drupal_add_http_header('Access-Control-Allow-Headers', "Origin, X-Requested-With, Content-Type, Accept, Authorization, X-Auth-Token");
 
  // Format du flux de sortie
  drupal_add_http_header('Content-Type', 'application/json');
 
  $article = node_load($nid);
 
  if (!empty($article) && $article->type == 'article') {
 
    // Récupération du corps de la requête
    $request_body_json = file_get_contents('php://input');
    $request_params = json_decode($request_body_json);
    $title = isset($request_params->title) ? $request_params->title : null;
    $content = isset($request_params->content) ? $request_params->content : null;
 
    if ($title != null || $content != null) {
 
      // Mise à jour de l'article
      $status_msg = _my_module_article_update($article, $title, $content);
      
      // Message de retour (ex: 'OK' si réussite, message d'erreur sinon)
      $data['content'] = $status_msg;
      
    } else {
 
      http_response_code('400');
      $data['error'] = 'Les données de mise à jour sont invalides, ou le json est mal formé (' . $request_params . ').';
    }
  } else {
 
    http_response_code('404');
    $data['error'] = 'Article introuvable (nid: ' . $nid . ').';
  }
 
  echo drupal_json_encode($data);
}

Explications :

  • On récupère le corps de la requête qui doit contenir les informations de mise à jour au format JSON. Exemple :
{
    "title": "Mon article",
    "content": "<p>Le contenu HTML de mon article</p>"
}
  • On vérifie que le JSON est valide et contient toutes les données.
  • On met à jour l'article

Remarques :

  • Il serait judicieux d'ajouter des logs dans ce webservice, en cas d'erreur d'une part, mais également en cas de réussite, puisqu'un contenu a été modifié et qu'il y a eu écriture dans la base.
  • L'accès à tout webservice en écriture devrait également être protégé. Dans l'exemple, on autorise toutes les requêtes quelle que soit leur origine. Il est important d'ajouter un filtrage sur les IP autorisées à appeler ce webservice, par exemple dans la configuration d'Apache, ou mieux, une authentification préliminaire (ex: oAuth).