drupal_yext-8.x-1.0/src/Yext/Yext.php

src/Yext/Yext.php
<?php

namespace Drupal\drupal_yext\Yext;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\node\Entity\Node;
use Drupal\drupal_yext\SelfTest\SelfTest;
use Drupal\drupal_yext\traits\Singleton;
use Drupal\drupal_yext\traits\CommonUtilities;
use Drupal\drupal_yext\YextContent\NodeMigrationOnSave;
use Drupal\drupal_yext\YextContent\NodeMigrationAtCreation;
use Drupal\drupal_yext\YextContent\YextSourceRecord;
use Drupal\drupal_yext\YextContent\YextEntity;
use Drupal\drupal_yext\YextContent\YextEntityFactory;
use Drupal\drupal_yext\YextContent\YextSourceRecordFactory;
use Drupal\drupal_yext\YextContent\YextTargetNode;
use Drupal\drupal_yext\YextPluginCollection;

/**
 * Represents the Yext API.
 */
class Yext {

  use CommonUtilities;
  use Singleton;
  use StringTranslationTrait;

  /**
   * Calls will break Yext if the offset is greater than this.
   */
  const MAX_OFFSET = 9999;

  /**
   * Yext account number getter/setter.
   *
   * @param string $acct
   *   An account number provided by Yext.
   */
  public function accountNumber(string $acct = '') : string {
    if (!empty($acct)) {
      $this->stateSet('drupal_yext_acct', $acct);
    }
    return $this->stateGet('drupal_yext_acct', 'me');
  }

  /**
   * Perform a function on all active nodes of a target type.
   *
   * Active nodes are nodes that have a non-empty Yext ID.
   *
   * @param string $log_function
   *   A log function such as 'print_r'.
   * @param int $chunk_size
   *   If you have a very large number of nodes, to avoid memory issues, you
   *   might want to have a chunk size of, say, 100.
   * @param string $log_message
   *   A log message.
   * @param string $function
   *   A function such as delete or save.
   * @param bool $increment
   *   Whether or not to increment between chunks. If deleting, it is best to
   *   set to FALSE.
   * @param int $start_at
   *   The first node id to use.
   */
  protected function actionOnAllExisting(string $log_function, int $chunk_size, string $log_message, string $function, bool $increment = TRUE, int $start_at = 0) {
    $start = 0;
    $i = 0;
    while ($nodes = $this->getAllExisting($start, $chunk_size, $start_at)) {
      $log_function('   => Processing chunk ' . $i++ . PHP_EOL);
      foreach ($nodes as $node) {
        $log_function($log_message . ' ' . $node->id() . PHP_EOL);
        $node->$function();
      }
      if ($increment) {
        $start += $chunk_size;
      }
    }
  }

  /**
   * Given a URL, adds filters.
   *
   * @param string $url
   *   The URL without the filters.
   * @param array $filters
   *   Filters to add.
   *
   * @return string
   *   The URL with the filters.
   */
  public function addFilters(string $url, array $filters = []) : string {
    $url2 = $url;
    if (!empty($filters)) {
      $url2 .= '&filters=' . urlencode(json_encode($filters));
    }
    return $url2;
  }

  /**
   * Merge the user-defined filters with the internal lastUpdated filter.
   *
   * @param string $date
   *   First date in range.
   * @param string $date2
   *   Last date in range.
   *
   * @return array
   *   An array suitable for jsonization, to be passed as a "filter" get
   *   parameter to Yext's API.
   */
  public function allFilters($date, $date2) : array {
    return array_merge([
      [
        'lastUpdated' => [
          'between' => [
            $date,
            $date2,
          ],
        ],
      ],
    ], $this->filtersAsArray());
  }

  /**
   * Yext API key getter/setter.
   *
   * @param string $api
   *   A hard-to-guess secret.
   */
  public function apiKey(string $api = '') : string {
    if (!empty($api)) {
      $this->stateSet('drupal_yext_api', $api);
    }
    return $this->stateGet('drupal_yext_api', '');
  }

  /**
   * The Yext API version to use.
   *
   * @return string
   *   The API version.
   */
  public function apiVersion() : string {
    return $this->stateGet('drupal_yext_api_version', '20180205');
  }

  /**
   * Getter/setter for the Yext base URL.
   *
   * @param string $base
   *   If set, changes the base URL.
   *
   * @return string
   *   The base URL.
   */
  public function base(string $base = '') : string {
    if (!empty($base)) {
      $this->stateSet('drupal_yext_base', $base);
    }
    return $this->stateGet('drupal_yext_base', $this->defaultBase());
  }

  /**
   * Build a URL for a Yext GET request.
   *
   * @param string $path
   *   For example /v2/api/...
   *   Any instance of /me/ will be replaced with the actual account.
   * @param string $key
   *   A key to use, defaults to the saved API key.
   * @param array $filters
   *   Filters as per the API documentation.
   * @param int $offset
   *   The offset.
   * @param string $base
   *   The base URL to use; if empty use the base URL in memory..
   *
   * @return string
   *   A URL.
   */
  public function buildUrl(string $path, string $key = '', array $filters = [], int $offset = 0, string $base = '') : string {
    if ($offset > self::MAX_OFFSET) {
      throw new \Exception('Due to a limitation with the version of the Yext API we are using, if the offset is above ' . self::MAX_OFFSET . ', it will cause a failure. Please avoid making such calls.');
    }

    $key2 = $key ?: $this->apiKey();
    $base2 = $base ?: $this->base();
    $path2 = str_replace('/me/', '/' . $this->accountNumber() . '/', $path);

    if (!$key2) {
      throw new \Exception('We are attempting to build a URL for Yext with an empty key; this will always fail.');
    }

    $return = $base2 . $path2 . '?limit=50&offset=' . $offset . '&api_key=' . $key2 . '&v=' . $this->apiVersion();
    $return2 = $this->addFilters($return, $filters);
    $for_the_log = str_replace($key2, 'YOUR-API-KEY', $return2);
    $this->watchdog('Yext: built url ' . $for_the_log);
    return $return2;
  }

  /**
   * Return TRUE if the entity is of the correct type.
   *
   * In /admin/config/yext/yext, a specific entity type can be mapped to
   * Yext. Only that type will be accepted.
   *
   * @return bool
   *   TRUE if types match.
   */
  public function checkEntityType(FieldableEntityInterface $entity) : bool {
    if ($entity->getEntityType()->id() != 'node') {
      return FALSE;
    }
    return method_exists($entity, 'getType') && $entity->getType() == $this->yextNodeType();
  }

  /**
   * Get the default Yext base URL.
   *
   * @return string
   *   The default Yext base URL.
   */
  public function defaultBase() : string {
    return 'https://api.yext.com';
  }

  /**
   * See ./README.md for how this works.
   *
   * @param string $log_function
   *   A log function such as 'print_r'.
   * @param int $chunk_size
   *   If you have a very large number of nodes, to avoid memory issues, you
   *   might want to have a chunk size of, say, 100.
   */
  public function deleteAllExisting(string $log_function = 'print_r', int $chunk_size = PHP_INT_MAX) {
    return $this->actionOnAllExisting($log_function, $chunk_size, 'permanently deleting node', 'delete', FALSE);
  }

  /**
   * Get total number of nodes having failed to import.
   *
   * @return int
   *   nodes having failed to import.
   */
  public function failed() {
    return count($this->stateGet('drupal_yext_failed', []));
  }

  /**
   * Get all existing nodes of the target type if it has a Yext ID.
   *
   * This is meant to load only the nodes which are linked to Yext entities.
   * We will want to ignore nodes which were created manually.
   *
   * @param int $start
   *   The offset, by default 0.
   * @param int $length
   *   The length of the desired array, by default all items. If you get out
   *   of memory errors, you can try something like 50 here. In which case
   *   in the next call you can call this with a start of 50.
   * @param int $start_at
   *   The first node id to use.
   *
   * @return array
   *   Array of Drupal nodes.
   */
  public function getAllExisting(int $start = 0, int $length = PHP_INT_MAX, int $start_at = 0) : array {
    $nids = $this->drupalEntityQuery('node')
      ->condition('nid', $start_at, '>=')
      ->condition('type', $this->yextNodeType())
      ->condition($this->uniqueYextIdFieldName(), NULL, '<>')
      ->range($start, $length)
      ->execute();
    return Node::loadMultiple($nids);
  }

  /**
   * Given a unique ID such as 0013800002eNtybAAC, return its record.
   *
   * An exception is thrown if the record does not exist.
   *
   * @param string $id
   *   A unique Yext ID.
   *
   * @return array
   *   A unique Yext record.
   */
  public function getRecordByUniqueId($id) : array {
    $url = $this->buildUrl('/v2/accounts/me/locations/' . $id);
    $body = (string) $this->httpGet($url)->getBody();
    return json_decode($body, TRUE);
  }

  /**
   * Get/set filters as text, with one per line.
   *
   * @param string $filters
   *   Get filters such as: '[{"locationType":{"is":[2]}}]'. One per
   *   line.
   *
   * @return string
   *   The filters as config text.
   */
  public function filtersAsText(string $filters = '') : string {
    if (!empty($filters)) {
      $this->configSet('drupal_yext_filters', $filters);
    }
    return $this->configGet('drupal_yext_filters', '');
  }

  /**
   * Get one filter as an array.
   *
   * @return array
   *   The filter as an array.
   */
  public function filterAsArray(string $filter) : array {
    $return = @json_decode($filter, TRUE);
    if (!is_array($return)) {
      throw new \Exception('Cannot json decode: ' . $filter);
    }
    return $return;
  }

  /**
   * Get get parameters as array.
   *
   * @return array
   *   The get params as an array.
   */
  public function filtersAsArray() : array {
    $return = [];
    $as_string = $this->filtersAsText();
    $as_array = explode(PHP_EOL, $as_string);
    foreach ($as_array as $line) {
      $line = trim($line);
      if (!$line) {
        continue;
      }
      try {
        $return = array_merge($return, $this->filterAsArray($line));
      }
      catch (\Exception $e) {
        $this->watchdogThrowable($e);
      }
    }
    return $return;
  }

  /**
   * Given a source Yext record, return a new or existing node.
   *
   * @param \Drupal\drupal_yext\YextContent\YextSourceRecord $record
   *   A record from Yext.
   *
   * @return \Drupal\drupal_yext\YextContent\YextTargetNode
   *   A node on Drupal.
   */
  public function getOrCreateUniqueNode(YextSourceRecord $record) : YextTargetNode {
    $result = [];

    $this->plugins()->alterNodeFromSourceRecord($result, $record);

    // As a last resort, create a brand new node.
    if (empty($result['target'])) {
      $result['target'] = YextEntityFactory::instance()->generate('node', $this->yextNodeType());
      $result['target']->setFieldValue($this->uniqueYextIdFieldName(), $record->getYextId());
      $result['target']->save();
    }

    return $result['target'];
  }

  /**
   * Testable implementation of hook_entity_presave().
   */
  public function hookEntityPresave(EntityInterface $entity) {
    if (!$entity instanceof FieldableEntityInterface) {
      return;
    }
    try {
      if ($this->checkEntityType($entity)) {
        $this->updateRaw($entity);
        $dest = YextEntityFactory::instance()->destinationIfLinkedToYext($entity);
        $source = YextSourceRecordFactory::instance()->sourceRecord($dest->getYextRawDataArray());
        $migrator = new NodeMigrationOnSave($source, $dest);
        // Migrating will do nothing if the dest and source are set to
        // "ignore"-type classes.
        $migrator->migrate();
      }
    }
    catch (\Throwable $t) {
      $this->watchdogThrowable($t);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function hookRequirements($phase) : array {
    $requirements = [];
    if ($phase == 'runtime') {
      $test = $this->test();
      $requirements['DrupalYext.yext.test'] = [
        'title' => $this->t('Yext API key'),
        'description' => $this->t('The API key is set at /admin/config/yext, and is working.'),
        'value' => $test['message'],
        'severity' => $test['success'] ? REQUIREMENT_INFO : REQUIREMENT_ERROR,
      ];
    }
    return $requirements;
  }

  /**
   * Get total number of imported nodes.
   *
   * @return int
   *   Imported nodes.
   */
  public function imported() : int {
    return $this->stateGet('drupal_yext_imported', 0);
  }

  /**
   * Import nodes from Yext until two days from now.
   */
  public function importNodesToNextDatePlusTwoDays() {
    if ($start_from_failure = $this->stateGet('drupal_yext_remember_in_case_of_failure')) {
      $this->watchdog('Yext: starter where we left off after a failure.');
      $start = $start_from_failure['start'];
      $end = $start_from_failure['end'];
      $offset = $start_from_failure['offset'];
    }
    else {
      $this->watchdog('Yext: no previous failure detected; starting in the next date to import.');
      $start = $this->nextDateToImport('Y-m-d');
      $end = $this->nextDateToImport('Y-m-d', 2 * 24 * 60 * 60);
      $offset = 0;
    }
    $this->watchdog('Yext: query between ' . $start . ' and ' . $end . ' at offset ' . $offset);
    $this->importYextAll($start, $end, min(self::MAX_OFFSET, $offset));
  }

  /**
   * Import nodes from an array of nodes.
   *
   * See also "Avoiding node collisions during gradual launch" in ./README.md.
   *
   * @param array $array
   *   An array of Nodes from Yext.
   */
  public function importFromArray(array $array) {
    $all_ids = [];
    array_walk($array, function ($item, $key) use (&$all_ids) {
      if (isset($item['id'])) {
        $all_ids[$item['id']] = $item['id'];
      }
    });

    // Preload all nodes which have the Yext IDs.
    $nodes = YextEntityFactory::instance()->preloadUniqueNodes($this->yextNodeType(), $this->uniqueYextIdFieldName(), $all_ids);

    // Walk through all items from yext.
    foreach ($array as $item) {
      // Wrap the item in a YextSourceRecord object for manipulation.
      $source = new YextSourceRecord($item);
      // If a node already exists, use that one; otherwise create a new one.
      // This ensures that we should never have two nodes with the same
      // Yext ID.
      $destination = empty($nodes[$source->getYextId()]) ? $this->getOrCreateUniqueNode($source) : $nodes[$source->getYextId()];

      $migrator = new NodeMigrationAtCreation($source, $destination);
      try {
        $result = $migrator->migrate() ? 'migration occurred' : 'migration skipped, probably because update time is identical in source/dest.';
        $this->watchdog('Yext ' . $result . ' for ' . $source->getYextId() . ' to ' . $destination->id());
        $this->incrementSuccess();
      }
      catch (\Throwable $t) {
        $this->watchdogThrowable($t);
        $this->incrementFailed($item);
      }
    }
  }

  /**
   * Import some nodes.
   */
  public function importSome() {
    try {
      if (!$this->apiKey()) {
        $this->watchdog('Yext: no API key has been set; skipping import of Yext items.');
        return;
      }
      $this->watchdog('Yext: starting to import some nodes.');
      $this->watchdog('Yext: try to import all nodes before our cutoff date plus two days.');
      // That way we can include all the latest nodes even if our cutoff
      // date was yesterday.
      $this->importNodesToNextDatePlusTwoDays();
      $this->watchdog('Yext: increment our cutoff date but not too much.');
      $this->updateRemaining();
      $this->importIncrementCutoffDateButNotTooMuch();
      $this->stateSet('drupal_yext_last_check', $this->date('U'));
      $this->watchdog('Yext: --- finished import session: success ---');
      $this->stateSet('drupal_yext_remember_in_case_of_failure', NULL);
    }
    catch (\Throwable $t) {
      $this->watchdogThrowable($t);
      $this->watchdog('Yext: --- finished import session: error ---');
    }
  }

  /**
   * Increment the cutoff date, but do not go past today's date.
   */
  public function importIncrementCutoffDateButNotTooMuch() {
    $this->watchdog('Yext: incrementing cutoff date');
    $previous = intval($this->nextDateToImport('U'));
    $candidate = $previous + 24 * 60 * 60;
    $date = min($this->date('U'), $candidate);
    $this->watchdog('Yext: cutoff date incremented to ' . $this->date('Y-m-d H:i:s', $date));
    $this->stateSet('drupal_yext_next_import', $date);
  }

  /**
   * Import all Yext nodes from a start to an end date.
   *
   * This will import all nodes, even those which are not on the first
   * page of the Yext report.
   *
   * @param string $start
   *   YYYY-MM-DD.
   * @param string $end
   *   YYYY-MM-DD.
   * @param int $offset
   *   An offset. Using during recursion.
   */
  public function importYextAll(string $start, string $end, int $offset = 0) {
    $this->watchdog('Yext: importing with offset ' . $offset);
    $api_result = $this->queryYext($start, $end, $offset);
    $response_count = $api_result['response']['count'];
    $response_count_less_offset = $response_count - $offset;
    $response_locations = $api_result['response']['locations'];
    $response_locations_count = count($response_locations);
    $this->watchdog('Yext: Offset is ' . $offset);
    $this->watchdog('Yext: Response count is ' . $response_count);
    $this->watchdog('Yext: Response count less offset is ' . $response_count_less_offset);
    $this->watchdog('Yext: Location count on this page is ' . $response_locations_count);

    // This call might result in an out-of-memory error for large datasets.
    // Remember where were were first.
    $this->stateSet('drupal_yext_remember_in_case_of_failure', [
      'start' => $start,
      'end' => $end,
      'offset' => $offset,
    ]);
    $this->importFromArray($response_locations);
    if ($response_count_less_offset > $response_locations_count) {
      $new_offset = $offset + $response_locations_count;
      $this->watchdog('Yext: incrementing offset to ' . $new_offset . ' because response count less offset > response location count');
      if ($new_offset > $offset && $new_offset <= self::MAX_OFFSET) {
        $this->importYextAll($start, $end, $new_offset);
      }
    }
  }

  /**
   * Increment the number of nodes having failed to import.
   *
   * @param array $structure
   *   A node structure from Yext.
   */
  public function incrementFailed(array $structure) {
    $failed = $this->stateGet('drupal_yext_failed', []);
    $failed[$structure['id']] = $structure;
    $this->stateSet('drupal_yext_failed', $failed);
  }

  /**
   * Increment the number of nodes imported successfully.
   */
  public function incrementSuccess() {
    $imported = $this->imported();
    $this->stateSet('drupal_yext_imported', ++$imported);
  }

  /**
   * Get the last checked data.
   *
   * @param string $format
   *   For example Y-m-d.
   *
   * @return string
   *   The formatted last date checked.
   */
  public function lastCheck($format) {
    return date($format, $this->stateGet('drupal_yext_last_check', 0));
  }

  /**
   * Get the next date to import.
   *
   * @param string $format
   *   For example Y-m-d.
   * @param int $add
   *   How many seconds to addd.
   *
   * @return string
   *   The formatted next date to import.
   */
  public function nextDateToImport($format, int $add = 0) {
    return date($format, $this->stateGet('drupal_yext_next_import', strtotime('2017-12-10')) + $add);
  }

  /**
   * Get all YextPlugin plugins.
   *
   * @return \Drupal\drupal_yext\YextPluginCollection
   *   All plugins.
   */
  public function plugins() : YextPluginCollection {
    return YextPluginCollection::instance();
  }

  /**
   * Query Yext for a given date.
   *
   * @param string $date
   *   From date: YYYY-MM-DD.
   * @param string $date2
   *   To date: YYYY-MM-DD.
   * @param int $offset
   *   The offset if there is one.
   *
   * @return array
   *   A response from the Yext API.
   */
  public function queryYext($date, $date2, $offset = 0) : array {
    $url = $this->buildUrl('/v2/accounts/me/locations', '', $this->allFilters($date, $date2), $offset);
    $body = (string) $this->httpGet($url)->getBody();
    return json_decode($body, TRUE);
  }

  /**
   * TRUE if the raw field should be updated for this entity.
   *
   * If the config variable "update_raw_on_save" is set, or if the raw field is
   * empty for the entity, this will return TRUE.
   *
   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
   *   A Drupal entity.
   *
   * @return bool
   *   TRUE if the raw field should be updated for this entity.
   */
  public function rawUpdatable(FieldableEntityInterface $entity) {
    if ($this->configGet('update_raw_on_save', FALSE)) {
      return TRUE;
    }

    $candidate = YextEntityFactory::instance()->entity($entity);

    $raw = $candidate->fieldValue($this->fieldmap()->raw());
    if (!$raw) {
      return TRUE;
    }

    return FALSE;
  }

  /**
   * Get the remaining nodes to fetch.
   *
   * @return int
   *   The number of known remaining nodes.
   */
  public function remaining() {
    return $this->stateGet('drupal_yext_remaining', 999999);
  }

  /**
   * See ./README.md for how this works.
   *
   * @param string $log_function
   *   A log function such as 'print_r'.
   * @param int $chunk_size
   *   If you have a very large number of nodes, to avoid memory issues, you
   *   might want to have a chunk size of, say, 100.
   * @param int $start_at
   *   The first node id to use.
   */
  public function resaveAllExisting(string $log_function = 'print_r', int $chunk_size = PHP_INT_MAX, int $start_at = 0) {
    return $this->actionOnAllExisting($log_function, $chunk_size, 'resaving existing node', 'save', TRUE, $start_at);
  }

  /**
   * Reset everything to factory defaults.
   */
  public function resetAll() {
    $this->stateSet('drupal_yext_remaining', 999999);
    $this->stateSet('drupal_yext_imported', 0);
    $this->stateSet('drupal_yext_next_import', strtotime('2017-12-10'));
    $this->stateSet('drupal_yext_failed', []);
    $this->stateSet('drupal_yext_last_check', 0);
  }

  /**
   * Run some self-tests. Exit with non-zero code if errors occur.
   *
   * Usage:
   *
   *   ./scripts/self-test-running-environment.sh
   */
  public function selftest() {
    SelfTest::instance()->run();
  }

  /**
   * Set the next date to check.
   *
   * @param string $date
   *   A date in the format YYYY-MM-DD.
   */
  public function setNextDate(string $date) {
    $this->stateSet('drupal_yext_next_import', strtotime($date));
  }

  /**
   * Set the target node type for Yext data.
   *
   * @param string $type
   *   The node type such as 'article'.
   */
  public function setNodeType(string $type) {
    $this->configSet('target_node_type', $type);
  }

  /**
   * Set the field name which contains the Yext unique ID.
   *
   * @param string $field
   *   The field such as 'field_something'.
   */
  public function setUniqueYextIdFieldName(string $field) {
    $this->configSet('target_unique_id_field', $field);
  }

  /**
   * Set the field name which contains the Yext last updated time.
   *
   * @param string $field
   *   The field such as 'field_something'.
   */
  public function setUniqueYextLastUpdatedFieldName(string $field) {
    $this->configSet('target_unique_last_updated_field', $field);
  }

  /**
   * Test the connection to Yext.
   *
   * @param string $key
   *   The API key to use; if empty use the api key in memory.
   * @param string $account
   *   The account number to use; if empty use the account in memory.
   * @param string $base
   *   The base URL to use; if empty use the base URL in memory.
   *
   * @return array
   *   An array with two keys, success and message.
   */
  public function test(string $key = '', string $account = '', string $base = '') : array {
    $key2 = $key ?: $this->apiKey();
    $acct2 = $account ?: $this->accountNumber();
    $base2 = $base ?: $this->base();
    static $return;
    if (!empty(($return[$base2][$acct2][$key2]))) {
      return $return[$base2][$acct2][$key2];
    }
    try {
      $message = '';
      $return[$base2][$acct2][$key2]['success'] = $this->checkServer($this->buildUrl('/v2/accounts/' . $acct2 . '/locations', $key2, [], 0, $base2), $message);
      if (!$return[$base2][$acct2][$key2]['success']) {
        $return[$base2][$acct2][$key2]['message'] = 'Connection failed';
      }
      $return[$base2][$acct2][$key2]['more'] = $message;
    }
    catch (\Exception $e) {
      $return[$base2][$acct2][$key2] = [
        'success' => FALSE,
        'message' => 'Exception thrown while connecting',
        'more' => $e->getMessage(),
      ];
    }
    if ($return[$base2][$acct2][$key2]['success']) {
      $return[$base2][$acct2][$key2]['message'] = 'Connection successful';
    }
    $return[$base2][$acct2][$key2]['more'] = str_replace($key2, 'API-KEY-HIDDEN-FOR-SECURITY', $return[$base2][$acct2][$key2]['more']);
    return $return[$base2][$acct2][$key2];
  }

  /**
   * The Drupal field name which contains the Yext unique id.
   *
   * @return string
   *   A field name such as 'field_yext_unique_id.
   */
  public function uniqueYextIdFieldName() : string {
    return $this->configGet('target_unique_id_field', 'field_yext_unique_id');
  }

  /**
   * The Drupal field name which contains the Yext last updated info.
   *
   * @return string
   *   A field name such as 'field_yext_last_updated.
   */
  public function uniqueYextLastUpdatedFieldName() : string {
    return $this->configGet('target_unique_last_updated_field', 'field_yext_last_updated');
  }

  /**
   * Given an entity, update it based on a response from the server.
   *
   * @param \Drupal\drupal_yext\YextContent\YextEntity $candidate
   *   An entity to be updated.
   */
  public function updateEntityFromId(YextEntity $candidate) {
    $id = $candidate->fieldValue($this->uniqueYextIdFieldName());
    if (!$id) {
      // No ID, nothing to do.
      return;
    }
    if ($this->stateGet('drupal_yext_dryrun', FALSE)) {
      // For example, during self-tests.
      return;
    }
    try {
      // We now have an ID, we need to populate the raw data and, if we have
      // an error, unpublish the node.
      $data = $this->getRecordByUniqueId($id);
      if (!empty($data['response'])) {
        if (is_a($candidate, YextTargetNode::class)) {
          $candidate->setYextRawData(json_encode($data['response']));
        }
      }
      else {
        throw new \Exception('Got data from Yext but it does not contain a "response" key.');
      }
    }
    catch (\Throwable $t) {
      $this->watchdogThrowable($t);
      $t_args = [
        '@i' => $candidate->id(),
        '@t' => $t->getMessage(),
        '@yext_id' => $id,
      ];
      if ($this->configGet('unpublish_node_if_id_invalid', FALSE)) {
        $message = $this->t('Unpublishing node with nid @i (Yext id @yext_id) because we got the error @t from Yext.', $t_args);
        $this->drupalSetMessage($message);
        if (is_a($candidate, YextTargetNode::class)) {
          $candidate->setYextRawData(json_encode($message));
        }
        $candidate->unpublish();
      }
      else {
        $this->drupalSetMessage($this->t('We got error @t from the server but we will not unpublish node @i (Yext id @yext_id)', $t_args));
      }
    }
  }

  /**
   * Update the raw data field but only if this is required.
   *
   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
   *   A Drupal entity.
   */
  public function updateRaw(FieldableEntityInterface $entity) {
    if ($this->rawUpdatable($entity)) {
      $candidate = YextEntityFactory::instance()->entity($entity);
      $this->drupalSetMessage($this->t('Will try to update data from Yext for node with nid @i', ['@i' => $entity->id()]));

      $this->updateEntityFromId($candidate);
    }
    else {
      $this->drupalSetMessage($this->t('Will not try to update data from Yext for node with nid @i', ['@i' => $entity->id()]));
    }
  }

  /**
   * Update the number representing the nodes remaining to import.
   */
  public function updateRemaining() {
    $start = $this->nextDateToImport('Y-m-d', 3 * 24 * 60 * 60);
    $end = $this->date('Y-m-d', intval($this->date('U')) + 24 * 60 * 60);
    $this->watchdog('Yext: query between ' . $start . ' and ' . $end);
    try {
      $result = $this->queryYext($start, $end);
    }
    catch (\Exception $e) {
      $this->watchdog('Yext: ' . $e->getMessage());
      $result = [];
    }
    if (!empty($result['response']['count'])) {
      $count = $result['response']['count'];
      $this->watchdog('Yext: updating remaining to ' . $count . '.');
      $this->stateSet('drupal_yext_remaining', $count);
    }
    else {
      $this->watchdog('Yext: could not figure out the remaining nodes.');
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc