diff --git a/core/modules/taxonomy/config/install/views.view.taxonomy_term.yml b/core/modules/taxonomy/config/install/views.view.taxonomy_term.yml index 26f32f8..929e63c 100644 --- a/core/modules/taxonomy/config/install/views.view.taxonomy_term.yml +++ b/core/modules/taxonomy/config/install/views.view.taxonomy_term.yml @@ -225,7 +225,7 @@ display: admin_label: '' empty: true tokenize: true - entity_id: '!1' + entity_id_uuid: '!1' view_mode: full bypass_access: false plugin_id: entity diff --git a/core/modules/views/config/schema/views.area.schema.yml b/core/modules/views/config/schema/views.area.schema.yml index 1262a89..244f022 100644 --- a/core/modules/views/config/schema/views.area.schema.yml +++ b/core/modules/views/config/schema/views.area.schema.yml @@ -8,9 +8,9 @@ views.area.entity: type: views_area label: 'Entity' mapping: - entity_id: + entity_id_uuid: type: string - label: 'ID' + label: 'Entity ID or UUID' view_mode: type: string label: 'View mode' diff --git a/core/modules/views/src/Plugin/views/area/Entity.php b/core/modules/views/src/Plugin/views/area/Entity.php index 93d0ad7..e1e9f75 100644 --- a/core/modules/views/src/Plugin/views/area/Entity.php +++ b/core/modules/views/src/Plugin/views/area/Entity.php @@ -7,9 +7,12 @@ namespace Drupal\views\Plugin\views\area; +use Drupal\Component\Uuid\Uuid; +use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\ViewExecutable; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides an area handler which renders an entity in a certain view mode. @@ -28,6 +31,43 @@ class Entity extends TokenizeAreaPluginBase { protected $entityType; /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * Constructs a new Entity instance. + * + * @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\Core\Entity\EntityManagerInterface $entity_manager + * The entity manager. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager) { + parent::__construct($configuration, $plugin_id, $plugin_definition); + + $this->entityManager = $entity_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') + ); + } + + /** * Overrides \Drupal\views\Plugin\views\area\AreaPluginBase::init(). */ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { @@ -45,9 +85,10 @@ protected function defineOptions() { // this handler. $options['tokenize']['default'] = TRUE; - $options['entity_id'] = array('default' => ''); - $options['view_mode'] = array('default' => 'default'); - $options['bypass_access'] = array('default' => FALSE); + // Contains either the entity ID or the UUID. + $options['entity_id_uuid'] = ['default' => '']; + $options['view_mode'] = ['default' => 'default']; + $options['bypass_access'] = ['default' => FALSE]; return $options; } @@ -60,16 +101,19 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { $form['view_mode'] = array( '#type' => 'select', - '#options' => \Drupal::entityManager()->getViewModeOptions($this->entityType), + '#options' => $this->entityManager->getViewModeOptions($this->entityType), '#title' => $this->t('View mode'), '#default_value' => $this->options['view_mode'], ); - $form['entity_id'] = array( - '#title' => $this->t('ID'), + $label = $this->entityManager->getDefinition($this->entityType)->getLabel(); + $form['entity_id_uuid'] = [ + '#title' => $this->t('@entity_type_label ID or UUID', ['@entity_type_label' => $label]), '#type' => 'textfield', - '#default_value' => $this->options['entity_id'], - ); + '#default_value' => $this->options['entity_id_uuid'], + '#autocomplete_route_name' => 'system.entity_autocomplete', + '#autocomplete_route_parameters' => ['entity_type' => $this->entityType], + ]; $form['bypass_access'] = array( '#type' => 'checkbox', @@ -82,16 +126,35 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { /** * {@inheritdoc} */ + public function submitOptionsForm(&$form, FormStateInterface $form_state, &$options = []) { + parent::submitOptionsForm($form, $form_state); + + // Always try to store the UUID, if possible. + if ($entity = $this->entityManager->getStorage($this->entityType)->load($options['entity_id_uuid'])) { + $options['entity_id_uuid'] = $entity->uuid(); + } + } + + /** + * {@inheritdoc} + */ public function render($empty = FALSE) { if (!$empty || !empty($this->options['empty'])) { - $entity_id = $this->tokenizeValue($this->options['entity_id']); - $entity = entity_load($this->entityType, $entity_id); - if ($entity && (!empty($this->options['bypass_access']) || $entity->access('view'))) { - return entity_view($entity, $this->options['view_mode']); + $entity_id_uuid = $this->tokenizeValue($this->options['entity_id_uuid']); + + $entity_storage = $this->entityManager->getStorage($this->entityType); + $view_builder = $this->entityManager->getViewBuilder($this->entityType); + // Validate whether the ID is a UUID, in that case load the entity as UUID + // otherwise use the ID. + $is_uuid = Uuid::isValid($entity_id_uuid); + if ((!$is_uuid && $entity = $entity_storage->load($entity_id_uuid)) || ($entities = $entity_storage->loadByProperties(['uuid' => $entity_id_uuid])) && ($entity = reset($entities))) { + if (!empty($this->options['bypass_access']) || $entity->access('view')) { + return $view_builder->view($entity, $this->options['view_mode']); + } } } - return array(); + return []; } } diff --git a/core/modules/views/src/Tests/Handler/AreaEntityTest.php b/core/modules/views/src/Tests/Handler/AreaEntityTest.php index af44b44..f68c8f0 100644 --- a/core/modules/views/src/Tests/Handler/AreaEntityTest.php +++ b/core/modules/views/src/Tests/Handler/AreaEntityTest.php @@ -73,6 +73,7 @@ public function testEntityAreaData() { */ public function testEntityArea() { + /** @var \Drupal\Core\Entity\EntityInterface[] $entities */ $entities = array(); for ($i = 0; $i < 3; $i++) { $random_label = $this->randomMachineName(); @@ -84,6 +85,25 @@ public function testEntityArea() { } $view = Views::getView('test_entity_area'); + $preview = $view->preview('default', [$entities[1]->id()]); + $this->setRawContent(\Drupal::service('renderer')->render($preview)); + + $result = $this->xpath('//div[@class = "view-header"]'); + $this->assertTrue(strpos(trim((string) $result[0]), $entities[0]->label()) !== FALSE, 'The rendered entity appears in the header of the view.'); + $this->assertTrue(strpos(trim((string) $result[0]), 'full') !== FALSE, 'The rendered entity appeared in the right view mode.'); + + $result = $this->xpath('//div[@class = "view-footer"]'); + $this->assertTrue(strpos(trim((string) $result[0]), $entities[1]->label()) !== FALSE, 'The rendered entity appears in the footer of the view.'); + $this->assertTrue(strpos(trim((string) $result[0]), 'full') !== FALSE, 'The rendered entity appeared in the right view mode.'); + + // Specific a UUID instead of the entity ID. + $uuid = $entities[0]->uuid(); + /** @var \Drupal\views\ViewStorageInterface $view */ + $view = Views::getView('test_entity_area'); + $item = $view->getHandler('default', 'header', 'entity_entity_test'); + $item['entity_id_uuid'] = $uuid; + $view->setHandler('default', 'header', 'entity_entity_test', $item); + $preview = $view->preview('default', array($entities[1]->id())); $this->drupalSetContent(drupal_render($preview)); diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_area.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_area.yml index 38bbf78..e46754a 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_area.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_entity_area.yml @@ -21,7 +21,7 @@ display: field: entity_entity_test id: entity_entity_test table: views - entity_id: '1' + entity_id_uuid: '1' view_mode: full plugin_id: entity footer: @@ -29,7 +29,7 @@ display: field: entity_entity_test id: entity_entity_test table: views - entity_id: '!1' + entity_id_uuid: '!1' view_mode: full plugin_id: entity fields: diff --git a/core/modules/views/tests/src/Unit/Plugin/area/EntityTest.php b/core/modules/views/tests/src/Unit/Plugin/area/EntityTest.php new file mode 100644 index 0000000..df1882e --- /dev/null +++ b/core/modules/views/tests/src/Unit/Plugin/area/EntityTest.php @@ -0,0 +1,224 @@ +entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + $this->entityStorage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface'); + $this->entityViewBuilder = $this->getMock('Drupal\Core\Entity\EntityViewBuilderInterface'); + + $this->executable = $this->getMockBuilder('Drupal\views\ViewExecutable') + ->disableOriginalConstructor() + ->getMock(); + $this->display = $this->getMockBuilder('Drupal\views\Plugin\views\display\DisplayPluginBase') + ->disableOriginalConstructor() + ->getMock(); + $this->stylePlugin = $this->getMockBuilder('Drupal\views\Plugin\views\style\StylePluginBase') + ->disableOriginalConstructor() + ->getMock(); + $this->executable->style_plugin = $this->stylePlugin; + + $this->entityHandler = new Entity(array(), 'entity', array('entity_type' => 'entity_test'), $this->entityManager); + + $token = $this->getMockBuilder('Drupal\Core\Utility\Token') + ->disableOriginalConstructor() + ->getMock(); + $token->expects($this->once()) + ->method('replace') + ->willReturnArgument(0); + $container = new ContainerBuilder(); + $container->set('token', $token); + \Drupal::setContainer($container); + } + + /** + * Ensures that the entity manager returns an entity storage. + */ + protected function setupEntityManager() { + $this->entityManager->expects($this->any()) + ->method('getStorage') + ->with('entity_test') + ->willReturn($this->entityStorage); + $this->entityManager->expects($this->any()) + ->method('getViewBuilder') + ->with('entity_test') + ->willReturn($this->entityViewBuilder); + } + + /** + * @covers ::render() + * @covers ::defineOptions() + * @covers ::init() + */ + public function testRenderWithId() { + $this->setupEntityManager(); + $options = [ + 'entity_id_uuid' => 1, + 'tokenize' => FALSE, + ]; + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->once()) + ->method('access') + ->willReturn(TRUE); + + $this->entityStorage->expects($this->never()) + ->method('loadByProperties'); + $this->entityStorage->expects($this->once()) + ->method('load') + ->with(1) + ->willReturn($entity); + $this->entityViewBuilder->expects($this->once()) + ->method('view') + ->with($entity, 'default') + ->willReturn(['#markup' => 'hallo']); + + $this->entityHandler->init($this->executable, $this->display, $options); + + $result = $this->entityHandler->render(); + $this->assertEquals(['#markup' => 'hallo'], $result); + } + + /** + * @covers ::render() + * @covers ::defineOptions() + * @covers ::init() + */ + public function testRenderWithIdAndToken() { + $this->setupEntityManager(); + $options = [ + 'entity_id_uuid' => '!1', + 'tokenize' => TRUE, + ]; + + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->once()) + ->method('access') + ->willReturn(TRUE); + + $this->stylePlugin->expects($this->once()) + ->method('tokenizeValue') + ->with('!1', 0) + ->willReturn(5); + + $this->entityStorage->expects($this->never()) + ->method('loadByProperties'); + $this->entityStorage->expects($this->once()) + ->method('load') + ->with(5) + ->willReturn($entity); + $this->entityViewBuilder->expects($this->once()) + ->method('view') + ->with($entity, 'default') + ->willReturn(['#markup' => 'hallo']); + + $this->entityHandler->init($this->executable, $this->display, $options); + + $result = $this->entityHandler->render(); + $this->assertEquals(['#markup' => 'hallo'], $result); + } + + /** + * @covers ::render() + * @covers ::defineOptions() + * @covers ::init() + */ + public function testRenderWithUuid() { + $this->setupEntityManager(); + $uuid = '1d52762e-b9d8-4177-908f-572d1a5845a4'; + $options = [ + 'entity_id_uuid' => $uuid, + 'tokenize' => FALSE, + ]; + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->once()) + ->method('access') + ->willReturn(TRUE); + + $this->entityStorage->expects($this->never()) + ->method('load'); + $this->entityStorage->expects($this->once()) + ->method('loadByProperties') + ->with(['uuid' => $uuid]) + ->willReturn([$entity]); + $this->entityViewBuilder->expects($this->once()) + ->method('view') + ->with($entity, 'default') + ->willReturn(['#markup' => 'hallo']); + + $this->entityHandler->init($this->executable, $this->display, $options); + + $result = $this->entityHandler->render(); + $this->assertEquals(['#markup' => 'hallo'], $result); + } + +}