dvf-2.x-dev/dvf_ckan/src/Plugin/Visualisation/Source/CkanResource.php
dvf_ckan/src/Plugin/Visualisation/Source/CkanResource.php
<?php
namespace Drupal\dvf_ckan\Plugin\Visualisation\Source;
use Drupal\ckan_connect\Client\CkanClientInterface;
use Drupal\ckan_connect\Parser\CkanResourceUrlParserInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\dvf\DvfHelpers;
use Drupal\dvf\Plugin\VisualisationInterface;
use Drupal\dvf\Plugin\Visualisation\Source\VisualisationSourceBase;
use GuzzleHttp\Client;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Plugin implementation of the 'dvf_ckan_resource' visualisation source.
*
* @VisualisationSource(
* id = "dvf_ckan_resource",
* label = @Translation("CKAN resource"),
* category = @Translation("CKAN"),
* visualisation_types = {
* "dvf_file",
* "dvf_url"
* }
* )
*/
class CkanResource extends VisualisationSourceBase implements ContainerFactoryPluginInterface {
/**
* The cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The CKAN client.
*
* @var \Drupal\ckan_connect\Client\CkanClientInterface
*/
protected $ckanClient;
/**
* The CKAN resource URL parser.
*
* @var \Drupal\ckan_connect\Parser\CkanResourceUrlParserInterface
*/
protected $ckanResourceUrlParser;
/**
* The CKAN resource fields.
*
* @var array
*/
protected $fields;
/**
* The CKAN resource records.
*
* @var array
*/
protected $records;
/**
* DVF Helpers.
*
* @var \Drupal\dvf\DvfHelpers
*/
protected $dvfHelpers;
/**
* Constructs a new CkanResource.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\dvf\Plugin\VisualisationInterface $visualisation
* The visualisation context in which the plugin will run.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Psr\Log\LoggerInterface $logger
* Instance of the logger object.
* @param \GuzzleHttp\Client $http_client
* The HTTP client.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\ckan_connect\Client\CkanClientInterface $ckan_client
* The CKAN client.
* @param \Drupal\ckan_connect\Parser\CkanResourceUrlParserInterface $ckan_resource_url_parser
* The CKAN resource URL parser.
* @param \Drupal\dvf\DvfHelpers $dvf_helpers
* The DVF helpers.
* @param \Drupal\Core\File\FileUrlGeneratorInterface $file_url_generator
* The file URL generator.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
VisualisationInterface $visualisation = NULL,
ModuleHandlerInterface $module_handler,
LoggerInterface $logger,
Client $http_client,
CacheBackendInterface $cache,
CkanClientInterface $ckan_client,
CkanResourceUrlParserInterface $ckan_resource_url_parser,
DvfHelpers $dvf_helpers,
FileUrlGeneratorInterface $file_url_generator
) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $visualisation, $module_handler, $logger, $http_client, $file_url_generator);
$this->cache = $cache;
$this->ckanClient = $ckan_client;
$this->ckanResourceUrlParser = $ckan_resource_url_parser;
$this->dvfHelpers = $dvf_helpers;
}
/**
* Creates an instance of the plugin.
*
* @param \Symfony\Component\DependencyInjection\ContainerInterface $container
* The container to pull out services used in the plugin.
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\dvf\Plugin\VisualisationInterface $visualisation
* The visualisation context in which the plugin will run.
*
* @return static
* Returns an instance of this plugin.
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
VisualisationInterface $visualisation = NULL
) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$visualisation,
$container->get('module_handler'),
$container->get('logger.channel.dvf'),
$container->get('http_client'),
$container->get('cache.dvf_ckan'),
$container->get('ckan_connect.client'),
$container->get('ckan_connect.resource_url_parser'),
$container->get('dvf.helpers'),
$container->get('file_url_generator')
);
}
/**
* {@inheritdoc}
*/
public function getFields() {
// Generate a cache key for fields.
$cache_key = $this->getCacheKey('fields');
$cache_object = $this->cache->get($cache_key);
// Use cache data if it exists, otherwise fetch fields and store it in cache.
if (is_object($cache_object)) {
$raw_fields = $cache_object->data;
}
else {
$raw_fields = $this->fetchFields();
$this->cache->set($cache_key, $raw_fields, $this->getCacheExpiry());
}
$fields = [];
foreach ($raw_fields as $field) {
$fields[$field->id] = $field->id;
}
return $fields;
}
/**
* Fetches the fields.
*
* @return array
* An array of CKAN resource fields.
*/
protected function fetchFields() {
// Construct a CKAN API query to retrieve fields for the specified resource ID.
$query = [
'id' => $this->getResourceId(),
'limit' => 1,
];
// Merge additional data filters into the CKAN API query.
$query = array_merge($query, $this->getDataFilters());
// Use the CKAN client to make a request to 'action/datastore_search'.
try {
$response = $this->ckanClient->get('action/datastore_search', $query);
$result = ($response->success === TRUE) ? $response->result : NULL;
}
catch (\Exception $e) {
$this->ckanResourceError('fields', $query['id'], $e->getMessage());
$result = NULL;
}
// Return the fields obtained from the CKAN datastore or an empty array if there are issues.
return ($result) ? $result->fields : [];
}
/**
* {@inheritdoc}
*/
public function getRecords() {
$cache_key = $this->getCacheKey('records');
$cache_object = $this->cache->get($cache_key);
if (is_object($cache_object)) {
$raw_records = $cache_object->data;
}
else {
$raw_records = $this->fetchRecords();
$this->cache->set($cache_key, $raw_records, $this->getCacheExpiry());
}
$records = [];
foreach ($this->getFields() as $field_id => $field_label) {
foreach ($raw_records as $record) {
if (!isset($records[$record->_id])) {
$records[$record->_id] = new \stdClass();
}
$records[$record->_id]->{$field_id} = $record->{$field_id};
}
}
return $records;
}
/**
* Fetches the records.
*
* @param array $records
* (optional) The records.
* @param int $limit
* (optional) The maximum number of records per request.
* @param int $offset
* (optional) The number of records to skip.
*
* @return array
* An array of records.
*/
protected function fetchRecords(array $records = [], $limit = 100, $offset = 0) {
$query = [
'id' => $this->getResourceId(),
'limit' => $limit,
'offset' => $offset,
];
$query = array_merge($query, $this->getDataFilters());
// Try to fetch records from the CKAN datastore using the CKAN client.
try {
$response = $this->ckanClient->get('action/datastore_search', $query);
$result = ($response->success === TRUE) ? $response->result : NULL;
}
catch (\Exception $e) {
$this->ckanResourceError('records', $query['id'], $e->getMessage());
$result = NULL;
}
// Update records, total, and offset based on the fetched result.
$records = ($result) ? array_merge($records, $result->records) : [];
$total = ($result) ? $result->total : 0;
$offset = count($records);
// If there are more records to fetch (total is greater than the current offset),
// recursively call fetchRecords with updated parameters.
if ($total > $offset) {
return $this->fetchRecords($records, $limit, $offset);
}
else {
return $records;
}
}
/**
* Gets the CKAN resource ID for this plugin.
*
* @return string
* The CKAN resource ID.
*/
protected function getResourceId() {
return $this->ckanResourceUrlParser->getResourceId($this->config('uri'));
}
/**
* Gets a cache key for a given type.
*
* @param string $object_type
* The object type.
*
* @return string
* The cache key.
*/
protected function getCacheKey($object_type) {
$plugin_id = hash('sha256', $this->getPluginId());
$resource_id = $this->getResourceId();
return $plugin_id . ':' . $resource_id . ':' . $object_type;
}
/**
* Gets the optional data filters and returns them in the correct format.
*
* @return array
* An array of data filters.
*/
protected function getDataFilters() {
// Retrieve configuration options for the visualisation's style.
$configuration_options = $this->visualisation->getConfiguration('style');
$data_filters = [];
// We bail early if data filters are not specified in the visualisation configuration.
if (empty($configuration_options['style']['options']['data']['data_filters'])) {
return $data_filters;
}
// Extract data filters from the visualisation configuration.
$data_filters = $configuration_options['style']['options']['data']['data_filters'];
// Check and validate if 'filters' key exists and contains valid JSON.
if (empty($data_filters['filters']) || !$this->dvfHelpers->validateJson($data_filters['filters'])) {
unset($data_filters['filters']);
}
if (empty($data_filters['q'])) {
unset($data_filters['q']);
}
return array_map('trim', $data_filters);
}
/**
* Generic error message for errors accessing resources.
*
* @param string $type
* Error type, eg "records" or "fields".
* @param string $resource_id
* The resource ID that we attempted to access.
* @param string $exception_message
* The exception message.
*/
protected function ckanResourceError($type, $resource_id, $exception_message) {
$this->messenger()->addError($this->t('Error displaying CKAN data'));
$message = $this->t(
'There was an error getting CKAN :type data for :id. Error :message',
[
':type' => $type,
':id' => $resource_id,
':message' => $exception_message,
]
);
$this->logger->error($message);
}
}
