You are here

class MigrateJSONReader in Migrate 7.2

An iterator over a JSON file. As is, this assumes that the file is structured as an array of objects, e.g.: [{"id":"53","field1":"value1"},{"id":"54","field1":"value2"}] To deal with different structures, extend this class and override next().

Hierarchy

Expanded class hierarchy of MigrateJSONReader

1 string reference to 'MigrateJSONReader'
MigrateSourceJSON::__construct in plugins/sources/json.inc
Source constructor.

File

plugins/sources/json.inc, line 184
Support for migration from JSON sources.

View source
class MigrateJSONReader implements Iterator {

  /**
   * URL of the source JSON file.
   *
   * @var string
   */
  public $url;

  /**
   * Handle of the JSON file we're currently parsing.
   *
   * @var resource
   */
  protected $fileHandle;

  /**
   * Current element object when iterating.
   *
   * @var
   */
  protected $currentElement = NULL;

  /**
   * Value of the ID for the current element when iterating.
   *
   * @var string
   */
  protected $currentId = NULL;

  /**
   * Initialize the members.
   *
   * @param $json_url
   *  URL or filespec of the JSON file to be parsed.
   * @param $id_field
   *  Name of the field within each object containing its unique ID.
   */
  public function __construct($json_url, $id_field) {
    $this->url = $json_url;
    $this->idField = $id_field;
  }

  /**
   * Implementation of Iterator::rewind().
   *
   * @return void
   */
  public function rewind() {

    // Close any open file - we open the files lazily in next().
    if ($this->fileHandle) {
      fclose($this->fileHandle);
      $this->fileHandle = NULL;
    }

    // Load the first element.
    $this
      ->next();
  }

  /**
   * Obtain the next non-whitespace character from the JSON file.
   *
   * @return string
   *  The next non-whitespace character, or FALSE on end-of-file.
   */
  protected function getNonBlank() {
    while (($c = fgetc($this->fileHandle)) !== FALSE && trim($c) == '') {
    }
    return $c;
  }

  /**
   * Implementation of Iterator::next().
   *
   * Populates currentElement (the object being retrieved) and currentId (that
   * object's unique identifier) from the specified JSON file. Sets both to
   * NULL at end-of-file. Handles properly-formed JSON, as well as some improper
   * coding (specifically that generated in Ning exports).
   *
   * @return void
   */
  public function next() {
    migrate_instrument_start('MigrateJSONReader::next');
    $this->currentElement = $this->currentId = NULL;

    // Open the file and position it if necessary
    if (!$this->fileHandle) {
      $this->fileHandle = fopen($this->url, 'r');
      if (!$this->fileHandle) {
        Migration::displayMessage(t('Could not open JSON file !url', array(
          '!url' => $this->url,
        )));
        return;
      }

      // We're expecting an array of characters, so the first character should be [.
      $char = $this
        ->getNonBlank();

      // Ning exports are wrapped in bogus (), so skip a leading (
      if ($char == '(') {
        $char = $this
          ->getNonBlank();
      }
      if ($char != '[') {
        Migration::displayMessage(t('!url is not a JSON file containing an array of objects', array(
          '!url' => $this->url,
        )));
        return;
      }
    }

    // We expect to be positioned either at an object (beginning with {) or
    // the end of the file (we should see a ] indicating this). Or, an
    // object-separating comma, to be skipped. Note that this treats
    // commas as optional between objects, which helps with processing
    // malformed JSON with missing commas (as in Ning exports).
    $c = $this
      ->getNonBlank();
    if ($c == ',') {
      $c = $this
        ->getNonBlank();
    }
    elseif ($c == ']') {
      $c = $this
        ->getNonBlank();
      if ($c != '{') {
        $c = NULL;
      }
    }

    // We expect to be at the first character of an object now.
    if ($c == '{') {

      // Start building a JSON string for this object.
      $json = $c;

      // Look for the closing }, ignoring brackets in strings, tracking nested
      // brackets. Watch out for escaped quotes, but also note that \\" is not
      // an escaped quote.
      $depth = 1;
      $in_string = FALSE;
      $in_escape = FALSE;
      while (($c = fgetc($this->fileHandle)) !== FALSE) {
        $json .= $c;
        if ($in_string) {

          // Quietly accept an escaped character
          if ($in_escape) {
            $in_escape = FALSE;
          }
          else {
            switch ($c) {

              // Unescaped " means end of string
              case '"':
                $in_string = FALSE;
                break;

              // Unescaped \\ means start of escape
              case '\\':
                $in_escape = TRUE;
                break;
            }
          }
        }
        else {

          // Outside of strings, recognize {} as depth changes, " as start of
          // string.
          switch ($c) {
            case '{':
              $depth++;
              break;
            case '}':
              $depth--;
              break;
            case '"':
              $in_string = TRUE;
              break;
          }

          // We've found our match, exit the loop.
          if ($depth < 1) {
            break;
          }
        }
      }

      // Turn the JSON string into an object.
      $this->currentElement = json_decode($json);
      $this->currentId = $this->currentElement->{$this->idField};
    }
    else {
      $this->currentElement = NULL;
      $this->currentId = NULL;
    }
    migrate_instrument_stop('MigrateJSONReader::next');
  }

  /**
   * Implementation of Iterator::current().
   *
   * @return null|object
   */
  public function current() {
    return $this->currentElement;
  }

  /**
   * Implementation of Iterator::key().
   *
   * @return null|string
   */
  public function key() {
    return $this->currentId;
  }

  /**
   * Implementation of Iterator::valid().
   *
   * @return bool
   */
  public function valid() {
    return !empty($this->currentElement);
  }

}

Members

Namesort descending Modifiers Type Description Overrides
MigrateJSONReader::$currentElement protected property Current element object when iterating.
MigrateJSONReader::$currentId protected property Value of the ID for the current element when iterating.
MigrateJSONReader::$fileHandle protected property Handle of the JSON file we're currently parsing.
MigrateJSONReader::$url public property URL of the source JSON file.
MigrateJSONReader::current public function Implementation of Iterator::current().
MigrateJSONReader::getNonBlank protected function Obtain the next non-whitespace character from the JSON file.
MigrateJSONReader::key public function Implementation of Iterator::key().
MigrateJSONReader::next public function Implementation of Iterator::next().
MigrateJSONReader::rewind public function Implementation of Iterator::rewind().
MigrateJSONReader::valid public function Implementation of Iterator::valid().
MigrateJSONReader::__construct public function Initialize the members.