external_entity-1.0.x-dev/src/Plugin/views/query/ExternalEntityQuery.php
src/Plugin/views/query/ExternalEntityQuery.php
<?php
declare(strict_types=1);
namespace Drupal\external_entity\Plugin\views\query;
use Drupal\views\ViewExecutable;
use Drupal\Core\Form\FormStateInterface;
use Drupal\external_entity\AjaxFormTrait;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\external_entity\ExternalEntityOptions;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\views\Plugin\views\join\JoinPluginBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Database\DatabaseExceptionWrapper;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\external_entity\Contracts\ExternalEntityInterface;
use Drupal\external_entity\Plugin\views\ExternalEntityResultRow;
use Drupal\external_entity\Contracts\ExternalEntityStorageInterface;
use Drupal\external_entity\Definition\ExternalEntitySearchDefinition;
use Drupal\external_entity\Definition\ExternalEntityDefaultDefinition;
/**
* Define the external entity views sql query plugin.
*
* @ViewsQuery(
* id = "external_entity",
* title = @Translation("External Entity")
* )
*/
class ExternalEntityQuery extends QueryPluginBase {
use AjaxFormTrait;
/**
* @var array
*/
protected $where = [];
/**
* @var array
*/
protected $orderBy = [];
/**
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\external_entity\ExternalEntityOptions
*/
protected $externalEntityOptions;
/**
* Define the class constructor.
*
* @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.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
ModuleHandlerInterface $module_handler,
EntityTypeManagerInterface $entity_type_manager,
ExternalEntityOptions $external_entity_options,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleHandler = $module_handler;
$this->entityTypeManager = $entity_type_manager;
$this->externalEntityOptions = $external_entity_options;
}
/**
* {@inheritDoc}
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('module_handler'),
$container->get('entity_type.manager'),
$container->get('external_entity.options')
);
}
/**
* @param \Drupal\views\ViewExecutable $view
*/
public function build(ViewExecutable $view): void {
$this->view = $view;
$view->initPager();
$view->pager->query();
$view->build_info['query'] = $this->query();
}
/**
* {@inheritDoc}
*/
public function buildOptionsForm(
&$form,
FormStateInterface $form_state,
): void {
$parents = ['query', 'options'];
$options = $this->externalEntityOptions;
$option_types = $options->types();
$type = $this->getFormStateValue(
array_merge($parents, ['type']),
$form_state,
$this->externalEntityTypeId()
);
$form['type'] = [
'#type' => 'select',
'#title' => $this->t('Type'),
'#description' => $this->t(
'Select the external entity type.'
),
'#options' => $option_types,
'#default_value' => $type,
'#empty_option' => $this->t('- Select -'),
];
views_ui_add_ajax_trigger(
$form,
'type',
array_merge(['options'], $parents)
);
if (isset($type) && !empty($type)) {
$form['resource'] = [
'#type' => 'select',
'#title' => $this->t('Resource'),
'#description' => $this->t(
'Select the external entity resource.'
),
'#options' => $options->resources($type),
'#empty_option' => $this->t('- Select -'),
'#default_value' => $this->getFormStateValue(
array_merge($parents, ['resource']),
$form_state,
$this->externalEntityResource()
),
];
}
}
/**
* {@inheritDoc}
*/
public function query($get_count = FALSE): QueryInterface {
$query = $this->getResourceQuery();
if (!empty($this->where)) {
foreach ($this->where as $where) {
if (!isset($where['conditions'])) {
continue;
}
foreach ($where['conditions'] as $condition) {
if (!isset($condition['value'])) {
continue;
}
$this->processCondition($condition);
$query->condition(
$condition['field'],
$condition['value'],
$condition['operator'] ?? '='
);
}
}
}
if (!empty($this->orderBy)) {
foreach ($this->orderBy as $order) {
if (!isset($order['field'])) {
continue;
}
$field = ltrim($order['field'], '.');
$query->sort($field, $order['direction'] ?? 'ASC');
}
}
return $query;
}
/**
* Add a simple WHERE clause to the query.
*
* The $field, $value and $operator arguments can also be passed in with a
* single DatabaseCondition object, like this:
* @code
* $this->query->addWhere(
* $this->options['group'],
* (new Condition('OR'))
* ->condition($field, $value, 'NOT IN')
* ->condition($field, $value, 'IS NULL')
* );
* @endcode
*
* @param mixed $group
* The WHERE group to add these to; groups are used to create AND/OR
* sections. Groups cannot be nested. Use 0 as the default group.
* If the group does not yet exist it will be created as an AND group.
* @param string $field
* The name of the field to check.
* @param mixed $value
* The value to test the field against. In most cases, this is a scalar.
* For more complex options, it is an array. The meaning of each element in
* the array is dependent on the $operator.
* @param string $operator
* The comparison operator, such as =, <, or >=. It also accepts more
* complex options such as IN, LIKE, LIKE BINARY, or BETWEEN. Defaults to
* =.
* If $field is a string you have to use 'formula' here.
*
* @see \Drupal\Core\Database\Query\ConditionInterface::condition()
* @see \Drupal\Core\Database\Query\Condition
*/
public function addWhere($group, $field, $value = NULL, $operator = NULL): void {
if (empty($group)) {
$group = 0;
}
if (!isset($this->where[$group])) {
$this->setWhereGroup('AND', $group);
}
$this->where[$group]['conditions'][] = [
'field' => $field,
'value' => $value,
'operator' => $operator,
];
}
/**
* Add a complex WHERE clause to the query.
*
* @param mixed $group
* The WHERE group to add these to; groups are used to create AND/OR
* sections. Groups cannot be nested. Use 0 as the default group.
* If the group does not yet exist it will be created as an AND group.
* @param string $snippet
* The snippet to check. This can be either a column or
* a complex expression like "UPPER(table.field) = 'value'".
* @param array $args
* An associative array of arguments.
*
* @see QueryConditionInterface::where()
*/
public function addWhereExpression(int $group, string $snippet, array $args = []): void {
if (empty($group)) {
$group = 0;
}
if (!isset($this->where[$group])) {
$this->setWhereGroup('AND', $group);
}
$this->where[$group]['conditions'][] = [
'field' => $snippet,
'value' => $args,
'operator' => 'formula',
];
}
/**
* Add an ORDER BY clause to the query.
*
* @param string|null $table
* The table this field is part of. If a formula, enter NULL.
* If you want to orderby random use "rand" as table and nothing else.
* @param string|null $field
* The field or formula to sort on. If already a field, enter NULL
* and put in the alias.
* @param string $order
* Either ASC or DESC.
* @param string $alias
* The alias to add the field as. In SQL, all fields in the order by
* must also be in the SELECT portion. If an $alias isn't specified
* one will be generated for from the $field; however, if the
* $field is a formula, this alias will likely fail.
* @param array $params
* Any params that should be passed through to the
* addField.
*/
public function addOrderBy(
?string $table,
?string $field = NULL,
string $order = 'ASC',
string $alias = '',
array $params = [],
): void {
$this->orderBy[] = [
'field' => $field,
'direction' => strtoupper($order),
];
}
/**
* {@inheritDoc}
*/
public function loadEntities(&$results): void {
/** @var \Drupal\external_entity\Plugin\views\ExternalEntityResultRow $result */
foreach ($results as &$result) {
if (!$result instanceof ExternalEntityResultRow || !$result->hasDefinition()) {
continue;
}
$definition = $result->definition;
$result->_entity = $this->createExternalEntityFromDefinition($definition);
}
}
/**
* {@inheritDoc}
*/
public function execute(ViewExecutable $view): void {
$index = 0;
$start = microtime(TRUE);
/** @var \Drupal\views\Plugin\views\pager\PagerPluginBase $pager */
$pager = $view->pager;
/** @var \Drupal\Core\Entity\Query\QueryInterface $query */
$query = $view->build_info['query'];
try {
if (!empty($this->limit) || !empty($this->offset)) {
$limit = (int) (!empty($this->limit) ? $this->limit : 999999);
$offset = (int) (!empty($this->offset) ? $this->offset : 0);
$query->range($offset, $limit);
}
/** @var \Drupal\external_entity\Definition\ExternalEntitySearchDefinition $response */
if ($response = $query->execute()) {
$results = $response->getResults();
$search_info = $response->getInfo();
$pager->total_items = $search_info['total'] ?? 0;
$pager->postExecute($results);
$pager->updatePageInfo();
foreach ($results as $definition) {
if ($entity = $this->createExternalEntityFromDefinition($definition)) {
$row = [
'index' => $index,
'_entity' => $entity,
'definition' => $definition,
];
$view->result[$index] = new ExternalEntityResultRow($row);
$index++;
}
}
}
}
catch (DatabaseExceptionWrapper $exception) {
$view->result = [];
if (property_exists($view, 'live_preview') && $view->live_preview) {
$this->messenger->addError(
$exception->getMessage()
);
}
else {
throw new DatabaseExceptionWrapper(
"Exception in {$view->storage->label()}[{$view->storage->id()}]: {$exception->getMessage()}"
);
}
}
$view->execute_time = microtime(TRUE) - $start;
}
/**
* Create an external entity from definition.
*
* @param \Drupal\external_entity\Definition\ExternalEntityDefaultDefinition $definition
* External entity definition.
*
* @return \Drupal\external_entity\Contracts\ExternalEntityInterface
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function createExternalEntityFromDefinition(
ExternalEntityDefaultDefinition $definition,
): ExternalEntityInterface {
return $this->externalEntityStorage()->createEntityFromDefinition(
$this->externalEntityTypeId(),
$definition
);
}
/**
* {@inheritDoc}
*/
public function addField(
$table,
$field,
$alias = '',
$params = [],
): string {
return $field;
}
/**
* {@inheritDoc}
*/
public function ensureTable(
$table,
$relationship = NULL,
?JoinPluginBase $join = NULL,
): ?string {
return NULL;
}
/**
* Process query condition.
*
* @param array $condition
* An array of query condition.
*
* @return void
*/
protected function processCondition(array &$condition): void {
$condition['field'] = ltrim($condition['field'], '.');
if (isset($condition['operator']) && $condition['operator'] === 'formula') {
$info = $this->parseFormulaField($condition['field']);
if (isset($info['field'], $info['operator'], $info['value'])) {
$condition['field'] = $info['field'];
$condition['value'] = $info['value'];
$condition['operator'] = $info['operator'];
}
}
if (isset($condition['value']) && !empty($condition['value'])) {
foreach ($this->getQuerySubstitutions() as $key => $substitution) {
$substitution = is_array($substitution)
? array_map('strval', $substitution)
: (string) $substitution;
$condition['value'] = is_array($condition['value'])
? array_map('strval', $condition['value'])
: (string) $condition['value'];
$condition['value'] = str_replace($key, $substitution, $condition['value']);
}
}
if (is_string($condition['value'])) {
$condition['value'] = $this->processArithmeticExpression(
$condition['value']
);
}
}
/**
* Get view query substitutions.
*
* @return array
* An array of views query substitutions.
*/
protected function getQuerySubstitutions(): array {
return $this->moduleHandler->invokeAll(
'views_query_substitutions',
[$this->view]
);
}
/**
* Process simple string arithmetic expression.
*
* @param string $expression
* An arithmetic expression.
*
* @return mixed
* The arithmetic expression value; otherwise the original expression.
*/
protected function processArithmeticExpression(string $expression) {
$matches = [];
$pattern = '/^(\d+)\s?([\+\-\*\/])\s?(\d+)$/';
if (preg_match($pattern, $expression, $matches)) {
array_shift($matches);
[$number1, $operator, $number2] = $matches;
switch ($operator) {
case '+':
$expression = $number1 + $number2;
break;
case '-':
$expression = $number1 - $number2;
break;
case '*':
$expression = $number1 * $number2;
break;
case '/':
$expression = $number1 / $number2;
break;
}
}
return $expression;
}
/**
* Parse formula field.
*
* @param string $field
* The field that contains the formula.
* @param string $delimiter
* The field statement delimiter.
*
* @return array
* An array of the formula information broken out into the following:
* - field
* - value
* - operator
* - conjunction
*/
protected function parseFormulaField(string $field, string $delimiter = ' '): array {
$info = [];
$keywords = $this->queryOperatorKeywords();
foreach (explode($delimiter, $field) as $segment) {
$key = 'value';
if (strpos($segment, 'field_') !== FALSE) {
$key = 'field';
}
elseif (in_array($segment, $keywords, TRUE)) {
$key = 'operator';
}
elseif (in_array($segment, ['AND', 'OR'], TRUE)) {
$key = 'conjunction';
}
$info[$key][] = $segment;
}
array_walk($info, static function (&$value) {
if (count($value) === 1) {
$value = reset($value);
}
});
return $info;
}
/**
* Define the query operator keywords.
*
* @return string[]
* An array of the query operator keywords.
*/
protected function queryOperatorKeywords(): array {
return [
'=',
'<>',
'>',
'>=',
'<',
'<=',
'IN',
'BETWEEN',
'CONTAINS',
'ENDS_WITH',
'STARTS_WITH',
];
}
/**
* @param \Drupal\Core\Entity\Query\QueryInterface $query
* @param int $limit
*
* @return \Drupal\external_entity\Definition\ExternalEntitySearchDefinition
*/
protected function searchResourceDefinitions(
QueryInterface $query,
int $limit = 10,
): ExternalEntitySearchDefinition {
return $query->pager($limit)->execute();
}
/**
* Get the external entity resource query instance.
*
* @return \Drupal\Core\Entity\Query\QueryInterface
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function getResourceQuery(): QueryInterface {
return $this->externalEntityStorage()->getResourceQuery(
$this->externalEntityResource(),
$this->externalEntityTypeId()
);
}
/**
* Get external entity type.
*
* @return string|null
* The external entity type ID.
*/
public function externalEntityTypeId(): ?string {
return $this->options['type_of'] ?? NULL;
}
/**
* Get the external entity resource.
*
* @return string|null
* The external entity resource.
*/
public function externalEntityResource(): ?string {
return $this->options['resource'] ?? NULL;
}
/**
* External entity storage instance.
*
* @return \Drupal\external_entity\Contracts\ExternalEntityStorageInterface
* The external entity storage instance.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
protected function externalEntityStorage(): ExternalEntityStorageInterface {
return $this->entityTypeManager->getStorage('external_entity');
}
}
