Commit 13922dbd authored by alexpott's avatar alexpott

Issue #1867518 by plach, dawehner, yched, iMiksu, epari.siva, marcvangend,...

Issue #1867518 by plach, dawehner, yched, iMiksu, epari.siva, marcvangend, Fabianx, Wim Leers, effulgentsia: Leverage entityDisplay to provide fast rendering for fields
parent 74802813
......@@ -857,7 +857,7 @@ public function getEntityTypeLabels($group = FALSE) {
public function getTranslationFromContext(EntityInterface $entity, $langcode = NULL, $context = array()) {
$translation = $entity;
if ($entity instanceof TranslatableInterface) {
if ($entity instanceof TranslatableInterface && count($entity->getTranslationLanguages()) > 1) {
if (empty($langcode)) {
$langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId();
}
......
......@@ -60,7 +60,7 @@ public function settingsSummary();
* items.
*
* @param \Drupal\Core\Field\FieldItemListInterface[] $entities_items
* Array of field values, keyed by entity ID.
* An array with the field values from the multiple entities being rendered.
*/
public function prepareView(array $entities_items);
......
......@@ -205,7 +205,7 @@ public function _testMultipleFieldRender() {
foreach ($pure_items as $j => $item) {
$items[] = $pure_items[$j]['value'];
}
$this->assertEqual($rendered_field, implode(', ', $items), 'Make sure that the amount of items is limited.');
$this->assertEqual($rendered_field, implode(', ', $items), 'The amount of items is limited.');
}
// Test that an empty field is rendered without error.
......@@ -227,7 +227,7 @@ public function _testMultipleFieldRender() {
foreach ($pure_items as $j => $item) {
$items[] = $pure_items[$j]['value'];
}
$this->assertEqual($rendered_field, implode(', ', $items), 'Make sure that the amount of items is limited.');
$this->assertEqual($rendered_field, implode(', ', $items), 'The amount of items is limited and the offset is correct.');
}
$view->destroy();
......@@ -248,7 +248,7 @@ public function _testMultipleFieldRender() {
foreach ($pure_items as $j => $item) {
$items[] = $pure_items[$j]['value'];
}
$this->assertEqual($rendered_field, implode(', ', $items), 'Make sure that the amount of items is limited.');
$this->assertEqual($rendered_field, implode(', ', $items), 'The amount of items is limited and they are reversed.');
}
$view->destroy();
......@@ -266,7 +266,7 @@ public function _testMultipleFieldRender() {
$pure_items = $this->nodes[$i]->{$field_name}->getValue();
$items[] = $pure_items[0]['value'];
$items[] = $pure_items[4]['value'];
$this->assertEqual($rendered_field, implode(', ', $items), 'Make sure that the amount of items is limited.');
$this->assertEqual($rendered_field, implode(', ', $items), 'Items are limited to first and last.');
}
$view->destroy();
......@@ -286,7 +286,7 @@ public function _testMultipleFieldRender() {
foreach ($pure_items as $j => $item) {
$items[] = $pure_items[$j]['value'];
}
$this->assertEqual($rendered_field, implode(':', $items), 'Make sure that the amount of items is limited.');
$this->assertEqual($rendered_field, implode(':', $items), 'The amount of items is limited and the custom separator is correct.');
}
$view->destroy();
......@@ -305,7 +305,7 @@ public function _testMultipleFieldRender() {
foreach ($pure_items as $j => $item) {
$items[] = $pure_items[$j]['value'];
}
$this->assertEqual($rendered_field, implode('<h2>test</h2>', $items), 'Make sure that the amount of items is limited.');
$this->assertEqual($rendered_field, implode('<h2>test</h2>', $items), 'The custom separator is correctly escaped.');
}
$view->destroy();
}
......
......@@ -383,10 +383,20 @@ protected function doTestEntityTranslationAPI($entity_type) {
// Verify that changing the default translation flag causes an exception to
// be thrown.
$message = 'The default translation flag cannot be changed.';
foreach ($entity->getTranslationLanguages() as $t_langcode => $language) {
$translation = $entity->getTranslation($t_langcode);
$default = $translation->isDefaultTranslation();
$message = 'The default translation flag can be reassigned the same value.';
try {
$translation->{$default_langcode_key}->value = $default;
$this->pass($message);
}
catch (\LogicException $e) {
$this->fail($message);
}
$message = 'The default translation flag cannot be changed.';
try {
$translation->{$default_langcode_key}->value = !$default;
$this->fail($message);
......@@ -394,6 +404,7 @@ protected function doTestEntityTranslationAPI($entity_type) {
catch (\LogicException $e) {
$this->pass($message);
}
$this->assertEqual($translation->{$default_langcode_key}->value, $default);
}
......
<?php
/**
* Implements hook_views_data_alter().
*/
function entity_test_views_data_alter(&$data) {
$data['entity_test']['name_alias'] = $data['entity_test']['name'];
$data['entity_test']['name_alias']['field']['real field'] = 'name';
}
......@@ -33,6 +33,7 @@ public function testUserName() {
$view->field['name']->options['link_to_user'] = TRUE;
$view->field['name']->options['type'] = 'user_name';
$view->field['name']->init($view, $view->getDisplay('default'));
$view->field['name']->options['id'] = 'name';
$this->executeView($view);
$anon_name = $this->config('user.settings')->get('anonymous');
......
......@@ -15,7 +15,7 @@
/**
* Renders entities in a configured language.
*/
class ConfigurableLanguageRenderer extends RendererBase {
class ConfigurableLanguageRenderer extends EntityTranslationRendererBase {
/**
* A specific language code for rendering if available.
......
......@@ -12,7 +12,7 @@
/**
* Renders entities in their default language.
*/
class DefaultLanguageRenderer extends RendererBase {
class DefaultLanguageRenderer extends EntityTranslationRendererBase {
/**
* {@inheritdoc}
......
<?php
/**
* @file
* Contains \Drupal\views\Entity\Render\EntityFieldRenderer.
*/
namespace Drupal\views\Entity\Render;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\views\Plugin\views\field\Field;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ResultRow;
use Drupal\views\ViewExecutable;
/**
* Renders entity fields.
*
* This is used to build render arrays for all entity field values of a view
* result set sharing the same relationship. An entity translation renderer is
* used internally to handle entity language properly.
*/
class EntityFieldRenderer extends RendererBase {
use EntityTranslationRenderTrait;
/**
* The relationship being handled.
*
* @var string
*/
protected $relationship;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* A list of indexes of rows whose fields have already been rendered.
*
* @var int[]
*/
protected $processedRows = [];
/**
* Constructs an EntityFieldRenderer object.
*
* @param \Drupal\views\ViewExecutable $view
* The view whose fields are being rendered.
* @param string $relationship
* The relationship to be handled.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
*/
public function __construct(ViewExecutable $view, $relationship, LanguageManagerInterface $language_manager, EntityTypeInterface $entity_type, EntityManagerInterface $entity_manager) {
parent::__construct($view, $language_manager, $entity_type);
$this->relationship = $relationship;
$this->entityManager = $entity_manager;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
return $this->getEntityTranslationRenderer()->getCacheContexts();
}
/**
* {@inheritdoc}
*/
public function getEntityTypeId() {
return $this->entityType->id();
}
/**
* {@inheritdoc}
*/
protected function getEntityManager() {
return $this->entityManager;
}
/**
* {@inheritdoc}
*/
protected function getLanguageManager() {
return $this->languageManager;
}
/**
* {@inheritdoc}
*/
protected function getView() {
return $this->view;
}
/**
* {@inheritdoc}
*/
public function query(QueryPluginBase $query, $relationship = NULL) {
$this->getEntityTranslationRenderer()->query($query, $relationship);
}
/**
* Renders entity field data.
*
* @param \Drupal\views\ResultRow $row
* A single row of the query result.
* @param \Drupal\views\Plugin\views\field\Field $field
* (optional) A field to be rendered.
*
* @return array
* A renderable array for the entity data contained in the result row.
*/
public function render(ResultRow $row, Field $field = NULL) {
// The method is called for each field in each result row. In order to
// leverage multiple-entity building of formatter output, we build the
// render arrays for all fields in all rows on the first call.
if (!isset($this->build)) {
$this->build = $this->buildFields($this->view->result);
}
if (isset($field)) {
$field_id = $field->options['id'];
// Pick the render array for the row / field we are being asked to render,
// and remove it from $this->build to free memory as we progress.
if (isset($this->build[$row->index][$field_id])) {
$build = $this->build[$row->index][$field_id];
unset($this->build[$row->index][$field_id]);
}
else {
// In the uncommon case where a field gets rendered several times
// (typically through direct Views API calls), the pre-computed render
// array was removed by the unset() above. We have to manually rebuild
// the render array for the row.
$build = $this->buildFields([$row])[$row->index][$field_id];
}
}
else {
// Same logic as above, in the case where we are being called for a whole
// row.
if (isset($this->build[$row->index])) {
$build = $this->build[$row->index];
unset($this->build[$row->index]);
}
else {
$build = $this->buildFields([$row])[$row->index];
}
}
return $build;
}
/**
* Builds the render arrays for all fields of all result rows.
*
* The output is built using EntityViewDisplay objects to leverage
* multiple-entity building and ensure a common code path with regular entity
* view.
* - Each relationship is handled by a separate EntityFieldRenderer instance,
* since it operates on its own set of entities. This also ensures different
* entity types are handled separately, as they imply different
* relationships.
* - Within each relationship, the fields to render are arranged in unique
* sets containing each field at most once (an EntityViewDisplay can
* only process a field once with given display options, but a View can
* contain the same field several times with different display options).
* - For each set of fields, entities are processed by bundle, so that
* formatters can operate on the proper field definition for the bundle.
*
* @param \Drupal\views\ResultRow[] $values
* An array of all ResultRow objects returned from the query.
*
* @return array
* A renderable array for the fields handled by this renderer.
*
* @see \Drupal\Core\Entity\Entity\EntityViewDisplay
*/
protected function buildFields(array $values) {
$build = [];
if ($values && ($field_ids = $this->getRenderableFieldIds())) {
$entity_type_id = $this->getEntityTypeId();
// Collect the entities for the relationship, fetch the right translation,
// and group by bundle. For each result row, the corresponding entity can
// be obtained from any of the fields handlers, so we arbitrarily use the
// first one.
$entities_by_bundles = [];
$field = $this->view->field[current($field_ids)];
foreach ($values as $result_row) {
$entity = $field->getEntity($result_row);
$entities_by_bundles[$entity->bundle()][$result_row->index] = $this->getEntityTranslation($entity, $result_row);
}
// Determine unique sets of fields that can be processed by the same
// display. Fields that appear several times in the View open additional
// "overflow" displays.
$display_sets = [];
foreach ($field_ids as $field_id) {
$field = $this->view->field[$field_id];
$index = 0;
while (isset($display_sets[$index][$field->definition['field_name']])) {
$index++;
}
$display_sets[$index][$field_id] = $field;
}
// For each set of fields, build the output by bundle.
foreach ($display_sets as $display_fields) {
foreach ($entities_by_bundles as $bundle => $bundle_entities) {
// Create the display, and configure the field display options.
$display = EntityViewDisplay::create([
'targetEntityType' => $entity_type_id,
'bundle' => $bundle,
'status' => TRUE,
]);
foreach ($display_fields as $field_id => $field) {
$display->setComponent($field->definition['field_name'], [
'type' => $field->options['type'],
'settings' => $field->options['settings'],
]);
}
// Let the display build the render array for the entities.
$display_build = $display->buildMultiple($bundle_entities);
// Collect the field render arrays and index them using our internal
// row indexes and field IDs.
foreach ($display_build as $row_index => $entity_build) {
foreach ($display_fields as $field_id => $field) {
$build[$row_index][$field_id] = !empty($entity_build[$field->definition['field_name']]) ? $entity_build[$field->definition['field_name']] : [];
}
}
}
}
}
return $build;
}
/**
* Returns a list of names of entity fields to be rendered.
*
* @return string[]
* An associative array of views fields.
*/
protected function getRenderableFieldIds() {
$field_ids = [];
foreach ($this->view->field as $field_id => $field) {
if ($field instanceof Field && $field->relationship == $this->relationship) {
$field_ids[] = $field_id;
}
}
return $field_ids;
}
/**
* Returns the entity translation matching the configured row language.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object the field value being processed is attached to.
* @param \Drupal\views\ResultRow $row
* The result row the field value being processed belongs to.
*
* @return \Drupal\Core\Entity\FieldableEntityInterface
* The entity translation object for the specified row.
*/
public function getEntityTranslation(EntityInterface $entity, ResultRow $row) {
// We assume the same language should be used for all entity fields
// belonging to a single row, even if they are attached to different entity
// types. Below we apply language fallback to ensure a valid value is always
// picked.
$langcode = $this->getEntityTranslationRenderer()->getLangcode($row);
return $this->entityManager->getTranslationFromContext($entity, $langcode);
}
}
......@@ -10,25 +10,25 @@
use Drupal\views\Plugin\views\PluginBase;
/**
* Trait used to instantiate the view's entity language render.
* Trait used to instantiate the view's entity translation renderer.
*/
trait EntityTranslationRenderTrait {
/**
* The renderer to be used to render the entity row.
*
* @var \Drupal\views\Entity\Render\RendererBase
* @var \Drupal\views\Entity\Render\EntityTranslationRendererBase
*/
protected $entityLanguageRenderer;
protected $entityTranslationRenderer;
/**
* Returns the current renderer.
*
* @return \Drupal\views\Entity\Render\RendererBase
* @return \Drupal\views\Entity\Render\EntityTranslationRendererBase
* The configured renderer.
*/
protected function getEntityTranslationRenderer() {
if (!isset($this->entityLanguageRenderer)) {
if (!isset($this->entityTranslationRenderer)) {
$view = $this->getView();
$rendering_language = $view->display_handler->getOption('rendering_language');
$langcode = NULL;
......@@ -52,9 +52,9 @@ protected function getEntityTranslationRenderer() {
}
$class = '\Drupal\views\Entity\Render\\' . $renderer;
$entity_type = $this->getEntityManager()->getDefinition($this->getEntityTypeId());
$this->entityLanguageRenderer = new $class($view, $this->getLanguageManager(), $entity_type, $langcode);
$this->entityTranslationRenderer = new $class($view, $this->getLanguageManager(), $entity_type, $langcode);
}
return $this->entityLanguageRenderer;
return $this->entityTranslationRenderer;
}
/**
......
<?php
/**
* @file
* Contains \Drupal\views\Entity\Render\EntityTranslationRendererBase.
*/
namespace Drupal\views\Entity\Render;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\ResultRow;
/**
* Defines a base class for entity translation renderers.
*/
abstract class EntityTranslationRendererBase extends RendererBase {
/**
* Returns the language code associated to the given row.
*
* @param \Drupal\views\ResultRow $row
* The result row.
*
* @return string
* A language code.
*/
abstract public function getLangcode(ResultRow $row);
/**
* {@inheritdoc}
*/
public function query(QueryPluginBase $query, $relationship = NULL) {
}
/**
* {@inheritdoc}
*/
public function preRender(array $result) {
$view_builder = $this->view->rowPlugin->entityManager->getViewBuilder($this->entityType->id());
/** @var \Drupal\views\ResultRow $row */
foreach ($result as $row) {
// @todo Take relationships into account.
// See https://www.drupal.org/node/2457999.
$entity = $row->_entity;
$entity->view = $this->view;
$this->build[$entity->id()] = $view_builder->view($entity, $this->view->rowPlugin->options['view_mode'], $this->getLangcode($row));
}
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $row) {
$entity_id = $row->_entity->id();
return $this->build[$entity_id];
}
}
......@@ -8,7 +8,6 @@
namespace Drupal\views\Entity\Render;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\views\Plugin\CacheablePluginInterface;
use Drupal\views\Plugin\views\query\QueryPluginBase;
......@@ -16,7 +15,7 @@
use Drupal\views\ViewExecutable;
/**
* Defines a base class for entity row renderers.
* Defines a base class for entity renderers.
*/
abstract class RendererBase implements CacheablePluginInterface {
......@@ -46,7 +45,7 @@ abstract class RendererBase implements CacheablePluginInterface {
*
* @var array
*/
protected $build = array();
protected $build;
/**
* Constructs a renderer object.
......@@ -78,17 +77,6 @@ public function getCacheContexts() {
return [];
}
/**
* Returns the language code associated to the given row.
*
* @param \Drupal\views\ResultRow $row
* The result row.
*
* @return string
* A language code.
*/
abstract public function getLangcode(ResultRow $row);
/**
* Alters the query if needed.
*
......@@ -97,38 +85,26 @@ public function getCacheContexts() {
* @param string $relationship
* (optional) The relationship, used by a field.
*/
public function query(QueryPluginBase $query, $relationship = NULL) {
}
abstract public function query(QueryPluginBase $query, $relationship = NULL);
/**
* Runs before each row is rendered.
* Runs before each entity is rendered.
*
* @param $result
* The full array of results from the query.
*/
public function preRender(array $result) {
$view_builder = $this->view->rowPlugin->entityManager->getViewBuilder($this->entityType->id());
/** @var \Drupal\views\ResultRow $row */
foreach ($result as $row) {
$entity = $row->_entity;
$entity->view = $this->view;
$this->build[$entity->id()] = $view_builder->view($entity, $this->view->rowPlugin->options['view_mode'], $this->getLangcode($row));
}
}
/**
* Renders a row object.
* Renders entity data.
*
* @param \Drupal\views\ResultRow $row
* A single row of the query result.
*
* @return array
* The renderable array of a single row.
* A renderable array for the entity data contained in the result row.
*/
public function render(ResultRow $row) {
$entity_id = $row->_entity->id();
return $this->build[$entity_id];
}
abstract public function render(ResultRow $row);
}
......@@ -12,9 +12,9 @@
use Drupal\views\ResultRow;
/**
* Renders entity translations in their active language.
* Renders entity translations in their row language.
*/
class TranslationLanguageRenderer extends RendererBase {
class TranslationLanguageRenderer extends EntityTranslationRendererBase {
/**
* Stores the field alias of the langcode column.
......
......@@ -85,8 +85,13 @@ public function testGroupRows() {
// Test ungrouped rows.
$this->executeView($view);
$view->render();
$view->row_index = 0;
$this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[0]), 'a');
$view->row_index = 1;
$this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[1]), 'b');
$view->row_index = 2;
$this->assertEqual($view->field[$this->fieldName]->advancedRender($view->result[2]), 'c');
}
......
......@@ -7,6 +7,10 @@
namespace Drupal\views\Tests;
use Drupal\entity_test\Entity\EntityTestMul;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\views\Views;
/**
......@@ -21,14 +25,14 @@ class QueryGroupByTest extends ViewUnitTestBase {
*
* @var array
*/
public static $testViews = array('test_group_by_in_filters', 'test_aggregate_count', 'test_group_by_count');
public static $testViews = array('test_group_by_in_filters', 'test_aggregate_count', 'test_group_by_count', 'test_group_by_count_multicardinality');
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('entity_test', 'system', 'field', 'user');
public static $modules = array('entity_test', 'system', 'field', 'user', 'language');
/**
* The storage for the test entity type.
......@@ -45,8 +49,11 @@ protected function setUp() {
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test');
$this->installEntitySchema('entity_test_mul');
$this->storage = $this->container->get('entity.manager')->getStorage('entity_test');
ConfigurableLanguage::createFromLangcode('it')->save();
}
......@@ -205,4 +212,84 @@ public function testGroupByBaseField() {
$this->assertTrue(strpos($view->build_info['query'], 'GROUP BY entity_test.id'), 'GROUP BY field includes the base table name when grouping on the base field.');
}
/**
* Tests grouping a field with cardinality > 1.
*/
public function testGroupByFieldWithCardinality() {
$field_storage = FieldStorageConfig::create([
'type' => 'integer',
'field_name' => 'field_test',
'cardinality' => 4,
'entity_type' => 'entity_test_mul',
]);
$field_storage->save();
$field = FieldConfig::create([