26 January 2015

Second post in my series “Drupal Store Locator” which is an update of a chapter from my book “Drupal Mobile Web Development: A Beginner’s Guide”.

Now that we have the storage container, we need a way of importing and exporting raw data into and out of the container. For that I took at look at the 3 REST API modules, Services, REST and RESTful. The one that made the most sense to me was RESTful because it basically allows you to write classes that represent your exposed data and tightly control how the data is imported.

###Services The bellweather module for implementing REST API’s in Drupal is the Services. There are, however, a couple of issues for which services does not adequately work. First and foremost is the API is not structured according to the standard noun/verb pattern that has become the standard for API’s and I found (my opinion here) posting data and getting that data saved to drupal’s database to be cumbersome at best and at worst, impossible.

###REST REST is an abandoned project that follows the API Pattern, e.g. POST /blog/entry +[data] = new entry. The problem is that it has, in fact, been abandoned and will not be updated, making this solution undesirable for that reason alone.

###RESTful Restful is a project by the guys over at Gizra. The way it differs is that the module itself does not expose any content to the REST interface, rather it is an API by which content can be mad available. The developer must explicitly write classes for each piece of exposed content. This may sound scary, but like I always say, “Let go of my ears and lean back, I’ve done this before.”

##Its About Creation So in evaluating REST frameworks, I tried creating content with the REST interface. While I eventually got each module to work, I found the one that was most customizable was RESTful because in writing the class, I could handle extra arguments in the class and run some error checking on the back end if I cared to do so. Also, the added bonus of exposing only the content types I cared to was very attractive. Whereas the other two REST frameworks exposed all content types to REST actions, RESTful only exposes content types for which you write a data class. This made sense to me.

The class I built was as follows:


/*
 *  @file {module}/plugins/restful/node/partner/1.0/AnkiRestfulPartnerResource.class.php
 *
 *
*/


class AnkiRestfulPartnerResource extends RestfulEntityBaseNode {

  /**
   * Overrides RestfulExampleArticlesResource::publicFieldsInfo().
   */

  public function publicFieldsInfo() {
    $public_fields = parent::publicFieldsInfo();

    $public_fields['nid'] = array(
      'property' => 'nid',
    );
    $public_fields['vid'] = array(
      'property' => 'vid',
    );
    $public_fields['status'] = array(
      'property' => 'status',
    );
    $public_fields['uuid'] = array(
      'property' => 'uuid',
    );
    $public_fields['language'] = array(
      'property' => 'language',
    );

    $public_fields['location'] = array(
      'property' => 'field_location',
    );
    return $public_fields;
  }
  
}

The publicFieldsInfo method exposes which fields will be available to the REST interface. Calling the parent on the first line of the method adds the node title to the list.

What I wanted to be able to do was to move the content from environment to environment without having to move the Drupal database so I wrote a script that does the import via REST with a few classes that extended Guzzle. This is a separate project and separate stack from the Drupal repo. This will be used almost entirely from the command line:


/*
 *  @file {import/export project}/src/Drupal/Partner.php
 *
 *
*/

namespace Drupal\REST;

use GuzzleHttp\Message\Request;
use GuzzleHttp\Post\PostFile;
use GuzzleHttp\Exception\RequestException;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Component\CssSelector\CssSelector;

/**
 * Class Node
 * @package Drupal\REST
 */
class Partner extends Base {

  static $addressFieldTranslation = [
    "state" => "province",
    "zip" => "postal_code",
    "street2" => "additional",
    "address" => "street",
  ];



  public $remoteInfo = [
    "label" => null,
    "location" => [
      "name" => null,
      "street" => null,
      "additional" => null,
      "city" => null,
      "province" => null,
      "postal_code" => null,
      "country" => "us",
      "latitude" => null,
      "longitude" => null,
      "source" => null,
      "is_primary" => 1,
      "province_name" => null,
      "email" => null,
      "fax" => null,
      "phone" => null
    ],

  ];



  function __construct($values) {
    if (empty($values)) {
      return null;
    }
    foreach ($values as $key => $value) {
      $this->setValue($key, $value);
    }
  }

  function save() {
    try {
      $config = Config::get_config(true);
      $client = Config::get_rest_client('restful');
      $resp = $client->post($config['drupalRestEndpoint']."/en/api/v1/partner", [
        "debug" => false,
        "json" => $this->__toArray()
      ]);
      if (in_array($resp->getStatusCode(), [200, 201, 202])) {
        echo $this->getValue("label")." has been saved.".PHP_EOL;
      }
    } catch(Exception $e) {
      if ($e instanceOf RequestException) {
        echo $e->getRequest() . PHP_EOL;
        if ($e->hasResponse()) {
          echo $e->getResponse() . PHP_EOL;
        }
      } else {
        echo $e->getMessage();
      }
    }
  }

  function setValue($key, $value) {
    if (in_array($key, array_keys(self::$addressFieldTranslation))) {
      $this->remoteInfo['location'][self::$addressFieldTranslation[$key]] = $value;
    } elseif(in_array($key, array_keys($this->remoteInfo['location']))) {
      if ($key == "country") {
        $this->remoteInfo['location'][$key] = strtolower($value);
      } else {
        $this->remoteInfo['location'][$key] = $value;
      }
    } else {
      $this->remoteInfo[$key] = $value;
    }
  }

  function getValue($key) {
    if (in_array($key, array_keys(self::$addressFieldTranslation))) {
      return $this->remoteInfo['location'][self::$addressFieldTranslation[$key]];
    } elseif (array_key_exists($key, $this->remoteInfo['location'])) {
      return $this->remoteInfo[$key];
    } elseif (array_key_exists($key, $this->remoteInfo)) {
      return $this->remoteInfo[$key];
    } else {
      return null;
    }
  }


  function __toJSON() {
    return json_encode($this->__toArray());
  }

  function __toArray() {
    $toReturn = [
      "label" => $this->getValue("label"),
      "location" => $this->getValue("location"),
      "language" => "en",
      "status" => 1,
    ];
    $toReturn['location']['name'] = $toReturn['label'];
    return $toReturn;
  }

}

This class takes an array as constructor argument and posts that array to the drupal resource /en/api/v1/partner. We’ll use this class to script the import from a standard CSV of partner locations and then use the script below to do the work of running the import:


#!/usr/bin/env php
<?php

/*
 *  @file {import/export project}/bin/importPartners.php
 *
 *
*/


ini_set("auto_detect_line_endings", true);

//error_reporting(E_ERROR | E_PARSE);

require_once(__DIR__ . "/../config/config.php");

if (!array_key_exists("GRUNT_CONFIG", $_ENV)) {
  $config = \Drupal\REST\Config::get_config();
}


use GuzzleHttp\Post\PostFile;
use GuzzleHttp\Exception\RequestException;
use Drupal\REST\Node;


$script = array_shift($argv);
$argc--;

if ($argc) {
  echo "Importing {$argv[0]} into drupal env ".$config["drupalRestEndpoint"].PHP_EOL;
  $incoming = [];
  try{
    if (file_exists($argv[0])) {
      $toImport = file($argv[0]);
      foreach($toImport as $line) {
        $incoming[] =  explode(",", substr($line, 0, -2));
      }
      $keys = array_shift($incoming);
    }
    
    if (!empty($incoming)) {
      foreach($incoming as $rowNum => $row) {
        $partner = new \Drupal\REST\Partner(array_combine($keys,$row));
        if ($partner instanceOf \Drupal\REST\Partner) {
          $partner->save();
        }
      }
    }

  } catch(Exception $e) {
    if ($e instanceOf RequestException) {
      echo $e->getRequest() . PHP_EOL;
      if ($e->hasResponse()) {
        echo $e->getResponse() . PHP_EOL;
      }
    } else {
      echo $e->getMessage();
    }
    print_r(debug_backtrace());
  }
}

Notice that the first line starts with a #!/usr/bin/env php. This as well as changing the file permissions to allow execution (chmod +x) will allow us to call this script from the command line with a single argument that represents the data CSV.

Part 3 will describe the front end user interface once we’ve got the data imported.