You are here

class ViewsJsonQuery in Views Json Source 1.x

Same name and namespace in other branches
  1. 8 src/Plugin/views/query/ViewsJsonQuery.php \Drupal\views_json_source\Plugin\views\query\ViewsJsonQuery

Base query handler for views_json_source.

Plugin annotation


@ViewsQuery(
  id = "views_json_source_query",
  title = @Translation("Views Json Source Query"),
  help = @Translation("Query against API(JSON).")
)

Hierarchy

Expanded class hierarchy of ViewsJsonQuery

File

src/Plugin/views/query/ViewsJsonQuery.php, line 20

Namespace

Drupal\views_json_source\Plugin\views\query
View source
class ViewsJsonQuery extends QueryPluginBase {

  /**
   * To store the contextual Filter info.
   *
   * @var array
   */
  protected $contextualFilter;

  /**
   * Generate a query from all of the information supplied to the object.
   *
   * @param bool $get_count
   *   Provide a countquery if this is true, otherwise provide a normal query.
   */
  public function query($get_count = FALSE) {
    $filters = [];
    if (isset($this->filter)) {
      foreach ($this->filter as $filter) {
        $filters[] = $filter
          ->generate();
      }
    }

    // @todo Add an option for the filters to be 'and' or 'or'.
    return $filters;
  }

  /**
   * Builds the necessary info to execute the query.
   */
  public function build(ViewExecutable $view) {
    $view
      ->initPager();

    // Let the pager modify the query to add limits.
    $view->pager
      ->query();
    $view->build_info['query'] = $this
      ->query();
    $view->build_info['query_args'] = [];
  }

  /**
   * Fetch file.
   */
  public function fetchFile($uri) {
    $parsed = parse_url($uri);

    // Check for local file.
    if (empty($parsed['host'])) {
      if (!file_exists($uri)) {
        throw new \Exception($this
          ->t('Local file not found.'));
      }
      return file_get_contents($uri);
    }
    $destination = 'public://views_json_source';
    if (!file_prepare_directory($destination, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
      throw new \Exception($this
        ->t('Files directory either cannot be created or is not writable.'));
    }
    $headers = [];
    $cache_file = 'views_json_source_' . md5($uri);
    if ($cache = \Drupal::cache()
      ->get($cache_file)) {
      $last_headers = $cache->data;
      if (!empty($last_headers['etag'])) {
        $headers['If-None-Match'] = $last_headers['etag'];
      }
      if (!empty($last_headers['last-modified'])) {
        $headers['If-Modified-Since'] = $last_headers['last-modified'];
      }
    }
    $result = \Drupal::httpClient()
      ->get($uri, [
      'headers' => $headers,
    ]);
    if (isset($result->error)) {
      $args = [
        '%error' => $result->error,
        '%uri' => $uri,
      ];
      $message = $this
        ->t('HTTP response: %error. URI: %uri', $args);
      throw new \Exception($message);
    }
    $cache_file_uri = "{$destination}/{$cache_file}";
    if ($result
      ->getStatusCode() == 304) {
      if (file_exists($cache_file_uri)) {
        return file_get_contents($cache_file_uri);
      }

      // We have the headers but no cache file. :(
      // Run it back.
      \Drupal::cache('bin')
        ->invalidate($cache_file);
      return $this
        ->fetchFile($uri);
    }

    // As learned from Feeds caching mechanism, save to file.
    file_save_data((string) $result
      ->getBody(), $cache_file_uri, FileSystemInterface::EXISTS_REPLACE);
    \Drupal::cache()
      ->set($cache_file, $result
      ->getHeaders());
    return (string) $result
      ->getBody();
  }

  /**
   * {@inheritdoc}
   */
  public function execute(ViewExecutable $view) {
    $start = microtime(TRUE);

    // Avoid notices about $view->execute_time being undefined if the query
    // doesn't finish.
    $view->execute_time = NULL;

    // Make sure that an xml file exists.
    // This could happen if you come from the add wizard to the actual views
    // edit page.
    if (empty($this->options['json_file'])) {
      return FALSE;
    }
    $data = new \stdClass();
    try {
      $data->contents = $this
        ->fetchFile($this->options['json_file']);
    } catch (\Exception $e) {
      \Drupal::messenger()
        ->addMessage($e
        ->getMessage(), 'error');
      return;
    }

    // When content is empty, parsing it is pointless.
    if (!$data->contents) {
      if ($this->options['show_errors']) {
        \Drupal::messenger()
          ->addMessage($this
          ->t('Views Json Backend: File is empty.'), 'warning');
      }
      return;
    }

    // Go!
    $ret = $this
      ->parse($view, $data);
    $view->execute_time = microtime(TRUE) - $start;
    if (!$ret) {
      if (version_compare(phpversion(), '5.3.0', '>=')) {
        $tmp = [
          JSON_ERROR_NONE => $this
            ->t('No error has occurred'),
          JSON_ERROR_DEPTH => $this
            ->t('The maximum stack depth has been exceeded'),
          JSON_ERROR_STATE_MISMATCH => $this
            ->t('Invalid or malformed JSON'),
          JSON_ERROR_CTRL_CHAR => $this
            ->t('Control character error, possibly incorrectly encoded'),
          JSON_ERROR_SYNTAX => $this
            ->t('Syntax error'),
          JSON_ERROR_UTF8 => $this
            ->t('Malformed UTF-8 characters, possibly incorrectly encoded'),
        ];
        $msg = $tmp[json_last_error()] . ' - ' . $this->options['json_file'];
        \Drupal::messenger()
          ->addMessage($msg, 'error');
      }
      else {
        \Drupal::messenger()
          ->addMessage($this
          ->t('Views Json Backend: Parse json error') . ' - ' . $this->options['json_file'], 'error');
      }
    }
  }

  /**
   * Fetch data in array according to apath.
   *
   * @param string $apath
   *   Something like '1/name/0'.
   * @param array $array
   *   The json document content.
   *
   * @return array
   *   The json document matching the path.
   */
  public function apath($apath, array $array) {
    $r =& $array;
    $paths = explode('/', trim($apath, '//'));
    foreach ($paths as $path) {
      if ($path == '%') {

        // Replace with the contextual filter value.
        $key = $this
          ->getCurrentContextualFilter();
        $r = $r[$key];
      }
      elseif (is_array($r) && isset($r[$path])) {
        $r =& $r[$path];
      }
      elseif (is_object($r)) {
        $r =& $r->{$path};
      }
      else {
        break;
      }
    }
    return $r;
  }

  /**
   * Define ops for using in filter.
   */
  public function ops($op, $l, $r) {
    $table = [
      '=' => function ($l, $r) {
        return $l === $r;
      },
      '!=' => function ($l, $r) {
        return $l !== $r;
      },
      'contains' => function ($l, $r) {
        return strpos($l, $r) !== FALSE;
      },
      '!contains' => function ($l, $r) {
        return strpos($l, $r) === FALSE;
      },
      'shorterthan' => function ($l, $r) {
        return strlen($l) < $r;
      },
      'longerthan' => function ($l, $r) {
        return strlen($l) > $r;
      },
    ];
    return call_user_func_array($table[$op], [
      $l,
      $r,
    ]);
  }

  /**
   * Parse.
   */
  public function parse(ViewExecutable &$view, $data) {
    $ret = json_decode($data->contents, TRUE);
    if (!$ret) {
      return FALSE;
    }

    // Get rows.
    $ret = $this
      ->apath($this->options['row_apath'], $ret);

    // Filter.
    foreach ($ret as $k => $row) {
      $check = TRUE;
      foreach ($view->build_info['query'] as $filter) {

        // Filter only when value is present.
        if (!empty($filter[0])) {
          $l = $row[$filter[0]];
          $check = $this
            ->ops($filter[1], $l, $filter[2]);
          if (!$check) {
            break;
          }
        }
      }
      if (!$check) {
        unset($ret[$k]);
      }
    }
    try {
      if ($view->pager
        ->useCountQuery() || !empty($view->get_total_rows)) {

        // Hackish execute_count_query implementation.
        $view->pager->total_items = count($ret);
        if (!empty($view->pager->options['offset'])) {
          $view->pager->total_items -= $view->pager->options['offset'];
        }
        $view->pager
          ->updatePageInfo();
      }
      if (!empty($this->orderby)) {

        // Array reverse, because the most specific are first.
        foreach (array_reverse($this->orderby) as $orderby) {
          $this
            ->sort($ret, $orderby['field'], $orderby['order']);
        }
      }

      // Deal with offset & limit.
      $offset = !empty($this->offset) ? intval($this->offset) : 0;
      $limit = !empty($this->limit) ? intval($this->limit) : 0;
      $ret = $limit ? array_slice($ret, $offset, $limit) : array_slice($ret, $offset);
      $result = [];
      foreach ($ret as $row) {
        $new_row = $this
          ->parseRow(NULL, $row, $row);
        $result[] = $new_row;
      }
      foreach ($result as $row) {
        $view->result[] = new ResultRow($row);
      }

      // Re-index array.
      $index = 0;
      foreach ($view->result as &$row) {
        $row->index = $index++;
      }
      $view->total_rows = count($result);
      $view->pager
        ->postExecute($view->result);
      return TRUE;
    } catch (\Exception $e) {
      $view->result = [];
      if (!empty($view->live_preview)) {
        \Drupal::messenger()
          ->addMessage(time());
        \Drupal::messenger()
          ->addMessage($e
          ->getMessage(), 'error');
      }
      else {
        \Drupal::messenger()
          ->addMessage($e
          ->getMessage());
      }
    }
  }

  /**
   * Parse row.
   *
   * A recursive function to flatten the json object.
   * Example:
   * {person:{name:{first_name:"John", last_name:"Doe"}}}
   * becomes:
   * $row->person/name/first_name = "John",
   * $row->person/name/last_name = "Doe"
   */
  public function parseRow($parent_key, $parent_row, &$row) {
    foreach ($parent_row as $key => $value) {
      if (is_array($value)) {
        unset($row[$key]);
        $this
          ->parseRow(is_null($parent_key) ? $key : $parent_key . '/' . $key, $value, $row);
      }
      else {
        if ($parent_key) {
          $new_key = $parent_key . '/' . $key;
          $row[$new_key] = $value;
        }
        else {
          $row[$key] = $value;
        }
      }
    }
    return $row;
  }

  /**
   * Option definition.
   */
  public function defineOptions() {
    $options = parent::defineOptions();
    $options['json_file'] = [
      'default' => '',
    ];
    $options['row_apath'] = [
      'default' => '',
    ];
    $options['show_errors'] = [
      'default' => TRUE,
    ];
    return $options;
  }

  /**
   * Options form.
   */
  public function buildOptionsForm(&$form, FormStateInterface $form_state) {
    $form['json_file'] = [
      '#type' => 'textfield',
      '#title' => $this
        ->t('Json File'),
      '#default_value' => $this->options['json_file'],
      '#description' => $this
        ->t("The URL or path to the Json file."),
      '#maxlength' => 1024,
    ];
    $form['row_apath'] = [
      '#type' => 'textfield',
      '#title' => $this
        ->t('Row Apath'),
      '#default_value' => $this->options['row_apath'],
      '#description' => $this
        ->t("Apath to records.<br />Apath is just a simple array item find method. Ex:<br /><pre>['data' => \n\t['records' => \n\t\t[\n\t\t\t['firstname' => 'abc', 'lastname' => 'pqr'],\n\t\t\t['firstname' => 'xyz', 'lastname' => 'aaa']\n\t\t]\n\t]\n]</pre><br />You want 'records', so Apath could be set to 'data/records'. <br />Notice: Use '%' as wildcard to get the child contents - EG: '%/records', Also add the contextual filter to replace the wildcard('%') with 'data'."),
      '#required' => TRUE,
    ];
    $form['show_errors'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Show Json errors'),
      '#default_value' => $this->options['show_errors'],
      '#description' => $this
        ->t('If there were any errors during Json parsing, display them. It is recommended to leave this on during development.'),
      '#required' => FALSE,
    ];
  }

  /**
   * {@inheritDoc}
   */
  public function addField($table, $field, $alias = '', $params = []) {
    $alias = $field;

    // Add field info array.
    if (empty($this->fields[$field])) {
      $this->fields[$field] = [
        'field' => $field,
        'table' => $table,
        'alias' => $alias,
      ] + $params;
    }
    return $field;
  }

  /**
   * Add Order By.
   */
  public function addOrderBy($table, $field = NULL, $orderby = 'ASC') {
    $this->orderby[] = [
      'field' => $field,
      'order' => $orderby,
    ];
  }

  /**
   * Add Filter.
   */
  public function addFilter($filter) {
    $this->filter[] = $filter;
  }

  /**
   * Sort.
   */
  public function sort(&$result, $field, $order) {
    if (strtolower($order) == 'asc') {
      usort($result, $this
        ->sortAsc($field));
    }
    else {
      usort($result, $this
        ->sortDesc($field));
    }
  }

  /**
   * Sort Ascending.
   */
  public function sortAsc($key) {
    return function ($a, $b) use ($key) {
      $a_value = $a[$key] ?? '';
      $b_value = $b[$key] ?? '';
      return strnatcasecmp($a_value, $b_value);
    };
  }

  /**
   * Sort Descending.
   */
  public function sortDesc($key) {
    return function ($a, $b) use ($key) {
      $a_value = $a[$key] ?? '';
      $b_value = $b[$key] ?? '';
      return -strnatcasecmp($a_value, $b_value);
    };
  }

  /**
   * To store the filter values required to pick the node from the json.
   */
  public function addContextualFilter($filter) {
    $this->contextualFilter[] = $filter;
  }

  /**
   * To get the next filter value to pick the node from the json.
   */
  public function getCurrentContextualFilter() {
    $filter = current($this->contextualFilter);
    next($this->contextualFilter);
    return $filter;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
DependencySerializationTrait::$_entityStorages protected property
DependencySerializationTrait::$_serviceIds protected property
DependencySerializationTrait::__sleep public function 2
DependencySerializationTrait::__wakeup public function 2
MessengerTrait::$messenger protected property The messenger. 27
MessengerTrait::messenger public function Gets the messenger. 27
MessengerTrait::setMessenger public function Sets the messenger.
PluginBase::$configuration protected property Configuration information passed into the plugin. 1
PluginBase::$definition public property Plugins's definition.
PluginBase::$displayHandler public property The display object this plugin is for.
PluginBase::$options public property Options for this plugin will be held here.
PluginBase::$pluginDefinition protected property The plugin implementation definition. 1
PluginBase::$pluginId protected property The plugin_id.
PluginBase::$renderer protected property Stores the render API renderer. 3
PluginBase::$usesOptions protected property Denotes whether the plugin has an additional options form. 8
PluginBase::$view public property The top object of a view. 1
PluginBase::create public static function Creates an instance of the plugin. Overrides ContainerFactoryPluginInterface::create 63
PluginBase::DERIVATIVE_SEPARATOR constant A string which is used to separate base plugin IDs from the derivative ID.
PluginBase::destroy public function Clears a plugin. Overrides ViewsPluginInterface::destroy 2
PluginBase::doFilterByDefinedOptions protected function Do the work to filter out stored options depending on the defined options.
PluginBase::filterByDefinedOptions public function Filter out stored options depending on the defined options. Overrides ViewsPluginInterface::filterByDefinedOptions
PluginBase::getAvailableGlobalTokens public function Returns an array of available token replacements. Overrides ViewsPluginInterface::getAvailableGlobalTokens
PluginBase::getBaseId public function Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface::getBaseId
PluginBase::getDerivativeId public function Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface::getDerivativeId
PluginBase::getPluginDefinition public function Gets the definition of the plugin implementation. Overrides PluginInspectionInterface::getPluginDefinition 2
PluginBase::getPluginId public function Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface::getPluginId
PluginBase::getProvider public function Returns the plugin provider. Overrides ViewsPluginInterface::getProvider
PluginBase::getRenderer protected function Returns the render API renderer. 1
PluginBase::globalTokenForm public function Adds elements for available core tokens to a form. Overrides ViewsPluginInterface::globalTokenForm
PluginBase::globalTokenReplace public function Returns a string with any core tokens replaced. Overrides ViewsPluginInterface::globalTokenReplace
PluginBase::INCLUDE_ENTITY constant Include entity row languages when listing languages.
PluginBase::INCLUDE_NEGOTIATED constant Include negotiated languages when listing languages.
PluginBase::init public function Initialize the plugin. Overrides ViewsPluginInterface::init 6
PluginBase::isConfigurable public function Determines if the plugin is configurable.
PluginBase::listLanguages protected function Makes an array of languages, optionally including special languages.
PluginBase::pluginTitle public function Return the human readable name of the display. Overrides ViewsPluginInterface::pluginTitle
PluginBase::preRenderAddFieldsetMarkup public static function Moves form elements into fieldsets for presentation purposes. Overrides ViewsPluginInterface::preRenderAddFieldsetMarkup
PluginBase::preRenderFlattenData public static function Flattens the structure of form elements. Overrides ViewsPluginInterface::preRenderFlattenData
PluginBase::queryLanguageSubstitutions public static function Returns substitutions for Views queries for languages.
PluginBase::setOptionDefaults protected function Fills up the options of the plugin with defaults.
PluginBase::themeFunctions public function Provide a full list of possible theme templates used by this style. Overrides ViewsPluginInterface::themeFunctions 1
PluginBase::trustedCallbacks public static function Lists the trusted callbacks provided by the implementing class. Overrides TrustedCallbackInterface::trustedCallbacks 6
PluginBase::unpackOptions public function Unpack options over our existing defaults, drilling down into arrays so that defaults don't get totally blown away. Overrides ViewsPluginInterface::unpackOptions
PluginBase::usesOptions public function Returns the usesOptions property. Overrides ViewsPluginInterface::usesOptions 8
PluginBase::validate public function Validate that the plugin is correct and can be saved. Overrides ViewsPluginInterface::validate 6
PluginBase::viewsTokenReplace protected function Replaces Views' tokens in a given string. The resulting string will be sanitized with Xss::filterAdmin. 1
PluginBase::VIEWS_QUERY_LANGUAGE_SITE_DEFAULT constant Query string to indicate the site default language.
PluginBase::__construct public function Constructs a PluginBase object. Overrides PluginBase::__construct
QueryPluginBase::$limit protected property Stores the limit of items that should be requested in the query.
QueryPluginBase::$pager public property A pager plugin that should be provided by the display.
QueryPluginBase::addSignature public function Add a signature to the query, if such a thing is feasible. 1
QueryPluginBase::alter public function Let modules modify the query just prior to finalizing it. 1
QueryPluginBase::calculateDependencies public function Calculates dependencies for the configured plugin. Overrides PluginBase::calculateDependencies 1
QueryPluginBase::getAggregationInfo public function Get aggregation info for group by queries. 1
QueryPluginBase::getCacheContexts public function The cache contexts associated with this object. Overrides CacheableDependencyInterface::getCacheContexts
QueryPluginBase::getCacheMaxAge public function The maximum age for which this object may be cached. Overrides CacheableDependencyInterface::getCacheMaxAge 1
QueryPluginBase::getCacheTags public function The cache tags associated with this object. Overrides CacheableDependencyInterface::getCacheTags 1
QueryPluginBase::getDateField public function Returns a Unix timestamp to database native timestamp expression. 1
QueryPluginBase::getDateFormat public function Creates cross-database date formatting. 1
QueryPluginBase::getEntityTableInfo public function Returns an array of all tables from the query that map to an entity type.
QueryPluginBase::getLimit public function Returns the limit of the query.
QueryPluginBase::getTimezoneOffset public function Get the timezone offset in seconds.
QueryPluginBase::loadEntities public function Loads all entities contained in the passed-in $results. 1
QueryPluginBase::setFieldTimezoneOffset public function Applies a timezone offset to the given field. 2
QueryPluginBase::setGroupOperator public function Control how all WHERE and HAVING groups are put together.
QueryPluginBase::setLimit public function Set a LIMIT on the query, specifying a maximum number of results.
QueryPluginBase::setOffset public function Set an OFFSET on the query, specifying a number of results to skip.
QueryPluginBase::setupTimezone public function Set the database to the current user timezone. 1
QueryPluginBase::setWhereGroup public function Create a new grouping for the WHERE or HAVING clause.
QueryPluginBase::submitOptionsForm public function Handle any special handling on the validate form. Overrides PluginBase::submitOptionsForm 1
QueryPluginBase::summaryTitle public function Returns the summary of the settings in the display. Overrides PluginBase::summaryTitle
QueryPluginBase::validateOptionsForm public function Validate the options form. Overrides PluginBase::validateOptionsForm
StringTranslationTrait::$stringTranslation protected property The string translation service. 4
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.
TrustedCallbackInterface::THROW_EXCEPTION constant Untrusted callbacks throw exceptions.
TrustedCallbackInterface::TRIGGER_SILENCED_DEPRECATION constant Untrusted callbacks trigger silenced E_USER_DEPRECATION errors.
TrustedCallbackInterface::TRIGGER_WARNING constant Untrusted callbacks trigger E_USER_WARNING errors.
ViewsJsonQuery::$contextualFilter protected property To store the contextual Filter info.
ViewsJsonQuery::addContextualFilter public function To store the filter values required to pick the node from the json.
ViewsJsonQuery::addField public function
ViewsJsonQuery::addFilter public function Add Filter.
ViewsJsonQuery::addOrderBy public function Add Order By.
ViewsJsonQuery::apath public function Fetch data in array according to apath.
ViewsJsonQuery::build public function Builds the necessary info to execute the query. Overrides QueryPluginBase::build
ViewsJsonQuery::buildOptionsForm public function Options form. Overrides PluginBase::buildOptionsForm
ViewsJsonQuery::defineOptions public function Option definition. Overrides PluginBase::defineOptions
ViewsJsonQuery::execute public function Executes the query and fills the associated view object with according values. Overrides QueryPluginBase::execute
ViewsJsonQuery::fetchFile public function Fetch file.
ViewsJsonQuery::getCurrentContextualFilter public function To get the next filter value to pick the node from the json.
ViewsJsonQuery::ops public function Define ops for using in filter.
ViewsJsonQuery::parse public function Parse.
ViewsJsonQuery::parseRow public function Parse row.
ViewsJsonQuery::query public function Generate a query from all of the information supplied to the object. Overrides QueryPluginBase::query
ViewsJsonQuery::sort public function Sort.
ViewsJsonQuery::sortAsc public function Sort Ascending.
ViewsJsonQuery::sortDesc public function Sort Descending.