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​