/restful

RESTful best practices for Drupal

Primary LanguagePHP

Build Status

RESTful best practices for Drupal

This module achieves a practical RESTful for Drupal following best practices.

Concept

The following also describes the difference between other modules such as RestWs and Services Entity.

  • Restful module requires explicitly declaring the exposed API. When enabling the module nothing will happen until the implementing developer will declare it
  • Instead of exposing resources by entity type (e.g. node, taxonomy term), Restful cares about bundles. So for example you may expose the Article content type, but not the Page content type
  • The exposed properties need to be explicitly declared. This allows a clean output without Drupal's internal implementation leaking out. This means the consuming client doesn't need to know if an entity is a node or a term, nor will they be presented with the field_ prefix
  • One of the core features is versioning. While it's debatable if this feature is indeed a pure REST, we believe it's a best practice one
  • It has configurable output formats. It ships with JSON and XML as examples. HAL+JSON is the recommended default.
  • Audience is developers and not site builders
  • Provide a key tool for a headless Drupal. See the AngularJs form example module.

Module dependencies

API via Drupal

Assuming you have enabled the RESTful example module

Getting handlers

// Get handler v1.0
$handler = restful_get_restful_handler('articles');

// Get handler v1.1
$handler = restful_get_restful_handler('articles', 1, 1);

Create and update an entity

$handler = restful_get_restful_handler('articles');
// POST method, to create.
$result = $handler->post('', array('label' => 'example title'));
$id = $result['id'];

// PATCH method to update only the title.
$request['label'] = 'new title';
$handler->patch($id, $request);

View an entity

By default the RESTful module will expose the ID, label and URL of the entity. You probably want to expose more than that. To do so you will need to implement the publicFieldsInfo method defining the names in the output array and how those are mapped to the queried entity. For instance the following example will retrieve the basic fields plus the body, tags and images from an article node. The RESTful module will know to use the MyRestfulPlugin class because your plugin definition will say so.

class MyArticlesResource extends \RestfulEntityBase {

  /**
   * Overrides \RestfulEntityBase::publicFieldsInfo().
   */
  public function publicFieldsInfo() {
    $public_fields = parent::publicFieldsInfo();

    $public_fields['body'] = array(
      'property' => 'body',
      'sub_property' => 'value',
    );

    $public_fields['tags'] = array(
      'property' => 'field_tags',
      'resource' => array(
        'tags' => 'tags',
      ),
    );

    $public_fields['image'] = array(
      'property' => 'field_image',
      'process_callbacks' => array(
        array($this, 'imageProcess'),
      ),
      // This will add 3 image variants in the output.
      'image_styles' => array('thumbnail', 'medium', 'large'),
    );

    return $public_fields;
  }

}
// Handler v1.0
$handler = restful_get_restful_handler('articles');
// GET method.
$result = $handler->get(1);

// Output:
array(
  'id' => 1,
  'label' => 'example title',
  'self' => 'https://example.com/node/1',
);

// Handler v1.1 extends v1.0, and removes the "self" property from the
// exposed properties.
$handler = restful_get_restful_handler('articles', 1, 1);
$result = $handler->get(1);

// Output:
array(
  'id' => 1,
  'label' => 'example title',
);

Filtering fields

Using the ?fields query string, you can declare which fields should be returned.

$handler = restful_get_restful_handler('articles');

// Define the fields.
$request['fields'] = 'id,label';
$result = $handler->get(2, $request);

// Output:
array(
  'id' => 2,
  'label' => 'another title',
);

Image derivatives

Many client side technologies have lots of problems resizing images to serve them optimized and thus avoiding browser scaling. For that reason the RESTful module will let you specify an array of image style names to get an array of image derivatives for your image fields. Just add an 'image_styles' key in your public field info (as shown above) with the list of styles to use and be done with it.

List entities

$handler = restful_get_restful_handler('articles');
$result = $handler->get();

// Output:
array(
  'data' => array(
    array(
      'id' => 1,
      'label' => 'example title',
      'self' => 'https://example.com/node/1',
    );
    array(
      'id' => 2,
      'label' => 'another title',
      'self' => 'https://example.com/node/2',
    );
  ),
);

Sort

You can sort the list of entities by multiple properties. Prefixing the property with a dash (-) will sort is in a descending order. If no sorting is specified the default sorting is by the entity ID.

$handler = restful_get_restful_handler('articles');

// Define the sorting by ID (descending) and label (ascending).
$request['sort'] = '-id,label';
$result = $handler->get('', $request);

// Output:
array(
  'data' => array(
    array(
      'id' => 2,
      'label' => 'another title',
      'self' => 'https://example.com/node/2',
    ),
    array(
      'id' => 1,
      'label' => 'example title',
      'self' => 'https://example.com/node/1',
    ),
  ),
);

Filter

RESTful allows filtering of a list.

$handler = restful_get_restful_handler('articles');
// Single value property.
$request['filter'] = array('label' => 'abc');
$result = $handler->get('', $request);

Autocomplete

By passing the autocomplete query string in the request, it is possible to change the normal listing behavior into autocomplete.

The following is the API equivilent of https://example.com?autocomplete[string]=foo&autocomplete[operator]=STARTS_WITH

$handler = restful_get_restful_handler('articles');

$request = array(
  'autocomplete' => array(
    'string' => 'foo',
    // Optional, defaults to "CONTAINS".
    'operator' => 'STARTS_WITH',
  ),
);

$handler->get('', $request);

API via URL

View an Article

# Handler v1.0
curl https://example.com/api/articles/1 \
  -H "X-API-Version: v1.0"
# or
curl https://example.com/api/v1.0/articles/1

# Handler v1.1
curl https://example.com/api/articles/1 \
  -H "X-API-Version: v1.1"
# or
curl https://example.com/api/v1.1/articles/1

View multiple Articles at once

# Handler v1.1
curl https://example.com/api/articles/1,2 \
  -H "X-API-Version: v1.1"

Filtering fields

Using the ?fields query string, you can declare which fields should be returned.

# Handler v1.0
curl https://example.com/api/v1/articles/2?fields=id

Returns:

{
  "data": [{
    "id": "2",
    "label": "Foo"
  }]
}

Filter

RESTful allows filtering of a list.

# Handler v1.0
curl https://example.com/api/v1/articles?filter[label]=abc

Authentication providers

Restful comes with cookie, base_auth (user name and password in the HTTP header) authentications providers, as well as a "RESTful token auth" module that has a token authentication provider.

Note: if you use cookie-based authentication then you also need to set the HTTP X-CSRF-Token header on all writing requests (POST, PUT and DELETE). You can retrieve the token from /api/session/token with a standard HTTP GET request.

See this AngularJs example that shows a login from a fully decoupled web app to a Drupal backend.

# (Change username and password)
curl -u "username:password" https://example.com/api/login

# Response has access token.
{"access_token":"YOUR_TOKEN"}

# Call a "protected" with token resource (Articles resource version 1.3 in "Restful example")
curl https://example.com/api/v1.3/articles/1?access_token=YOUR_TOKEN

Error handling

While a PHP Exception is thrown when using the API via Drupal, this is not the case when consuming the API externally. Instead of the exception a valid JSON with code, message and description would be returned.

The RESTful module adheres to the Problem Details for HTTP APIs draft to improve DX when dealing with HTTP API errors. Download and enable the Advanced Help module for more information about the errors.

For example, trying to sort a list by an invalid key

curl https://example.com/api/v1/articles?sort=wrong_key

Will result with an HTTP code 400, and the following JSON:

{
  'type' => 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.1',
  'title' => 'The sort wrong_key is not allowed for this path.',
  'status' => 400,
  'detail' => 'Bad Request.',
}

Sub-requests

It is possible to create multiple referencing entities in a single request. A typical example would be a node referencing a new taxonomy term. For example if there was a taxonomy reference or entity reference field called field_tags on the Article bundle (node) with an articles and a Tags bundle (taxonomy term) with a tags resource, we would define the relation via the RestfulEntityBase::publicFieldsInfo()

public function publicFieldsInfo() {
  $public_fields = parent::publicFieldsInfo();
  // ...
  $public_fields['tags'] = array(
    'property' => 'field_tags',
    'resource' => array(
      'tags' => 'tags',
    ),
  );
}

And create both entities with a single request:

$handler = restful_get_restful_handler('articles');
$request = array(
  'label' => 'parent',
  'body' => 'Drupal',
  'tags' => array(
    array(
      // Create a new term.
      'label' => 'child1',
    ),
    array(
      // PATCH an existing term.
      'label' => 'new title by PATCH',
    ),
    array(
      '__application' => array(
        'method' => \RestfulInterface::PUT,
      ),
      // PUT an existing term.
      'label' => 'new title by PUT',
    ),
  ),
);

$handler->post('', $request);

Output formats

The RESTful module outputs all resources by using HAL+JSON encoding by default. That means that when you have the following data:

array(
  array(
    'id' => 2,
    'label' => 'another title',
    'self' => 'https://example.com/node/2',
  ),
  array(
    'id' => 1,
    'label' => 'example title',
    'self' => 'https://example.com/node/1',
  ),
);

Then the following output is generated (using the header ContentType:application/hal+json; charset=utf-8):

{
  "data": [
    {
      "id": 2,
      "label": "another title",
      "self": "https:\/\/example.com\/node\/2"
    },
    {
      "id": 1,
      "label": "example title",
      "self": "https:\/\/example.com\/node\/1"
    }
  ],
  "count": 2,
  "_links": []
}

You can change that to be anything that you need. You have a plugin that will allow you to output XML instead of JSON in the example module. Take that example and create you custom module that contains the formatter plugin the you need (maybe you need to output JSON but following a different data structure, you may even want to use YAML, ...). All that you will need is to create a formatter plugin and tell your restful resource to use that in the restful plugin definition:

$plugin = array(
  'label' => t('Articles'),
  'resource' => 'articles',
  'description' => t('Export the article content type in my cool format.'),
  ...
  'formatter' => 'my_formatter', // <-- The name of the formatter plugin.
);

Changing the default output format.

If you need to change the output format for everything at once then you just have to set a special variable with the name of the new output format plugin. When you do that all the resources that don't specify a 'formatter' key in the plugin definition will use that output format by default. Ex:

variable_set('restful_default_output_formatter', 'my_formatter');

Cache layer

The RESTful module is compatible and leverages the popular Entity Cache module and adds a new cache layer on its own for the rendered entity. Two requests made by the same user requesting the same fields on the same entity will benefit from the render cache layer. This means that no entity will need to be loaded if it was rendered in the past under the same conditions.

Developers have absolute control where the cache is stored and the expiration for every resource, meaning that very volatile resources can skip cache entirely while other resources can have its cache in MemCached or the database. To configure this developers just have to specify the following keys in their restful plugin definition.

Rate Limit

RESTful provides rate limit functionality out of the box. A rate limit is a way to protect your API service from flooding, basically consisting on checking is the number of times an event has happened in a given period is greater that the maximum allowed.

Rate Limit events

You can define your own rate limit events for your resources and define the limit an period for those, for that you only need to create a new rate_limit CTools plugin and implement the isRequestedEvent method. Every request the isRequestedEvent will be evaluated and if it returns true that request will increase the number of hits -for that particular user- for that event. If the number of hits is bigger than the allowed limit an exception will be raised.

Two events are provided out of the box: the request event -that is always true for every request- and the global event -that is always true and is not contained for a given resource, all resources will increment the hit counter-.

This way, for instance, you could define different limit for read operations than for write operations by checking the HTTP method in isRequestedEvent.

Configuring your Rate Limits

You can configure the declared Rate Limit events in every resource by providing a configuration array. The following is taken from the example resource articles 1.4 (articles__1_4.inc):

'rate_limit' => array(
    // The 'request' event is the basic event. You can declare your own events.
    'request' => array(
      'event' => 'request',
      // Rate limit is cleared every day.
      'period' => new \DateInterval('P1D'),
      'limits' => array(
        'authenticated user' => 3,
        'anonymous user' => 2,
        'administrator' => \RestfulRateLimitManager::UNLIMITED_RATE_LIMIT,
      ),
    ),
  ),
…

As you can see in the example you can set the rate limit differently depending on the role of the visiting user.

Since the global event is not tied to any resource the limit and period is specified by setting the following variables:

  • restful_global_rate_limit: The number of allowed hits. This is global for all roles.
  • restful_global_rate_period: The period string compatible with \DateInterval.

Documenting your API

It is of most importance to document your API, this is why the RESTful module provides a way to comprehensively document your resources and endpoints. This documentation can be accessed through the HTTP methods and through extending modules. The API will be documented both for humans and for machine consumption, allowing client implementations to know about the API without explicit programming.

Documenting your resources.

A resource can will be documented in the plugin definition using the 'label' and 'description' keys:

$plugin = array(
  // This is the human readable name of the resource.
  'label' => t('User'),
  // Use de description to provide more extended information about the resource.
  'description' => t('Export the "User" entity.'),
  'resource' => 'users',
  'class' => 'RestfulEntityBaseUser',
  ...
);

This should not include any information about the endpoints or the allowed HTTP methods on them, since those will be accessed directly on the aforementioned endpoint. This information aims to describe what the accessed resource represents.

To access this information just use the discovery resource at the api homepage:

# List resources
curl -u user:password https://example.org/api

Documenting your fields.

When declaring your public field and their mappings you will have the opportunity to also provide information about the field itself. This includes basic information about the field, information about the data the field holds and about how to generate a form element in the client side for this particular field. By declaring this information a client can write an implementation that reads this information and provide form elements for free via reusable form components.

$public_fields['text_multiple'] = array(
  'property' => 'text_multiple',
  'discovery' => array(
    // Basic information about the field for human consumption.
    'info' => array(
      // The name of the field. Defaults to: ''.
      'name' => t('Text multiple'),
      // The description of the field. Defaults to: ''.
      'description' => t('This field holds different text inputs.'),
    ),
    // Information about the data that the field holds. Typically used to help the client to manage the data appropriately.
    'data' => array(
      // The type of data. For instance: 'int', 'string', 'boolean', 'object', 'array', ... Defaults to: NULL.
      'type' => 'string',
      // The number of elements that this field can contain. Defaults to: 1.
      'cardinality' => FIELD_CARDINALITY_UNLIMITED,
      // Avoid updating/setting this field. Typically used in fields representing the ID for the resource. Defaults to: FALSE.
      'read_only' => FALSE,
    ),
    'form_element' => array(
      // The type of the input element as in Form API. Defaults to: NULL.
      'type' => 'textfield',
      // The default value for the form element. Defaults to: ''.
      'default_value' => '',
      // The placeholder text for the form element. Defaults to: ''.
      'placeholder' => t('This is helpful.'),
      // The size of the form element (if applies).
      'size' => 255, Defaults to: NULL.
      // The allowed values for form elements with a limited set of options. Defaults to: NULL.
      'allowed_values' => NULL,
    ),
  ),
);

This is the default set of information provided by RESTful. You can add your own information to the 'discovery' property and it will be exposed as well.

To access the information about an specific endpoint just make an OPTIONS call to it. You will get the field information in the body, the information about the available output formats and the permitted HTTP methods will be contained in the corresponding headers.

Auto-documented fields.

If your resource is an entity then some of this information will be populated for you out of the box, without you needing to do anything else. This information will be derived from the Entity API and Field API. The following will be populated automatically:

  • $discovery_info['info']['label']
  • $discovery_info['info']['description']
  • $discovery_info['data']['type']
  • $discovery_info['data']['required']
  • $discovery_info['form_element']['default_value']
  • $discovery_info['form_element']['allowed_values'] for text lists.

Modules integration

Credits