Typed Data API in Drupal 8

Posted: Aug 22, 2019
By: Igor
Category: Drupal

In this blog post, you can find some useful information about the Typed Data API in Drupal 8 and several examples. The examples can be handy for understanding how Typed Data API works. The Typed Data API was created to provide developers with a consistent way of interacting with data in different ways. It allows you to interact with the actual data, but it also provides the means of fetching more metadata about the actual entities.

I have used the typed data API on many Drupal projects, and they all were related to integration with external services. With the typed data's help, I have created wrappers for the service response data, and it allowed me to validate and interact with data through the OOP way.

Here I want to show you similar and more straightforward examples of this approach. Instead of external services, we use a form with a text area, and a submit button. In the text area, we can insert JSON data, and on submit an event, the JSON should be validated.

If the data is invalid, we should see a message with validation errors. Otherwise, nodes should be created based on the data from JSON.

Here's how our incoming data look like:

{
  "results":[  
    {  
      "title":"Demo article 1",
      "body":"Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.",
      "author":1
    },
    {
      "title":"Demo article 2",
      "body":"Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium.",
      "author":1
    },
    {
      "title":"Demo article 3",
      "body":"Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim.",
      "author":1
    }
  ],
  "status":"OK",
  "command":"generate-article"
}

Based on the structure of data, we need to create Complex Data Definitions

I want to remind you that the typed data has three interfaces: Primitive, List, and Complex. We use all of these interfaces for a custom data type.

Complex Data structure
  • Complex data 1 – wrapper for the whole response. It contains two primitive data items (status and command) and one set of data (results).
  • List data - list of similar data types items. It can be primitive or complex data. In our case on the list, we have Complex data 2.
  • Complex data 2 – wrapper for a result item. It contains three primitive data items (title, body, and author).

Let's create our custom module (tp_demo) with new complex data definitions for this response. 
tp_demo/tp_demo.info.yml

name: tp_demo
type: module
description: Typed data example module
core: 8.x
package: Custom

First of all, we need to create a complex data definition for Complex data 1, let's call it ResponseDefinition. It contains three properties – status, command, results.

tp_demo/src/TypedData/Definition/ResponseDefinition.php

<?php

namespace Drupal\tp_demo\TypedData\Definition;

use Drupal\Core\TypedData\ComplexDataDefinitionBase;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\ListDataDefinition;

/**
 * Demo Api Response Definition.
 */
class ResponseDefinition extends ComplexDataDefinitionBase {

  /**
   * {@inheritdoc}
   */
  public function getPropertyDefinitions() {
    if (!isset($this->propertyDefinitions)) {
      $info = &$this->propertyDefinitions;

      $info['status'] = DataDefinition::create('string')
        ->setRequired(TRUE)
        ->setLabel('status')
        ->addConstraint('AllowedValues', ['OK']);
      $info['command'] = DataDefinition::create('string')
        ->setRequired(TRUE)
        ->setLabel('command');
      $info['results'] = ListDataDefinition::create('demo_results')
        ->setLabel('results')
        ->addConstraint('NotNull');
    }
    return $this->propertyDefinitions;
  }

}

All properties have their constraints:

  • Status – is required, and has only one allowed value 'OK'
  • Command – required
  • Results – list of data and can't be NULL.

If these conditions not met, then we get an error at the validation step.

Pay attention to results property. It is ListDataDefinition of demo_results custom data type. demo_results is our Complex Data 2 from the image:

tp_demo/src/TypedData/Definition/ResultsDefinition.php

<?php

namespace Drupal\tp_demo\TypedData\Definition;

use Drupal\Core\TypedData\ComplexDataDefinitionBase;
use Drupal\Core\TypedData\DataDefinition;

/**
 * Demo Api Response results definition.
 */
class ResultsDefinition extends ComplexDataDefinitionBase {

  /**
   * {@inheritdoc}
   */
  public function getPropertyDefinitions() {
    if (!isset($this->propertyDefinitions)) {
      $info = &$this->propertyDefinitions;

      $info['title'] = DataDefinition::create('string')
        ->setLabel('title')
        ->addConstraint('NotNull');
      $info['body'] = DataDefinition::create('string')
        ->setLabel('body');
      $info['author'] = DataDefinition::create('integer')
        ->setRequired(TRUE)
        ->setLabel('author');
    }
    return $this->propertyDefinitions;
  }

}

ResultsDefinition contains only primitive data like string and integer. It also has its constraints for validation.

For each data definition, we need to create a DataType plugin, and it extends Map – a base data type for complex data. 

tp_demo/src/Plugin/DataType/ResponseData.php

<?php

namespace Drupal\tp_demo\Plugin\DataType;

use Drupal\Core\TypedData\Plugin\DataType\Map;

/**
 * Slide processor response `Convert png` data type.
 *
 * @DataType(
 * id = "demo_response",
 * label = @Translation("Demo response"),
 * definition_class = "\Drupal\tp_demo\TypedData\Definition\ResponseDefinition"
 * )
 */
class ResponseData extends Map {

}

The critical part here is the Annotation. It contains all the information about the data type. 

I left this class empty, but you can add any methods for the easy use of a custom data type.

tp_demo/src/Plugin/DataType/ResultsData.php

<?php

namespace Drupal\tp_demo\Plugin\DataType;

use Drupal\Core\TypedData\Plugin\DataType\Map;

/**
 * Slide processor response `Convert png` data type.
 *
 * @DataType(
 * id = "demo_results",
 * label = @Translation("Demo results"),
 * definition_class = "\Drupal\tp_demo\TypedData\Definition\ResultsDefinition"
 * )
 */
class ResultsData extends Map {

}

Custom data types are ready for use, so the next step is a form for testing this data. First, let's create a route for the page with the form:

tp_demo/tp_demo.routing.yml
 

tp_demo.tp_demo_form:
  path: '/tp_demo'
  defaults:
    _form: '\Drupal\tp_demo\Form\TpDemoForm'
    _title: 'TpDemoForm'
  requirements:
    _access: 'TRUE'

tp_demo/src/Form/TpDemoForm.php

<?php

namespace Drupal\tp_demo\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\Entity\Node;
use Drupal\tp_demo\TypedData\Definition\ResponseDefinition;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\TypedData\TypedDataManager;

/**
 * Class TpDemoForm.
 */
class TpDemoForm extends FormBase {

  /**
   * Drupal\Core\TypedData\TypedDataManager definition.
   *
   * @var \Drupal\Core\TypedData\TypedDataManager
   */
  protected $typedDataManager;

  /**
   * Constructs a new TpDemoForm object.
   */
  public function __construct(TypedDataManager $typed_data_manager) {
    $this->typedDataManager = $typed_data_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('typed_data_manager')
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'tp_demo_form';
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['json_data'] = [
      '#type' => 'textarea',
      '#title' => $this->t('JSON data'),
    ];
    $form['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Submit'),
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    // Create Response Data Type instance.
    $definition = ResponseDefinition::create('demo_response');
    $response = $this->typedDataManager->create($definition);
    // Convert json to array.
    $raw_response = json_decode($form_state->getValue('json_data'), TRUE);
    $response->setValue($raw_response);
    // Validate inserted data.
    $violations = $response->validate();
    if ($violations->count() != 0) {
      $form_state->setErrorByName('json_data', $this->t('Json data is invalid'));
      // If we have validation errors - print message with error.
      foreach ($violations as $violation) {
        // Print validation errors.
        drupal_set_message($this->t('@message (@property = @value)', [
          '@message' => $violation->getMessage(),
          '@property' => $violation->getPropertyPath(),
          '@value' => $violation->getInvalidValue(),
        ]), 'error');
      }
    }
    else {
      // Move response object to form_state storage.
      $form_state->setStorage(['response' => $response]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $storage = $form_state->getStorage();
    if (!isset($storage['response'])) {
      drupal_set_message($this->t('Response not found!'), 'error');
      return;
    }

    $response = $storage['response'];
    // For generate-article command create new nodes based on response data.
    if ($response->get('command')->getValue() == 'generate-article') {
      foreach ($response->get('results') as $result) {
        $node = Node::create([
          'type' => 'article',
          'title' => $result->get('title')->getValue(),
          'body' => [
            'value' => $result->get('body')->getValue(),
            'format' => 'full_html',
          ],
          'author' => $result->get('author')->getValue(),
        ]);
        $node->save();

        drupal_set_message($this->t('Article @title has been created (NID=@id).', [
          '@id' => $node->id(),
          '@title' => $node->getTitle(),
        ]));
      }
    }

  }

}

In validateForm, we have created a typed data object and inserted submitted data to this object. Then it was validated, and in case of any problems, we see all errors from typed data.

On a form submit, we create nodes based on data from the response. Check that we have a 'get' method for interaction with any datatype property. List data has a built-in iterator so that we can use 'foreach' for access to each item from the list.

Typed data demo form

I hope you'll find this article helpful, and below you can have additional resources to learn more about Type Data API in Drupal 8.

Igor
Team Lead and technology leader. Writes about Drupal, technologies and digital innovations.

LET'S CONNECT

Get a stunning website, integrate with your tools,
measure, optimize and focus on success!