Skip to main content

Decoupled Drupal - JSON-RPC To The Rescue

JSON-RPC is a great Drupal module filling when from a decoupled application you e.g. need to extract or manipulate data in a custom way or more importantly run code in your content providing Drupal installation.

Drupal JSON-RPC

Published on 2019-07-12 at 18:43

Drupal JSON-RPC decoupled

Oftentimes when you design a decoupled application with Drupal as your data provider, you run into some cases, where the data available to you from your e.g. JSON:API is not full. E.g. you want your app user to fetch or manipulate some of the configuration on the backend like the site name. Or even allow your consumer to do administrative tasks such as clearing the cache, adding a permission to a role?

The JSON-RPC module allows you to do just that. Let's have a look on how to use it and create a custom plugin.

What is JSON-RPC and how does it work in Drupal

Wikipedia has a very good first sentence on what JSON-RPC is:

JSON-RPC is a remote procedure call protocol encoded in JSON. It is a very simple protocol (and very similar to XML-RPC), defining only a few data types and commands. JSON-RPC allows for notifications (data sent to the server that does not require a response) and for multiple calls to be sent to the server which may be answered out of order.

The consumer is able to send a request to your Drupal site, which is implementing the JSON-RPC protocol. The consumer passes a JSON-object to the server, specifying

  • the version of the protocol, should be "2.0"
  • which method will be called on the remote machine
  • which parameter should be passed, if any
  • an id, so that the response can be matched properly

Here is an example of such a request from the jsonrpc_core-module:

{
	"jsonrpc": "2.0",
	"method": "plugins.list",
	"params": {
		"service": "plugin.manager.link_relation_type",
		"page": {"limit": 15, "offset": 1}
	},
	"id": "relation-types"
}

The request will be handled by a plugin provided by the Drupal JSON-RPC module, which has to annotate an id (corresponding to the "method"-key in the request), parameters (mapped to the "params"-key), access restriction (a Drupal permission) and a description.

The discovered class will call its' execute-method, where the provided parameters will be passed. Each parameter can be annotated with a key of schema (where it will be passed as is) or factory (which allows the parameter to be processed and transformed through a class defined by the factory key). Finally the output of the "execute" is being handled by the outputSchema-method, so that the JSON-schema of the response is defined correctly.

The project page of the module has a Postman collection with some examples for requests for the plugins provided by the jsonrpc_core module, definitely with checking these out.

The response of the request above would look something like this (in this example it is cut-off for simplicity):

{
    "jsonrpc": "2.0",
    "id": "relation-types",
    "result": {
        "add-page": {
            "class": "Drupal\\Core\\Http\\LinkRelationType",
            "uri": "https:\/\/drupal.org\/link-relations\/add-page",
            "description": "A page where a resource of this type and related types can be created.",
            "provider": "core",
            "id": "add-page"
        },
        ...
        "alternate": {
            "class": "Drupal\\Core\\Http\\LinkRelationType",
            "description": "Refers to a substitute for this context",
            "reference": "[http:\/\/www.w3.org\/TR\/html5\/links.html#link-type-alternate]",
            "provider": "core",
            "id": "alternate"
        }
    }
}

Create own plugin - allow configuration to be imported via a JSON-RPC call

Let's make our hands dirty and create our own plugin that will make Drupal import the active configuration, basically the same as running drush cim -y from the command line. We need a custom module for that, let's call it drupov_jsonrpc. Add the basic drupov_jsonrpc.info.yaml file:

name: Drupov JSON-RPC
description: Drupal 8 JSON-RPC plugin demo module.
package: Custom
core: 8.x
type: module

With that the module can be enabled. Let's create our plugin - for that we need a file src/Plugin/jsonrpc/Method/ConfigImport.php. The ConfigImport class will need to extend JsonRpcMethodBase and provide its' annotation - no params needed to be provided for our action:

/**
 * Import config.
 *
 * @JsonRpcMethod(
 *   id = "config.import",
 *   usage = @Translation("Config import."),
 *   access = {"administer site configuration"},
 * ),
 */
class ConfigImport extends JsonRpcMethodBase {

The outputSchema is simple as we return only a message to the consumer. Our execute() method will look like this (logic is borrowed from core/lib/Drupal/Core/Config/Importer/ConfigImporterBatch.php)

  /**
   * {@inheritdoc}
   */
  public function execute(ParameterBag $params) {
    $storage_comparer = new StorageComparer($this->syncStorage, $this->activeStorage);

    $config_importer = new ConfigImporter(
      $storage_comparer,
      $this->eventDispatcher,
      $this->configManager,
      $this->lock,
      $this->typedConfigManager,
      $this->moduleHandler,
      $this->moduleInstaller,
      $this->themeHandler,
      $this->getStringTranslation()
    );
    if ($config_importer->alreadyImporting()) {
      $this->message .= $this->t('Another request may be synchronizing configuration already.');
    }
    else {
      try {
        $sync_steps = $config_importer->initialize();
        foreach ($sync_steps as $sync_step) {
          $config_importer->doSyncStep($sync_step, $context);
        }
        $this->message = $this->t( 'The configuration was imported successfully');
      } catch (ConfigImporterException $e) {
        $this->message .= $this->t('The configuration cannot be imported because it failed validation for the following reasons: ');

        foreach ($config_importer->getErrors() as $message) {
          $this->message .= $message;
        }
      }

    }

    return $this->message;
  }

  /**
   * {@inheritdoc}
   */
  public static function outputSchema() {
    return ['type' => 'string'];
  }

You can find the full code at this GitHub-repository. Enable the module in your Drupal installation, make some changes to the config (e.g. change the site name) and send a POST-request to /jsonrpc-route of your Drupal site.

{
	"jsonrpc": "2.0",
	"method": "config.import",
	"id": "config.import"
}

That's it. Hopefully this will be useful when you need to make your decoupled Drupal backend perform a task that is not tied to the entity eco-system.

You can find the full module in this GitHub repo.

Share this