Drupal 8 - Create a REST Plugin to allow multiple entity nodes to be created or updated via POST verb

Previously, I have written a couple of different blog posts that show how to use Drupal 7 web services module to interact with other applications. Now, I have a very interesting project that requires a REST module plugin (for Drupal 8) to interact with existing custom applications.

The problem that I need to solve is to expose a REST end point that processes an array of entity nodes in JSON format (via POST). Below is an example that contains nodes that need to be updated or created in Drupal. The nid value indicates whether the node has already been created in Drupal or not.

[
   // existing entity  (Update)
   {
"nid" : [{"value" : "15"}],    
"type" : [{"target_id":"staff"}],
"title" : [{"value" : "Senior Programmer"}],
"field_staff_name" : [{"value" : "Peter Rabbit Sr."}]
   },
   // new entity (Create)
   {
"type" : [{"target_id":"staff"}],
"title" : [{"value" : "System Administrator"}],
"field_staff_name" : [{"value" : "Tom Kitten"}]
   }
   ...

   ...
]

Initially, I thought an existing end point (/node or /entity/node) can process an array of entity nodes, but what I have noticed so far is that it only accepts one entity node at a time. Because of this limitation, I need to build a function (in sync module) that iterates through the elements (node) of an array and make a separate POST call per each element. I don't think this is an elegant solution to my problem; so I have decided to build a REST plugin to create a post function that can handle an array of entity nodes.

Note that you need to use Drupal 8.2 or higher because of a minor change in REST Plugin. I will explain this later.

First, I am going to install Drupal Console, a very handy tool that automatically generates scaffold code for REST plugin. The installation is straightforward. Follow the steps below:

     %> curl https://drupalconsole.com/installer -L -o drupal.phar

     %> mv drupal.phar /usr/local/bin/drupal

     %> chmod +x /usr/local/bin/drupal


Second, run Drupal Console to generate a module that will hold the REST resource plugin:

     %> drupal generate:module

Then, follow the screen instructions. When you are prompted to add dependencies, add rest

Third, run Drupal Console to create the rest resource plugin

     %> drupal generate:Plugin:rest:resource

Follow the screen instructions.

  • When you are prompted to enter the resource name, enter MultipleNodesPostResource
  • When you are prompted to enter the url, enter /api/multinodespost (my sample end point).
  • When you are prompted to enter REST states you wish to implement, enter POST which is 1
The plug resource will be placed under your custom module location such as module/custom/your_module_name/src/Plugin/rest/resource. You can find a file called, "MultipleNodesPostResource.php". Before implementing post function to handle array of entity nodes, I have to change the Annotation lines as shown below:

/**
 * Provides a resource to get view modes by entity and bundle.
 *
 * @RestResource(
 *   id = "multiple_nodes_post_resource",
 *   label = @Translation("Multiple nodes post resource"),
 *   serialization_class = "",
 *   uri_paths = {
 *     "canonical" = "/api/multinodespost",
 *     "https://www.drupal.org/link-relations/create" = "/api/multinodespost"
 *   }
 * )
 */

The serialization_class annotation is used to ensure that the data passed to the post function is of a specified type, in this case, Entity\Node class. Prior to Drupal 8.2, I had to specify a Entity\Node class as shown below because it was mandatory.

* serialization_class = "Drupal\node\Entity\Node"

However, starting Drupal 8.2, the serialization class becomes optional, which makes my life easier to address the problem. To explain this, I need to discuss a little bit about how Drupal 8 rest module uses Symfony Serialization component to convert JSON string to Entity or vice versa.

Figure 1. (from Symfony Documentation)

The Symfony serialization process has two parts. The first part is to either convert JSON string to an array (decode) or convert an array to JSON string (encode). This is done by JsonEncoder, and in my case, the JSON string will be decoded to an array, which is said to be in Normalized form. The second part is to either convert an array to a Drupal entity node (denormalize) or vice versa (normalize). This conversion is done by EntityNormalizer. In my example, the normalized data (array) will be converted to Drupal entity nodes.

If the serialization_class (in plugin) is specified with Entity\Node class, the data ($data) passed to the post function will be of a single instance of Entity/Node type because the rest module will decode and denormalize JSON data (to Entity\Node) before passing it to the post function.

public function post($data) {
   ...
}

The problem is that the denormalize function only processes a single entity (normalized data); so, if the sync module passes a JSON array string, the denormalize function will fail. However, if the serialization_class is not specified, the data passed to the post function will be of normalized format (array) instead of Entity class type. In my case, the normalized data will be of two-dimensional array. Since the normalized data is in two-dimensional array, I can implement the post function to iterate through the elements of a two-dimensional array and convert each element (normalized data) to a Drupal Entity\Node.

Let's implement the post function. First, I have to include EntityNormailzer.

use Drupal\serialization\Normalizer\EntityNormalizer;
...

The post function should look like this:

/**
   * Responds to POST requests.
   *
   * Returns a list of bundles for specified entity.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\HttpException
   *   Throws exception expected.
   */
  public function post($data) {

    // You must to implement the logic of your REST Resource here.
    // Use current user after pass authentication to validate access.
    if (!$this->currentUser->hasPermission('access content')) {
      throw new AccessDeniedHttpException();
    }

    $enormalizer = new EntityNormalizer(\Drupal::entityManager());
    $nodes = array();

    foreach($data as $item) {
      $node = $enormalizer->denormalize($item, 'Drupal\node\Entity\Node');
      
      if(isset($node->nid->value))
      {

        $nodetmp = Node::load($node->nid->value);

        if(is_null($nodetmp)){
           // Reset nid & create a new node
           $node->nid->value = '';
           $node->save();
        }else{
          // Update existing node
          $nodetmp->set("title", $node->title->value);
          $nodetmp->save();
          $node = $nodetmp;
        }

      }else{
        // Create a new node
        $node->save();
      }

      $nodes[] = $node;
    }

    return new ResourceResponse(array("processed" => $nodes));
  }

I think the code above is self-explanatory, but let me point out a few things. The EntityNormalizer is used to convert a normalized data to a Drupal entity node, and the $nodes array is used to keep track of a converted entity nodes during the iteration. 

    $enormalizer = new EntityNormalizer(\Drupal::entityManager());
    $nodes = array();

    foreach($data as $item) {
      $node = $enormalizer->denormalize($item, 'Drupal\node\Entity\Node');
      ...

      $node->save();

The post function will send the response along with the list of nodes (stored in $nodes array) back to the sync module. It will be the confirmation of POST operation.

    return new ResourceResponse(array("processed" => $nodes));

I think the REST plugin implementation is pretty straightforward. I hope this post is helpful to understand how to create a plugin that handles multiple entity nodes in Drupal 8 :)


Comments

Popular Posts