Commit 4ea00421 authored by bojanz's avatar bojanz Committed by tim.plunkett

Issue #1758634 by bojanz: The query plugin should load entities after the query has been executed.

parent 1f4c2c16
<?php
/**
* @file
* Definition of Drupal\views\Plugin\views\field\Entity.
*/
namespace Drupal\views\Plugin\views\field;
use Drupal\Core\Annotation\Plugin;
/**
* A handler to display data from entity objects.
*
* Fields based upon this handler work with all query-backends if the tables
* used by the query backend have an 'entity type' specified. In order to
* make fields based upon this handler automatically available to all compatible
* query backends, the views field can be defined in the table
* @code views_entity_{ENTITY_TYPE} @endcode.
*
* @ingroup views_field_handlers
*
* @Plugin(
* id = "entity"
* )
*/
class Entity extends FieldPluginBase {
/**
* Stores the entity type which is loaded by this field.
*/
public $entity_type;
/**
* Stores all entites which are in the result.
*/
public $entities;
/**
* The base field of the entity type assosiated with this field.
*/
public $base_field;
/**
* Initialize the entity type.
*/
public function init(&$view, &$options) {
parent::init($view, $options);
// Initialize the entity-type used.
$table_data = views_fetch_data($this->table);
$this->entity_type = $table_data['table']['entity type'];
}
/**
* Overriden to add the field for the entity id.
*/
function query() {
$this->table_alias = $base_table = $this->view->base_table;
$this->base_field = $this->view->base_field;
if (!empty($this->relationship)) {
foreach ($this->view->relationship as $relationship) {
if ($relationship->alias == $this->relationship) {
$base_table = $relationship->definition['base'];
$this->table_alias = $relationship->alias;
$table_data = views_fetch_data($base_table);
$this->base_field = empty($relationship->definition['base field']) ? $table_data['table']['base']['field'] : $relationship->definition['base field'];
}
}
}
// Add the field if the query back-end implements an add_field() method,
// just like the default back-end.
if (method_exists($this->query, 'add_field')) {
$this->field_alias = $this->query->add_field($this->table_alias, $this->base_field, '');
}
$this->add_additional_fields();
}
/**
* Load the entities for all rows that are about to be displayed.
*/
function pre_render(&$values) {
if (!empty($values)) {
list($this->entity_type, $this->entities) = $this->query->get_result_entities($values, !empty($this->relationship) ? $this->relationship : NULL, $this->field_alias);
}
}
/**
* Overridden to return the entity object, or a certain property of the entity.
*/
function get_value($values, $field = NULL) {
if (isset($this->entities[$this->view->row_index])) {
$entity = $this->entities[$this->view->row_index];
// Support to get a certain part of the entity.
if (isset($field) && isset($entity->{$field})) {
return $entity->{$field};
}
// Support to get a part of the values as the normal get_value.
elseif (isset($field) && isset($values->{$this->aliases[$field]})) {
return $values->{$this->aliases[$field]};
}
else {
return $entity;
}
}
return FALSE;
}
}
......@@ -362,6 +362,22 @@ function element_wrapper_classes($row_index = NULL) {
return implode(' ', $classes);
}
/**
* Get the entity matching the current row and relationship.
*
* @param $values
* An object containing all retrieved values.
*/
function get_entity($values) {
$relationship_id = $this->options['relationship'];
if ($relationship_id == 'none') {
return $values->_entity;
}
else {
return $values->_relationship_entities[$relationship_id];
}
}
/**
* Get the value that's supposed to be rendered.
*
......
......@@ -158,10 +158,14 @@ function set_group_operator($type = 'AND') {
}
/**
* Returns the according entity objects for the given query results.
* Loads all entities contained in the passed-in $results.
*.
* If the entity belongs to the base table, then it gets stored in
* $result->_entity. Otherwise, it gets stored in
* $result->_relationship_entities[$relationship_id];
*
* Query plugins that don't support entities can leave the method empty.
*/
function get_result_entities($results, $relationship = NULL) {
return FALSE;
}
function load_entities(&$results) {}
}
......@@ -1316,6 +1316,25 @@ function query($get_count = FALSE) {
$groupby = array_unique(array_merge($this->groupby, $non_aggregates));
}
// Make sure each entity table has the base field added so that the
// entities can be loaded.
$entity_tables = $this->get_entity_tables();
if ($entity_tables) {
$params = array();
if ($groupby) {
// Handle grouping, by retrieving the minimum entity_id.
$params = array(
'function' => 'min',
);
}
foreach ($entity_tables as $table_alias => $table) {
$info = entity_get_info($table['entity_type']);
$base_field = empty($table['revision']) ? $info['entity keys']['id'] : $info['entity keys']['revision'];
$this->add_field($table_alias, $base_field, '', $params);
}
}
// Add all fields to the query.
$this->compile_fields($query);
......@@ -1486,6 +1505,9 @@ function execute(&$view) {
if ($view->pager->use_count_query() || !empty($view->get_total_rows)) {
$view->total_rows = $view->pager->get_total_items();
}
// Load all entities contained in the results.
$this->load_entities($view->result);
}
catch (DatabaseExceptionWrapper $e) {
$view->result = array();
......@@ -1504,6 +1526,125 @@ function execute(&$view) {
$view->execute_time = microtime(TRUE) - $start;
}
/**
* Returns an array of all tables from the query that map to an entity type.
*
* Includes the base table and all relationships, if eligible.
* Available keys for each table:
* - base: The actual base table (i.e. "user" for an author relationship).
* - relationship_id: The id of the relationship, or "none".
* - entity_type: The entity type matching the base table.
* - revision: A boolean that specifies whether the table is a base table or
* a revision table of the entity type.
*
* @return array
* An array of table information, keyed by table alias.
*/
function get_entity_tables() {
// Start with the base table.
$entity_tables = array();
$base_table_data = views_fetch_data($this->base_table);
if (isset($base_table_data['table']['entity type'])) {
$entity_tables[$this->base_table] = array(
'base' => $this->base_table,
'relationship_id' => 'none',
'entity_type' => $base_table_data['table']['entity type'],
'revision' => FALSE,
);
}
// Include all relationships.
foreach ($this->view->relationship as $relationship_id => $relationship) {
$table_data = views_fetch_data($relationship->definition['base']);
if (isset($table_data['table']['entity type'])) {
$entity_tables[$relationship->alias] = array(
'base' => $relationship->definition['base'],
'relationship_id' => $relationship_id,
'entity_type' => $table_data['table']['entity type'],
'revision' => FALSE,
);
}
}
// Determine which of the tables are revision tables.
foreach ($entity_tables as $table_alias => $table) {
$info = entity_get_info($table['entity_type']);
if (isset($info['revision table']) && $info['revision table'] == $table['base']) {
$entity_tables[$table_alias]['revision'] = TRUE;
}
}
return $entity_tables;
}
/**
* Loads all entities contained in the passed-in $results.
*.
* If the entity belongs to the base table, then it gets stored in
* $result->_entity. Otherwise, it gets stored in
* $result->_relationship_entities[$relationship_id];
*/
function load_entities(&$results) {
$entity_tables = $this->get_entity_tables();
// No entity tables found, nothing else to do here.
if (empty($entity_tables)) {
return;
}
// Initialize the entity placeholders in $results.
foreach ($results as $index => $result) {
$results[$index]->_entity = FALSE;
$results[$index]->_relationship_entities = array();
}
// Assemble a list of entities to load.
$ids_by_table = array();
foreach ($entity_tables as $table_alias => $table) {
$entity_type = $table['entity_type'];
$info = entity_get_info($entity_type);
$id_key = empty($table['revision']) ? $info['entity keys']['id'] : $info['entity keys']['revision'];
$id_alias = $this->get_field_alias($table_alias, $id_key);
foreach ($results as $index => $result) {
// Store the entity id if it was found.
if (!empty($result->$id_alias)) {
$ids_by_table[$table_alias][$index] = $result->$id_alias;
}
}
}
// Load all entities and assign them to the correct result row.
foreach ($ids_by_table as $table_alias => $ids) {
$table = $entity_tables[$table_alias];
$entity_type = $table['entity_type'];
$relationship_id = $table['relationship_id'];
// Drupal core currently has no way to load multiple revisions. Sad.
if ($table['revision']) {
$entities = array();
foreach ($ids as $index => $revision_id) {
$entity = entity_revision_load($entity_type, $revision_id);
if ($entity) {
$entities[$revision_id] = $entity;
}
}
}
else {
$entities = entity_load_multiple($entity_type, $ids);
}
foreach ($ids as $index => $id) {
$entity = isset($entities[$id]) ? $entities[$id] : FALSE;
if ($relationship_id == 'none') {
$results[$index]->_entity = $entity;
}
else {
$results[$index]->_relationship_entities[$relationship_id] = $entity;
}
}
}
}
function add_signature(&$view) {
$view->query->add_field(NULL, "'" . $view->name . ':' . $view->current_display . "'", 'view_name');
}
......@@ -1589,50 +1730,6 @@ function get_aggregation_info() {
);
}
/**
* Returns the according entity objects for the given query results.
*
*/
function get_result_entities($results, $relationship = NULL) {
$base_table = $this->base_table;
$base_table_alias = $base_table;
if (!empty($relationship)) {
foreach ($this->view->relationship as $current) {
if ($current->alias == $relationship) {
$base_table = $current->definition['base'];
$base_table_alias = $relationship;
break;
}
}
}
$table_data = views_fetch_data($base_table);
// Bail out if the table has not specified the according entity-type.
if (!isset($table_data['table']['entity type'])) {
return FALSE;
}
$entity_type = $table_data['table']['entity type'];
$info = entity_get_info($entity_type);
$id_alias = $this->get_field_alias($base_table_alias, $info['entity keys']['id']);
// Assemble the ids of the entities to load.
$ids = array();
foreach ($results as $key => $result) {
if (isset($result->$id_alias)) {
$ids[$key] = $result->$id_alias;
}
}
$entities = entity_load_multiple($entity_type, $ids);
// Re-key the array by row-index.
$result = array();
foreach ($ids as $key => $id) {
$result[$key] = isset($entities[$id]) ? $entities[$id] : FALSE;
}
return array($entity_type, $result);
}
function aggregation_method_simple($group_type, $field) {
return strtoupper($group_type) . '(' . $field . ')';
}
......
......@@ -7,7 +7,7 @@
namespace Views\comment\Plugin\views\field;
use Drupal\views\Plugin\views\field\Entity;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\Core\Annotation\Plugin;
/**
......@@ -20,11 +20,7 @@
* module = "comment"
* )
*/
class Link extends Entity {
function construct() {
parent::construct();
}
class Link extends FieldPluginBase {
function option_definition() {
$options = parent::option_definition();
......@@ -47,19 +43,16 @@ function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
}
function query() {
$this->ensure_my_table();
$this->add_additional_fields();
}
function query() {}
function render($values) {
$value = $this->get_value($values, 'cid');
return $this->render_link($this->sanitize_value($value), $values);
$comment = $this->get_entity($values);
return $this->render_link($comment, $values);
}
function render_link($data, $values) {
$text = !empty($this->options['text']) ? $this->options['text'] : t('view');
$comment = $this->get_value($values);
$comment = $data;
$nid = $comment->nid;
$cid = $comment->id();
......
......@@ -7,7 +7,7 @@
namespace Views\comment\Plugin\views\field;
use Drupal\views\Plugin\views\field\Entity;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\Core\Annotation\Plugin;
/**
......@@ -20,22 +20,7 @@
* module = "comment"
* )
*/
class NodeLink extends Entity {
function construct() {
parent::construct();
// Add the node fields that comment_link will need..
$this->additional_fields['nid'] = array(
'field' => 'nid',
);
$this->additional_fields['type'] = array(
'field' => 'type',
);
$this->additional_fields['comment'] = array(
'field' => 'comment',
);
}
class NodeLink extends FieldPluginBase {
function option_definition() {
$options = parent::option_definition();
......@@ -54,14 +39,10 @@ function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
}
function query() {
$this->ensure_my_table();
$this->add_additional_fields();
}
function query() {}
function render($values) {
// Build fake $node.
$node = $this->get_value($values);
$node = $this->get_entity($values);
// Call comment.module's hook_link: comment_link($type, $node = NULL, $teaser = FALSE)
// Call node by reference so that something is changed here
......
......@@ -135,58 +135,15 @@ function get_base_table() {
/**
* Called to add the field to a query.
*
* By default, the only columns added to the query are entity_id and
* entity_type. This is because other needed data is fetched by entity_load().
* Other columns are added only if they are used in groupings, or if
* 'add fields to query' is specifically set to TRUE in the field definition.
*
* The 'add fields to query' switch is used by modules which need all data
* present in the query itself (such as "sphinx").
* By default, all needed data is taken from entities loaded by the query
* plugin. Columns are added only if they are used in groupings.
*/
function query($use_groupby = FALSE) {
$this->get_base_table();
$params = array();
if ($use_groupby) {
// When grouping on a "field API" field (whose "real_field" is set to
// entity_id), retrieve the minimum entity_id to have a valid entity_id to
// pass to field_view_field().
$params = array(
'function' => 'min',
);
$this->ensure_my_table();
}
// Get the entity type according to the base table of the field.
// Then add it to the query as a formula. That way we can avoid joining
// the field table if all we need is entity_id and entity_type.
$entity_type = $this->definition['entity_tables'][$this->base_table];
$entity_info = entity_get_info($entity_type);
if (isset($this->relationship)) {
$this->base_table_alias = $this->relationship;
}
else {
$this->base_table_alias = $this->base_table;
}
// We always need the base field (entity_id / revision_id).
if (empty($this->definition['is revision'])) {
$this->field_alias = $this->query->add_field($this->base_table_alias, $entity_info['entity keys']['id'], '', $params);
}
else {
$this->field_alias = $this->query->add_field($this->base_table_alias, $entity_info['entity keys']['revision'], '', $params);
$this->aliases['entity_id'] = $this->query->add_field($this->base_table_alias, $entity_info['entity keys']['id'], '', $params);
}
// The alias needs to be unique, so we use both the field table and the entity type.
$entity_type_alias = $this->definition['table'] . '_' . $entity_type . '_entity_type';
$this->aliases['entity_type'] = $this->query->add_field(NULL, "'$entity_type'", $entity_type_alias);
$fields = $this->additional_fields;
// We've already added entity_type, so we can remove it from the list.
// No need to add the entity type.
$entity_type_key = array_search('entity_type', $fields);
if ($entity_type_key !== FALSE) {
unset($fields[$entity_type_key]);
......@@ -195,14 +152,11 @@ function query($use_groupby = FALSE) {
if ($use_groupby) {
// Add the fields that we're actually grouping on.
$options = array();
if ($this->options['group_column'] != 'entity_id') {
$options = array($this->options['group_column'] => $this->options['group_column']);
}
$options += is_array($this->options['group_columns']) ? $this->options['group_columns'] : array();
$fields = array();
$rkey = $this->definition['is revision'] ? 'FIELD_LOAD_REVISION' : 'FIELD_LOAD_CURRENT';
// Go through the list and determine the actual column name from field api.
......@@ -245,23 +199,14 @@ function query($use_groupby = FALSE) {
$this->query->add_where_expression(0, "$column IN($placeholder) OR $column IS NULL", array($placeholder => $langcode_fallback_candidates));
}
}
// The revision id inhibits grouping.
// So, stop here if we're using grouping, or if aren't adding all columns to
// the query.
if ($use_groupby || empty($this->definition['add fields to query'])) {
return;
}
$this->add_additional_fields(array('revision_id'));
}
/**
* Determine if the field table should be added to the query.
*/
function add_field_table($use_groupby) {
// Grouping is enabled, or we are explicitly required to do this.
if ($use_groupby || !empty($this->definition['add fields to query'])) {
// Grouping is enabled.
if ($use_groupby) {
return TRUE;
}
// This a multiple value field, but "group multiple values" is not checked.
......@@ -621,69 +566,6 @@ function groupby_form_submit(&$form, &$form_state) {
$item['group_columns'] = array_filter($form_state['values']['options']['group_columns']);
}
/**
* Load the entities for all fields that are about to be displayed.
*/
function post_execute(&$values) {
if (!empty($values)) {
// Divide the entity ids by entity type, so they can be loaded in bulk.
$entities_by_type = array();
$revisions_by_type = array();
foreach ($values as $key => $object) {
if (isset($this->aliases['entity_type']) && isset($object->{$this->aliases['entity_type']}) && isset($object->{$this->field_alias}) && !isset($values[$key]->_field_data[$this->field_alias])) {
$entity_type = $object->{$this->aliases['entity_type']};
if (empty($this->definition['is revision'])) {
$entity_id = $object->{$this->field_alias};
$entities_by_type[$entity_type][$key] = $entity_id;
}
else {
$revision_id = $object->{$this->field_alias};
$entity_id = $object->{$this->aliases['entity_id']};
$entities_by_type[$entity_type][$key] = array($entity_id, $revision_id);
}
}
}
// Load the entities.
foreach ($entities_by_type as $entity_type => $entity_ids) {
$entity_info = entity_get_info($entity_type);
if (empty($this->definition['is revision'])) {
$entities = entity_load_multiple($entity_type, $entity_ids);
$keys = $entity_ids;
}
else {
// Revisions can't be loaded multiple, so we have to load them
// one by one.
$entities = array();
$keys = array();
foreach ($entity_ids as $key => $combined) {
list($entity_id, $revision_id) = $combined;
$entity = entity_revision_load($entity_type, $revision_id);
if ($entity) {
$entities[$revision_id] = array_shift($entity);
$keys[$key] = $revision_id;
}
}
}
foreach ($keys as $key => $entity_id) {
// If this is a revision, load the revision instead.
if (isset($entities[$entity_id])) {
$values[$key]->_field_data[$this->field_alias] = array(
'entity_type' => $entity_type,
'entity' => $entities[$entity_id],
);
}
}