Building an API service on Drupal 8

Jan Zavrl

Drupal Camp Zagreb, May 2017

Jan Zavrl

@jnzavrl | jzavrl | janzavrl.me

@ndp_studio | ndp-studio.com

Building an API service on Drupal 8, Zagreb, May 2017​

The brief and the decisions

Building an API service on Drupal 8, Zagreb, May 2017​

  • API service
  • Decoupled architecture
  • Is Drupal the right way forward?

Into the unknown

  • The boring specification work
  • The frightening concepts
  • The hopeful relevation

Building an API service on Drupal 8, Zagreb, May 2017​

Now the fun begins

  • Starting up a new project
  • Setting up the architecture
  • Base classes

Building an API service on Drupal 8, Zagreb, May 2017​

Building an API service on Drupal 8, Zagreb, May 2017​

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Provides a base class for all Salesforce resource classes.
 */
abstract class SalesforceBaseResource extends ResourceBase {

...

/**
 * The Salesforce SOAP contact creation method.
 *
 * @var string
 */
const ACCOUNT_CONTACT_METHOD = 'addAccountContact';

...

/**
 * Returns a new ResourceResponse object containing data.
 *
 * @param array $data
 *   Array of information needed to respond with.
 *
 * @return \Drupal\rest\ResourceResponse
 *   The response.
 */
public function respond(array $data) {
  // Prepare a new ResourceResponse with the data.
  $response = new ResourceResponse($data);

  return $this->disableCache($response);
}

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Returns a new ModifiedResourceResponse object containing data.
 *
 * @param array $data
 *   Array of information needed to respond with.
 *
 * @return \Drupal\rest\ModifiedResourceResponse
 *   The response.
 */
public function patchRespond(array $data) {
  // Prepare a new ModifiedResourceResponse with the data.
  return new ModifiedResourceResponse($data);
}

Building an API service on Drupal 8, Zagreb, May 2017​

The endpoints

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Provides a resource to get view modes by entity and bundle.
 *
 * @RestResource(
 *   id = "salesforce_get_order",
 *   label = @Translation("Salesforce get Order"),
 *   uri_paths = {
 *     "canonical" = "/v1/salesforce/orders/{id}/{property}"
 *   }
 * )
 */
class SalesforceGetOrder extends SalesforceBaseResource {

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Responds to GET requests for retrieving order information.
 *
 * @param string $property
 *   The property of the object to retrieve. Will be passed from the URL
 *   and is optional.
 * @param string $id
 *   The id of the object to retrieve. Will be passed from the URL.
 *
 * @return \Drupal\rest\ResourceResponse
 *   A new ResourceResponse instance.
 */
public function get($property, $id) {
  // Get the order array ready for response.
  $response = $this->salesforceHelper->getOrder($id, $property);

  return $this->respond($response);
}

Building an API service on Drupal 8, Zagreb, May 2017​

namespace Drupal\apiservice_salesforce\Plugin\rest\resource;

/**
 * Provides a resource to get view modes by entity and bundle.
 *
 * @RestResource(
 *   id = "salesforce_create_order",
 *   label = @Translation("Salesforce create Order"),
 *   uri_paths = {
 *     "canonical" = "/v1/salesforce/orders/create",
 *     "https://www.drupal.org/link-relations/create" = "/v1/salesforce/orders/create"
 *   }
 * )
 */
class SalesforceCreateOrder extends SalesforceBaseResource {

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Responds to POST requests for creating orders.
 *
 * @param array $data
 *   The request body with fields to insert.
 *
 * @return \Drupal\rest\ResourceResponse
 *   A new ResourceResponse instance.
 */
public function post(array $data = []) {
  // Create a new order based on the given array.
  $response = $this->salesforceHelper->createOrder($data);

  return $this->respond($response);
}

Building an API service on Drupal 8, Zagreb, May 2017​

Making the connections

  • Configuration entities for storing credentials
  • Implementing and using them in the endpoints

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Class SalesforceSoapForm.
 *
 * @package Drupal\apiservice_salesforce\Form
 */
class SalesforceSoapForm extends ConfigFormBase {

...

/**
 * {@inheritdoc}
 */
public function buildForm(array $form, FormStateInterface $form_state) {
  $form['username'] = [
    '#type' => 'textfield',
    '#title' => $this->t('Salesforce SOAP username'),
    '#maxlength' => 64,
    '#size' => 64,
    '#default_value' => $this->config->get('username'),
    '#description' => $this->t('Enter the username of your Salesforce account.'),
  ];

...

/**
 * {@inheritdoc}
 */
public function submitForm(array &$form, FormStateInterface $form_state) {
  parent::submitForm($form, $form_state);

  $this->config->set('username', $form_state->getValue('username'));

...

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Defines the Moodle client entity.
 *
 * @ConfigEntityType(
 *   id = "moodle_client",
 *   label = @Translation("Moodle client"),
 *   handlers = {
 *     "list_builder" = "Drupal\apiservice_moodle\MoodleClientListBuilder",
 *     "form" = {
 *       "add" = "Drupal\apiservice_moodle\Form\MoodleClientForm",
 *       "edit" = "Drupal\apiservice_moodle\Form\MoodleClientForm",
 *       "delete" = "Drupal\apiservice_moodle\Form\MoodleClientDeleteForm"
 *     },
 *     "route_provider" = {
 *       "html" = "Drupal\apiservice_moodle\MoodleClientHtmlRouteProvider",
 *     },
 *   },
 *   config_prefix = "moodle_client",
 *   admin_permission = "administer site configuration",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "label",
 *     "uuid" = "uuid",
 *   },
 *   links = {
 *     "canonical" = "/admin/config/services/moodle/moodle_client/{moodle_client}",
 *     "add-form" = "/admin/config/services/moodle/moodle_client/add",
 *     "edit-form" = "/admin/config/services/moodle/moodle_client/{moodle_client}/edit",
 *     "delete-form" = "/admin/config/services/moodle/moodle_client/{moodle_client}/delete",
 *     "collection" = "/admin/config/services/moodle/moodle_client"
 *   }
 * )
 */
class MoodleClient extends ConfigEntityBase implements MoodleClientInterface  {

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * {@inheritdoc}
 */
public function getSoapClient() {
  return $this->getMoodleClientPluginManager()->createInstance($this->plugin_id, [
    'token' => $this->getToken(),
    'endpoint' => $this->getEndpoint(),
    'instance' => $this->id(),
    'email_notification_status' => $this->getEmailNotificationStatus(),
    'basic_auth' => $this->getBasicAuth()
  ]);
}

Building an API service on Drupal 8, Zagreb, May 2017​

The heavy lifting

  • Standardising input and output parameters
  • Services have references as well
  • Dependency injection
  • Events and event subscribers

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Salesforce mapper service.
 *
 * @package Drupal\apiservice_salesforce
 */
class SalesforceMapper {

  /**
   * Main method for retrieving mappings.
   *
   * The method retrieves mapping information based on the type provided,
   * the Salesforce object name and also standardizes the type itself.
   *
   * @param string $type
   *   The type of the object we are looking for.
   *
   * @return array
   *   Array containing the standardizes type string and the mappings array.
   */
  public function getMappings($type) {
    $mapping_type = '';
    $mapping_object = '';
    $mapping_reference = '';

    switch ($type) {
      case 'account':
      case 'Account':
        $mapping_type = 'account';
        $mapping_object = 'Account';
        $mappings = $this->accountMapping();
        break;

...

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Contact mapping from this API to the object retrieved from Salesforce.
 *
 * @return array
 *   The array containing the mappings.
 */
private function contactMapping() {
  return [
    'id' => 'X18_Digit_Contact_ID__c',
    'account_id' => 'AccountId',
    'principal_building' => 'Contact_Principle_Address_Building__c',
    'principal_street' => 'Contact_Principle_Address_Street__c',
    'principal_area' => 'Contact_Principle_Address_Area__c',
    'principal_city' => 'Contact_Principle_Address_City__c',
    'principal_state' => 'Contact_Principle_Address_State_Province__c',
    'principal_postal_code' => 'Contact_Principle_Address_ZIP_Postal__c',
    'principal_country' => 'Contact_Principle_Address_Country__c',
    'email_opt_out' => 'HasOptedOutOfEmail',
    'website_user_id' => 'WebId__c',
    'website_user_ref' => 'Website_User_Ref__c',
    'salutation' => 'Salutation',
    'first_name' => 'FirstName',
    'last_name' => 'LastName',
    'title' => 'Title',
    'email' => 'Email',
    'phone' => 'MobilePhone',
    'subscription' => $this->subscriptionMapping(),
    'external_credentials' => $this->externalCredentialsMapping(),
  ];
}

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Maps given data to the ones defined in the mappings.
 *
 * @param array $mappings
 *   The mappings structure array.
 * @param array $data
 *   The data that needs mapping.
 *
 * @return array
 *   The newly mapped array of data.
 */
private function mapData(array $mappings, array $data) {
  $mapped_data = [];

  // Loop over the given data and map it properly against the mappings.
  foreach ($data as $key => $value) {
    if ($mappings['mappings'][$key]) {
      $mapped_data[$mappings['mappings'][$key]] = $value;
    }
  }

  return $mapped_data;
}

Building an API service on Drupal 8, Zagreb, May 2017​

/**
 * Retrieves a referenced object based on the parent object.
 *
 * @param \Drupal\salesforce\SObject $parent
 *   The Salesforce object of the parent.
 * @param string $child
 *   The API identifier for the referenced object.
 *
 * @return array|mixed
 *   Prepared data of the new referenced object,
 *   or a message if it couldn't be retrieved.
 */
private function getReferencedObject(SObject $parent, $child) {
  // Get mapping for this reference object.
  $reference_mapping = $this->salesforceMapper->getMappings($child);

  // Prepare the condition fields for the query.
  $mapping = array_flip($reference_mapping['mappings']);
  $mapping = implode(',', $mapping);
  $conditions = [
    'conditions' => [
      $reference_mapping['reference'] => $parent->id()->__toString(),
    ],
  ];

  // Run the query.
  $result = $this->runQuery($child, $mapping, $conditions);

  return $result;
}

Building an API service on Drupal 8, Zagreb, May 2017​

Being on the final straight

  • Deploying to multiple environments
  • Several credentials
  • Ignoring the configuration files

Building an API service on Drupal 8, Zagreb, May 2017​

@jnzavrl | jzavrl | janzavrl.me

@ndp_studio | ndp-studio.com

Questions, comments?

Feel free to give me a nudge outside.

Building an API service on Drupal 8, Zagreb, May 2017​

Building an API service on Drupal 8, the ifs, whys and hows.

By Jan Zavrl

Building an API service on Drupal 8, the ifs, whys and hows.

Myself and the team were recently given a project to create an API service that would connect the client's website with several 3rd party integrations such as CRM systems etc. Of course with the REST API included in Drupal's core (and the fact that we are a Drupal studio) we immediately thought about using Drupal 8 for this. And after a couple of proof of concepts, we liked the idea more and more.

  • 1,475