View source
<?php
namespace Drupal\views_json_source\Plugin\views\query;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\query\QueryPluginBase;
class ViewsJsonQuery extends QueryPluginBase {
protected $contextualFilter;
public function query($get_count = FALSE) {
$filters = [];
if (isset($this->filter)) {
foreach ($this->filter as $filter) {
$filters[] = $filter
->generate();
}
}
return $filters;
}
public function build(ViewExecutable $view) {
$view
->initPager();
$view->pager
->query();
$view->build_info['query'] = $this
->query();
$view->build_info['query_args'] = [];
}
public function fetchFile($uri) {
$parsed = parse_url($uri);
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);
}
\Drupal::cache('bin')
->invalidate($cache_file);
return $this
->fetchFile($uri);
}
file_save_data((string) $result
->getBody(), $cache_file_uri, FileSystemInterface::EXISTS_REPLACE);
\Drupal::cache()
->set($cache_file, $result
->getHeaders());
return (string) $result
->getBody();
}
public function execute(ViewExecutable $view) {
$start = microtime(TRUE);
$view->execute_time = NULL;
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;
}
if (!$data->contents) {
if ($this->options['show_errors']) {
\Drupal::messenger()
->addMessage($this
->t('Views Json Backend: File is empty.'), 'warning');
}
return;
}
$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');
}
}
}
public function apath($apath, array $array) {
$r =& $array;
$paths = explode('/', trim($apath, '//'));
foreach ($paths as $path) {
if ($path == '%') {
$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;
}
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,
]);
}
public function parse(ViewExecutable &$view, $data) {
$ret = json_decode($data->contents, TRUE);
if (!$ret) {
return FALSE;
}
$ret = $this
->apath($this->options['row_apath'], $ret);
foreach ($ret as $k => $row) {
$check = TRUE;
foreach ($view->build_info['query'] as $filter) {
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)) {
$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)) {
foreach (array_reverse($this->orderby) as $orderby) {
$this
->sort($ret, $orderby['field'], $orderby['order']);
}
}
$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);
}
$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());
}
}
}
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;
}
public function defineOptions() {
$options = parent::defineOptions();
$options['json_file'] = [
'default' => '',
];
$options['row_apath'] = [
'default' => '',
];
$options['show_errors'] = [
'default' => TRUE,
];
return $options;
}
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,
];
}
public function addField($table, $field, $alias = '', $params = []) {
$alias = $field;
if (empty($this->fields[$field])) {
$this->fields[$field] = [
'field' => $field,
'table' => $table,
'alias' => $alias,
] + $params;
}
return $field;
}
public function addOrderBy($table, $field = NULL, $orderby = 'ASC') {
$this->orderby[] = [
'field' => $field,
'order' => $orderby,
];
}
public function addFilter($filter) {
$this->filter[] = $filter;
}
public function sort(&$result, $field, $order) {
if (strtolower($order) == 'asc') {
usort($result, $this
->sortAsc($field));
}
else {
usort($result, $this
->sortDesc($field));
}
}
public function sortAsc($key) {
return function ($a, $b) use ($key) {
$a_value = $a[$key] ?? '';
$b_value = $b[$key] ?? '';
return strnatcasecmp($a_value, $b_value);
};
}
public function sortDesc($key) {
return function ($a, $b) use ($key) {
$a_value = $a[$key] ?? '';
$b_value = $b[$key] ?? '';
return -strnatcasecmp($a_value, $b_value);
};
}
public function addContextualFilter($filter) {
$this->contextualFilter[] = $filter;
}
public function getCurrentContextualFilter() {
$filter = current($this->contextualFilter);
next($this->contextualFilter);
return $filter;
}
}