diff --git a/config/schema/entity.schema.yml b/config/schema/entity.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..a70ef2a1fb704a377884034785bf44140b9a44d2 --- /dev/null +++ b/config/schema/entity.schema.yml @@ -0,0 +1,7 @@ +views.field.rendered_entity: + type: views_field + label: 'Rendered entity' + mapping: + view_mode: + type: string + label: 'View mode' diff --git a/entity.views.inc b/entity.views.inc new file mode 100644 index 0000000000000000000000000000000000000000..f4d5db32f420c3b15c2110513d022792377c958d --- /dev/null +++ b/entity.views.inc @@ -0,0 +1,32 @@ +<?php + +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; + +/** + * Implements hook_views_data(). + */ +function entity_views_data() { + $entity_types = \Drupal::entityTypeManager()->getDefinitions(); + $entity_types = array_filter($entity_types, function (EntityTypeInterface $entity_type) { + return $entity_type->isSubclassOf(ContentEntityInterface::class); + }); + + $data = []; + foreach ($entity_types as $entity_type) { + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ + $base_table = $entity_type->getBaseTable() ?: $entity_type->id(); + + if ($entity_type->hasViewBuilderClass()) { + $data[$base_table]['rendered_entity'] = [ + 'field' => [ + 'title' => t('Rendered entity'), + 'help' => t('Renders an entity in a view mode.'), + 'id' => 'rendered_entity', + ], + ]; + } + } + + return $data; +} diff --git a/src/Plugin/views/field/RenderedEntity.php b/src/Plugin/views/field/RenderedEntity.php new file mode 100644 index 0000000000000000000000000000000000000000..27f99462a8b5075b2fdf0775cfb3b5563af0d75a --- /dev/null +++ b/src/Plugin/views/field/RenderedEntity.php @@ -0,0 +1,207 @@ +<?php + +/** + * @file + * Contains \Drupal\entity\Plugin\views\field\RenderedEntity. + */ + +namespace Drupal\entity\Plugin\views\field; + +use Drupal\Core\Cache\CacheableDependencyInterface; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Language\LanguageManagerInterface; +use Drupal\views\Entity\Render\EntityTranslationRenderTrait; +use Drupal\views\ResultRow; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a field handler which renders an entity in a certain view mode. + * + * @ingroup views_field_handlers + * + * @ViewsField("rendered_entity") + */ +class RenderedEntity extends FieldPluginBase implements CacheableDependencyInterface { + + use EntityTranslationRenderTrait; + + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * The language manager. + * + * @var \Drupal\Core\Language\LanguageManagerInterface + */ + protected $languageManager; + + /** + * Constructs a new RenderedEntity object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager + * The language manager. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, EntityManagerInterface $entity_manager, LanguageManagerInterface $language_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->entityManager = $entity_manager; + $this->languageManager = $language_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.manager'), + $container->get('language_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function usesGroupBy() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function defineOptions() { + $options = parent::defineOptions(); + $options['view_mode'] = ['default' => 'default']; + + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + $form['view_mode'] = [ + '#type' => 'select', + '#options' => $this->entityManager->getViewModeOptions($this->getEntityTypeId()), + '#title' => $this->t('View mode'), + '#default_value' => $this->options['view_mode'], + ]; + } + + /** + * {@inheritdoc} + */ + public function render(ResultRow $values) { + $entity = $this->getEntityTranslation($this->getEntity($values), $values); + if (isset($entity) && $entity->access('view')) { + $view_builder = $this->entityManager->getViewBuilder($this->getEntityTypeId()); + return $view_builder->view($entity, $this->options['view_mode']); + } + return []; + } + + /** + * {@inheritdoc} + */ + public function getCacheContexts() { + return []; + } + + /** + * {@inheritdoc} + */ + public function getCacheTags() { + $view_display_storage = $this->entityManager->getStorage('entity_view_display'); + $view_displays = $view_display_storage->loadMultiple($view_display_storage + ->getQuery() + ->condition('targetEntityType', $this->getEntityTypeId()) + ->execute()); + + $tags = []; + foreach ($view_displays as $view_display) { + $tags = array_merge($tags, $view_display->getCacheTags()); + } + return $tags; + } + + /** + * {@inheritdoc} + */ + public function getCacheMaxAge() { + return 0; + } + + /** + * {@inheritdoc} + */ + public function query() { + // We purposefully do not call parent::query() because we do not want the + // default query behavior for Views fields. Instead, let the entity + // translation renderer provide the correct query behavior. + if ($this->languageManager->isMultilingual()) { + $this->getEntityTranslationRenderer()->query($this->query, $this->relationship); + } + } + + /** + * {@inheritdoc} + */ + public function getEntityTypeId() { + return $this->getEntityType(); + } + + /** + * {@inheritdoc} + */ + protected function getEntityManager() { + return $this->entityManager; + } + + /** + * {@inheritdoc} + */ + protected function getLanguageManager() { + return $this->languageManager; + } + + /** + * {@inheritdoc} + */ + protected function getView() { + return $this->view; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + + $view_mode = $this->entityManager + ->getStorage('entity_view_mode') + ->load($this->entityTypeId . '.' . $this->options['view_mode']); + if ($view_mode) { + $dependencies[$view_mode->getConfigDependencyKey()][] = $view_mode->getConfigDependencyName(); + } + + return $dependencies; + } + +} diff --git a/src/Tests/Plugin/views/field/RenderedEntityTest.php b/src/Tests/Plugin/views/field/RenderedEntityTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d50a37f92ac70022d6a879ad2414ec8e550f378c --- /dev/null +++ b/src/Tests/Plugin/views/field/RenderedEntityTest.php @@ -0,0 +1,164 @@ +<?php + +/** + * @file + * Contains \Drupal\entity\Tests\Plugin\views\field\RenderedEntityTest. + */ + +namespace Drupal\entity\Tests\Plugin\views\field; + +use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\entity_test\Entity\EntityTest; +use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; +use Drupal\user\Entity\Role; +use Drupal\user\Entity\User; +use Drupal\views\Tests\ViewKernelTestBase; +use Drupal\views\Tests\ViewTestData; +use Drupal\views\Views; +use Drupal\Core\Entity\Entity\EntityViewMode; + +/** + * Tests the Drupal\entity\Plugin\views\field\RenderedEntity handler. + * + * @group entity + */ +class RenderedEntityTest extends ViewKernelTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['entity_test', 'entity_module_test', 'field']; + + /** + * Views used by this test. + * + * @var array + */ + public static $testViews = ['test_entity_rendered']; + + /** + * The logged in user. + * + * @var \Drupal\user\UserInterface + */ + protected $user; + + /** + * {@inheritdoc} + */ + protected function setUp($import_test_views = TRUE) { + parent::setUp($import_test_views); + + if ($import_test_views) { + ViewTestData::createTestViews(get_class($this), ['entity_module_test']); + } + } + + /** + * {@inheritdoc} + */ + protected function setUpFixtures() { + $this->installEntitySchema('user'); + $this->installEntitySchema('entity_test'); + $this->installConfig(['entity_test']); + + EntityViewMode::create([ + 'id' => 'entity_test.foobar', + 'targetEntityType' => 'entity_test', + 'status' => TRUE, + 'enabled' => TRUE, + 'label' => 'My view mode', + ])->save(); + + $display = EntityViewDisplay::create([ + 'targetEntityType' => 'entity_test', + 'bundle' => 'entity_test', + 'mode' => 'foobar', + 'label' => 'My view mode', + 'status' => TRUE, + ]); + $display->save(); + + $field_storage = FieldStorageConfig::create([ + 'field_name' => 'test_field', + 'entity_type' => 'entity_test', + 'type' => 'string', + ]); + $field_storage->save(); + + $field_config = FieldConfig::create([ + 'field_name' => 'test_field', + 'entity_type' => 'entity_test', + 'bundle' => 'entity_test', + ]); + $field_config->save(); + + // Create some test entities. + for ($i = 1; $i <= 3; $i++) { + EntityTest::create([ + 'name' => "Article title $i", + 'test_field' => "Test $i", + ])->save(); + } + + $role = Role::create([ + 'id' => 'test_role', + ]); + $role->grantPermission('bypass node access'); + $role->save(); + $this->user = User::create([ + 'name' => 'test user', + ]); + $this->user->addRole($role->id()); + $this->user->save(); + + parent::setUpFixtures(); + } + + /** + * Tests the default rendered entity output. + */ + public function testRenderedEntityWithoutField() { + \Drupal::currentUser()->setAccount($this->user); + + EntityViewDisplay::load('entity_test.entity_test.foobar') + ->removeComponent('test_field') + ->save(); + + // The view should not display the body field. + $view = Views::getView('test_field_entity_test_rendered'); + $build = $view->preview(); + $renderer = \Drupal::service('renderer'); + $renderer->renderPlain($build); + for ($i = 1; $i <= 3; $i++) { + $view_field = $view->style_plugin->getField($i - 1, 'rendered_entity'); + $search_result = strpos($view_field, "Test $i") !== FALSE; + $this->assertFalse($search_result, "The text 'Test $i' not found in the view."); + } + } + + /** + * Tests the rendered entity output with the body field configured to show. + */ + public function testRenderedEntityWithField() { + \Drupal::currentUser()->setAccount($this->user); + + // Show the body on the node.x.foobar view mode. + EntityViewDisplay::load('entity_test.entity_test.foobar')->setComponent('test_field', ['type' => 'string', 'label' => 'above'])->save(); + + // The view should display the body field. + $view = Views::getView('test_field_entity_test_rendered'); + $build = $view->preview(); + $renderer = \Drupal::service('renderer'); + $renderer->renderPlain($build); + for ($i = 1; $i <= 3; $i++) { + $view_field = $view->style_plugin->getField($i - 1, 'rendered_entity'); + $search_result = strpos($view_field, "Test $i") !== FALSE; + $this->assertTrue($search_result, "The text 'Test $i' found in the view."); + } + } + +} diff --git a/tests/modules/entity_module_test/test_views/views.view.test_field_entity_test_rendered.yml b/tests/modules/entity_module_test/test_views/views.view.test_field_entity_test_rendered.yml new file mode 100644 index 0000000000000000000000000000000000000000..541ee969306fd6c9efc62c1057f8988eac093ede --- /dev/null +++ b/tests/modules/entity_module_test/test_views/views.view.test_field_entity_test_rendered.yml @@ -0,0 +1,161 @@ +langcode: en +status: true +dependencies: + module: + - entity_test + - user +id: test_field_entity_test_rendered +label: 'Test Rendered entity test' +module: views +description: '' +tag: '' +base_table: entity_test +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: none + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + rendered_entity: + id: rendered_entity + table: entity_test + field: rendered_entity + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + view_mode: foobar + entity_type: entity_test + plugin_id: rendered_entity + filters: { } + sorts: + id: + id: id + table: entity_test + field: id + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: entity_test + entity_field: nid + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_interface' + - url.query_args + - user.permissions + tags: { }