diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php index b943d17..12a4543 100644 --- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityBase.php @@ -418,6 +418,17 @@ public function getConfigDependencyName() { /** * {@inheritdoc} */ + public function getConfigTarget() { + // For configuration entities, use the config ID for the config target + // identifier. This ensures that default configuration (which does not yet + // have UUIDs) can be provided and installed with references to the target, + // and also makes config dependencies more readable. + return $this->id(); + } + + /** + * {@inheritdoc} + */ public function onDependencyRemoval(array $dependencies) { } diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php index afdfcb0..5a9c85f 100644 --- a/core/lib/Drupal/Core/Entity/Entity.php +++ b/core/lib/Drupal/Core/Entity/Entity.php @@ -569,4 +569,13 @@ public function getConfigDependencyName() { return $this->getEntityTypeId() . ':' . $this->bundle() . ':' . $this->uuid(); } + /** + * {@inheritdoc} + */ + public function getConfigTarget() { + // For content entities, use the UUID for the config target identifier. + // This ensures that references to the target can be deployed reliably. + return $this->uuid(); + } + } diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php index 130612b..fac0380 100644 --- a/core/lib/Drupal/Core/Entity/EntityInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityInterface.php @@ -429,4 +429,15 @@ public function getConfigDependencyKey(); */ public function getConfigDependencyName(); + /** + * Gets the configuration target identifier for the entity. + * + * Used to supply the correct format for storing a reference targeting this + * entity in configuration. + * + * @return string + * The configuration target identifier. + */ + public function getConfigTarget(); + } diff --git a/core/lib/Drupal/Core/Entity/EntityManager.php b/core/lib/Drupal/Core/Entity/EntityManager.php index 96365cc..b09d9ff 100644 --- a/core/lib/Drupal/Core/Entity/EntityManager.php +++ b/core/lib/Drupal/Core/Entity/EntityManager.php @@ -981,6 +981,27 @@ public function loadEntityByUuid($entity_type_id, $uuid) { /** * {@inheritdoc} */ + public function loadEntityByConfigTarget($entity_type_id, $target) { + $entity_type = $this->getDefinition($entity_type_id); + + // For configuration entities, the config target is given by the entity ID. + // @todo Should we allow entity types to return their target identifier key + // rather than hard-coding this check? + if ($entity_type instanceof ConfigEntityTypeInterface) { + $entity = $this->getStorage($entity_type_id)->load($target); + } + + // For content entities, the config target is given by the UUID. + else { + $entity = $this->loadEntityByUuid($entity_type_id, $target); + } + + return $entity; + } + + /** + * {@inheritdoc} + */ public function getEntityTypeFromClass($class_name) { // Check the already calculated classes first. diff --git a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php index d60c47d..f1e6921 100644 --- a/core/lib/Drupal/Core/Entity/EntityManagerInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityManagerInterface.php @@ -470,6 +470,26 @@ public function getFormModeOptions($entity_type_id, $include_disabled = FALSE); public function loadEntityByUuid($entity_type_id, $uuid); /** + * Loads an entity by the config target identifier. + * + * @param string $entity_type_id + * The entity type ID to load from. + * @param string $target + * The configuration target to load, as returned from + * \Drupal\Core\Entity\EntityInterface::getConfigTarget(). + * + * @return \Drupal\Core\Entity\EntityInterface|FALSE + * The entity object, or FALSE if there is no entity with the given UUID. + * + * @throws \Drupal\Core\Entity\EntityStorageException + * Thrown if the target identifier is a UUID but the entity type does not + * support UUIDs. + * + * @see \Drupal\Core\Entity\EntityInterface::getConfigTarget() + */ + public function loadEntityByConfigTarget($entity_type_id, $target); + + /** * Returns the entity type ID based on the class that is called on. * * Compares the class this is called on against the known entity classes diff --git a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php index b4c897e..387e0fb 100644 --- a/core/lib/Drupal/Core/Entity/EntityStorageInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityStorageInterface.php @@ -104,7 +104,7 @@ public function deleteRevision($revision_id); * An associative array where the keys are the property names and the * values are the values those properties must have. * - * @return array + * @return \Drupal\Core\Entity\EntityInterface[] * An array of entity objects indexed by their ids. */ public function loadByProperties(array $values = array()); 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 15f80ec..885d416 100644 --- a/core/modules/taxonomy/config/install/views.view.taxonomy_term.yml +++ b/core/modules/taxonomy/config/install/views.view.taxonomy_term.yml @@ -227,7 +227,7 @@ display: admin_label: '' empty: true tokenize: true - entity_id: '!1' + target: '!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..a7d5aa2 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: + target: type: string - label: 'ID' + label: 'The target entity' view_mode: type: string label: 'View mode' diff --git a/core/modules/views/src/Entity/View.php b/core/modules/views/src/Entity/View.php index e42058e..6129d55 100644 --- a/core/modules/views/src/Entity/View.php +++ b/core/modules/views/src/Entity/View.php @@ -266,6 +266,7 @@ public function calculateDependencies() { $executable = $this->getExecutable(); $executable->initDisplay(); + $executable->initStyle(); $handler_types = array_keys(Views::getHandlerTypes()); foreach ($executable->displayHandlers as $display) { diff --git a/core/modules/views/src/Plugin/views/PluginBase.php b/core/modules/views/src/Plugin/views/PluginBase.php index e09545f..dfe960e 100644 --- a/core/modules/views/src/Plugin/views/PluginBase.php +++ b/core/modules/views/src/Plugin/views/PluginBase.php @@ -319,13 +319,68 @@ public function globalTokenReplace($string = '', array $options = array()) { return \Drupal::token()->replace($string, array('view' => $this->view), $options); } + + /** + * Indicates whether the value has a potential Views argument token. + * + * @param string $value + * The string to check + * + * @return bool + * TRUE if the string has a potential Views argument token. + * + * @see \Drupal\views\Plugin\views\display\DisplayPluginBase::getArgumentTokens() + */ + public function hasArgumentToken($value) { + // If the string definitely does not contain a token, return FALSE + // immediately for performance. + if ((strpos($value, '!') === FALSE) && (strpos($value, '%') === FALSE)) { + return FALSE; + } + // Otherwise, scan for valid token patterns. + return preg_match('/[!%]\d+/', $value); + } + + /** + * Indicates whether the value has a potential Views row-level token. + * + * @param string $value + * The string to check + * + * @return bool + * TRUE if the string has a potential row token. + * + * @see \Drupal\views\Plugin\views\field\FieldPluginBase::getRenderTokens() + */ + public function hasRenderToken($value) { + // If the string definitely does not contain a token, return FALSE + // immediately for performance. + if (strpos($value, '{{') === FALSE) { + return FALSE; + } + // Otherwise, scan for valid token patterns. + // Match any non-empty string between {{ and }} except another {{. + return preg_match('/\{\{[^(\{\{)]+\}\}/', $value); + } + + /** + * Indicates whether the value has a potential Views token. + * + * @param string $value + * The string to check + * + * @return bool + * TRUE if the string has any token pattern. + */ + public function hasToken($value) { + return ($this->hasArgumentToken($value) || $this->hasRenderToken($value)); + } + /** - * Replaces Views' tokens in a given string. It is the responsibility of the - * calling function to ensure $text and $token replacements are sanitized. + * Replaces Views' tokens in a given string. * - * This used to be a simple strtr() scattered throughout the code. Some Views - * tokens, such as arguments (e.g.: %1 or !1), still use the old format so we - * handle those as well as the new Twig-based tokens (e.g.: {{ field_name }}) + * It is the responsibility of the calling function to ensure $text and + * $token replacements are sanitized. * * @param $text * String with possible tokens. @@ -333,6 +388,11 @@ public function globalTokenReplace($string = '', array $options = array()) { * Array of token => replacement_value items. * * @return String + * The text with the tokens replaced. + * + * @see \Drupal\views\Plugin\views\PluginBase::hasToken() + * @see \Drupal\views\Plugin\views\display\DisplayPluginBase::getArgumentTokens() + * @see \Drupal\views\Plugin\views\field\FieldPluginBase::getRenderTokens() */ protected function viewsTokenReplace($text, $tokens) { if (empty($tokens)) { @@ -344,8 +404,9 @@ protected function viewsTokenReplace($text, $tokens) { $twig_tokens = array(); $other_tokens = array(); foreach ($tokens as $token => $replacement) { - if (strpos($token, '{{') !== FALSE) { + if ($this->hasRenderToken($token)) { // Twig wants a token replacement array stripped of curly-brackets. + // @todo Do this with the regex instead. $token = trim(str_replace(array('{', '}'), '', $token)); $twig_tokens[$token] = $replacement; } diff --git a/core/modules/views/src/Plugin/views/area/Entity.php b/core/modules/views/src/Plugin/views/area/Entity.php index 93d0ad7..a452ff9 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 the config target identifier for the entity. + $options['target'] = ['default' => '']; + $options['view_mode'] = ['default' => 'default']; + $options['bypass_access'] = ['default' => FALSE]; return $options; } @@ -60,16 +101,31 @@ 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(); + $target = $this->options['target']; + + // @todo This form appears to be missing some test coverage; no explicit + // fail when the below was broken. + + // If the target does not contain tokens, try to load the entity and + // display the entity ID to the admin form user. + if (!$this->view->getStyle()->hasToken($this->options['target'])) { + // @todo If the entity does not exist, this will will show the config + // target identifier. Is that the correct behavior? + if ($target_entity = $this->entityManager->loadEntityByConfigTarget($this->entityType, $this->options['target'])) { + $target = $target_entity->id(); + } + } + $form['target'] = [ + '#title' => $this->t('@entity_type_label ID', ['@entity_type_label' => $label]), '#type' => 'textfield', - '#default_value' => $this->options['entity_id'], - ); + '#default_value' => $target, + ]; $form['bypass_access'] = array( '#type' => 'checkbox', @@ -82,16 +138,62 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { /** * {@inheritdoc} */ + public function submitOptionsForm(&$form, FormStateInterface $form_state) { + parent::submitOptionsForm($form, $form_state); + + // @todo Add validation in https://www.drupal.org/node/2392833. + + // Load the referenced entity and store its config target identifier if + // the target does not contains tokens. + if (!$this->view->getStyle()->hasToken($this->options['target'])) { + $options = $form_state->getValue('options'); + if ($entity = $this->entityManager->getStorage($this->entityType)->load($options['target'])) { + $options['target'] = $entity->getConfigTarget(); + } + $form_state->setValue('options', $options); + } + } + + /** + * {@inheritdoc} + */ public function render($empty = FALSE) { + // @todo Simplify this code. 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']); + if ($this->view->getStyle()->hasToken($this->options['target'])) { + $target_id = $this->tokenizeValue($this->options['target']); + if ($entity = $this->entityManager->getStorage($this->entityType)->load($target_id)) { + $target_entity = $entity; + } + } + else { + if ($entity = $this->entityManager->loadEntityByConfigTarget($this->entityType, $this->options['target'])) { + $target_entity = $entity; + } + } + if (isset($target_entity) && (!empty($this->options['bypass_access']) || $target_entity->access('view'))) { + $view_builder = $this->entityManager->getViewBuilder($this->entityType); + return $view_builder->view($target_entity, $this->options['view_mode']); } } - return array(); + return []; } + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + + // Ensure that we don't add dependencies for placeholders. + // @todo No tests fail even if this never is false; add coverage. + if (!$this->view->getStyle()->hasToken($this->options['target'])) { + if ($entity = $this->entityManager->loadEntityByConfigTarget($this->entityType, $this->options['target'])) { + $dependencies[$this->entityManager->getDefinition($this->entityType)->getConfigDependencyKey()][] = $entity->getConfigDependencyName(); + } + } + + return $dependencies; + } } diff --git a/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php b/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php index bf6fbf6..4da1200 100644 --- a/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php +++ b/core/modules/views/src/Plugin/views/area/TokenizeAreaPluginBase.php @@ -106,7 +106,7 @@ public function tokenForm(&$form, FormStateInterface $form_state) { */ public function tokenizeValue($value) { if ($this->options['tokenize']) { - $value = $this->view->style_plugin->tokenizeValue($value, 0); + $value = $this->view->getStyle()->tokenizeValue($value, 0); } // As we add the globalTokenForm() we also should replace the token here. return $this->globalTokenReplace($value); diff --git a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php index 61c9e84..e0f33cb 100644 --- a/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php +++ b/core/modules/views/src/Plugin/views/display/DisplayPluginBase.php @@ -1053,7 +1053,7 @@ public function optionLink($text, $section, $class = '', $title = '') { * This function is similar to views_handler_field::getRenderTokens() * but without fields tokens. */ - public function getArgumentsTokens() { + public function getArgumentTokens() { $tokens = array(); if (!empty($this->view->build_info['substitutions'])) { $tokens = $this->view->build_info['substitutions']; @@ -2131,7 +2131,7 @@ public function renderMoreLink() { $path = $this->getPath(); if ($this->getOption('link_display') == 'custom_url' && $override_path = $this->getOption('link_url')) { - $tokens = $this->getArgumentsTokens(); + $tokens = $this->getArgumentTokens(); $path = $this->viewsTokenReplace($override_path, $tokens); } diff --git a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php index 74c8620..520af7c 100644 --- a/core/modules/views/src/Plugin/views/field/FieldPluginBase.php +++ b/core/modules/views/src/Plugin/views/field/FieldPluginBase.php @@ -331,7 +331,7 @@ public function elementClasses($row_index = NULL) { * {@inheritdoc} */ public function tokenizeValue($value, $row_index = NULL) { - if (strpos($value, '{{') !== FALSE || strpos($value, '!') !== FALSE || strpos($value, '%') !== FALSE) { + if ($this->view->getStyle()->hasRenderToken($value) || $this->view->getStyle()->hasArgumentToken($value)) { $fake_item = array( 'alter_text' => TRUE, 'text' => $value, @@ -1468,22 +1468,7 @@ protected function renderAsLink($alter, $text, $tokens) { * {@inheritdoc} */ public function getRenderTokens($item) { - $tokens = array(); - if (!empty($this->view->build_info['substitutions'])) { - $tokens = $this->view->build_info['substitutions']; - } - $count = 0; - foreach ($this->view->display_handler->getHandlers('argument') as $arg => $handler) { - $token = '%' . ++$count; - if (!isset($tokens[$token])) { - $tokens[$token] = ''; - } - - // Use strip tags as there should never be HTML in the path. - // However, we need to preserve special characters like " that - // were removed by String::checkPlain(). - $tokens['!' . $count] = isset($this->view->args[$count - 1]) ? strip_tags(String::decodeEntities($this->view->args[$count - 1])) : ''; - } + $tokens = $this->view->display_handler->getArgumentTokens(); // Get flattened set of tokens for any array depth in query parameters. $tokens += $this->getTokenValuesRecursive($this->view->getRequest()->query->all()); diff --git a/core/modules/views/src/Plugin/views/style/StylePluginBase.php b/core/modules/views/src/Plugin/views/style/StylePluginBase.php index 9c229da..14389ad 100644 --- a/core/modules/views/src/Plugin/views/style/StylePluginBase.php +++ b/core/modules/views/src/Plugin/views/style/StylePluginBase.php @@ -184,20 +184,6 @@ function usesFields() { } /** - * Return TRUE if this style uses tokens. - * - * Used to ensure we don't fetch tokens when not needed for performance. - */ - public function usesTokens() { - if ($this->usesRowClass()) { - $class = $this->options['row_class']; - if (strpos($class, '{{') !== FALSE || strpos($class, '!') !== FALSE || strpos($class, '%') !== FALSE) { - return TRUE; - } - } - } - - /** * Return TRUE if this style enables field labels by default. * * @return bool @@ -228,7 +214,7 @@ public function getRowClass($row_index) { * Take a value and apply token replacement logic to it. */ public function tokenizeValue($value, $row_index) { - if (strpos($value, '{{') !== FALSE || strpos($value, '!') !== FALSE || strpos($value, '%') !== FALSE) { + if ($this->hasArgumentToken($value) || $this->hasRenderToken($value)) { // Row tokens might be empty, for example for node row style. $tokens = isset($this->rowTokens[$row_index]) ? $this->rowTokens[$row_index] : array(); if (!empty($this->view->build_info['substitutions'])) { @@ -239,7 +225,6 @@ public function tokenizeValue($value, $row_index) { } return $value; } - /** * Should the output of the style plugin be rendered even if it's a empty view. */ diff --git a/core/modules/views/src/Tests/Handler/AreaEntityTest.php b/core/modules/views/src/Tests/Handler/AreaEntityTest.php index af44b44..bbcacaf 100644 --- a/core/modules/views/src/Tests/Handler/AreaEntityTest.php +++ b/core/modules/views/src/Tests/Handler/AreaEntityTest.php @@ -73,17 +73,44 @@ public function testEntityAreaData() { */ public function testEntityArea() { + /** @var \Drupal\Core\Entity\EntityInterface[] $entities */ $entities = array(); for ($i = 0; $i < 3; $i++) { $random_label = $this->randomMachineName(); $data = array('bundle' => 'entity_test', 'name' => $random_label); $entity_test = $this->container->get('entity.manager')->getStorage('entity_test')->create($data); + + $uuid_map[0] = 'aa0c61cb-b7bb-4795-972a-493dabcf529c'; + $uuid_map[1] = '62cef0ff-6f30-4f7a-b9d6-a8ed5a3a6bf3'; + $uuid_map[2] = '3161d6e9-3326-4719-b513-8fa68a731ba2'; + $entity_test->uuid->value = $uuid_map[$i]; + $entity_test->save(); $entities[] = $entity_test; \Drupal::state()->set('entity_test_entity_access.view.' . $entity_test->id(), $i != 2); + } $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.'); + + // Specify 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['target'] = $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..c948544 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 @@ -1,6 +1,8 @@ langcode: und status: true -dependencies: { } +dependencies: + content: + - entity_test:entity_test:aa0c61cb-b7bb-4795-972a-493dabcf529c id: test_entity_area label: '' module: views @@ -21,7 +23,7 @@ display: field: entity_entity_test id: entity_entity_test table: views - entity_id: '1' + target: 'aa0c61cb-b7bb-4795-972a-493dabcf529c' view_mode: full plugin_id: entity footer: @@ -29,7 +31,7 @@ display: field: entity_entity_test id: entity_entity_test table: views - entity_id: '!1' + target: '!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..b4cb957 --- /dev/null +++ b/core/modules/views/tests/src/Unit/Plugin/area/EntityTest.php @@ -0,0 +1,314 @@ +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); + + $this->display->expects($this->any()) + ->method('getPlugin') + ->with('style') + ->willReturn($this->stylePlugin); + $this->executable->expects($this->any()) + ->method('getStyle') + ->willReturn($this->stylePlugin); + + + $token = $this->getMockBuilder('Drupal\Core\Utility\Token') + ->disableOriginalConstructor() + ->getMock(); + $token->expects($this->any()) + ->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 = [ + 'target' => 1, + 'tokenize' => FALSE, + ]; + + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity->expects($this->once()) + ->method('access') + ->willReturn(TRUE); + + $this->entityStorage->expects($this->never()) + ->method('loadByProperties'); + $this->entityManager->expects($this->any()) + ->method('loadEntityByConfigTarget') + ->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 = [ + 'target' => '!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->stylePlugin->expects($this->once()) + ->method('hasToken') + ->with('!1') + ->willReturn(TRUE); + + $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 = [ + 'target' => $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->entityManager->expects($this->once()) + ->method('loadEntityByConfigTarget') + ->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 ::calculateDependencies + */ + public function testCalculateDependenciesWithPlaceholder() { + $this->setupEntityManager(); + + $options = [ + 'target' => '!1', + ]; + $this->entityHandler->init($this->executable, $this->display, $options); + + $this->assertEquals([], $this->entityHandler->calculateDependencies()); + } + + /** + * @covers ::calculateDependencies + */ + public function testCalculateDependenciesWithUuid() { + $this->setupEntityManager(); + + $uuid = '1d52762e-b9d8-4177-908f-572d1a5845a4'; + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface'); + $entity->expects($this->once()) + ->method('getConfigDependencyName') + ->willReturn('entity_test:test-bundle:1d52762e-b9d8-4177-908f-572d1a5845a4'); + $this->entityStorage->expects($this->never()) + ->method('load'); + $this->entityManager->expects($this->once()) + ->method('loadEntityByConfigTarget') + ->willReturn($entity); + $entity_type->expects($this->once()) + ->method('getConfigDependencyKey') + ->willReturn('content'); + $this->entityManager->expects($this->once()) + ->method('getDefinition') + ->willReturn($entity_type); + + $options = [ + 'target' => $uuid, + ]; + $this->entityHandler->init($this->executable, $this->display, $options); + + $this->assertEquals(['content' => ['entity_test:test-bundle:1d52762e-b9d8-4177-908f-572d1a5845a4']], $this->entityHandler->calculateDependencies()); + } + + /** + * @covers ::calculateDependencies + */ + public function testCalculateDependenciesWithEntityId() { + $this->setupEntityManager(); + + $uuid = '1d52762e-b9d8-4177-908f-572d1a5845a4'; + $entity = $this->getMock('Drupal\Core\Entity\EntityInterface'); + $entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface'); + $entity->expects($this->once()) + ->method('getConfigDependencyName') + ->willReturn('entity_test:test-bundle:1d52762e-b9d8-4177-908f-572d1a5845a4'); + $this->entityManager->expects($this->once()) + ->method('loadEntityByConfigTarget') + ->willReturn($entity); + $this->entityStorage->expects($this->never()) + ->method('loadByProperties'); + $entity_type->expects($this->once()) + ->method('getConfigDependencyKey') + ->willReturn('content'); + $this->entityManager->expects($this->once()) + ->method('getDefinition') + ->willReturn($entity_type); + + $options = [ + 'target' => 1, + ]; + $this->entityHandler->init($this->executable, $this->display, $options); + + $this->assertEquals(['content' => ['entity_test:test-bundle:1d52762e-b9d8-4177-908f-572d1a5845a4']], $this->entityHandler->calculateDependencies()); + } + +} diff --git a/core/modules/views/tests/src/Unit/PluginBaseTest.php b/core/modules/views/tests/src/Unit/PluginBaseTest.php index 9c180a6..240f36a 100644 --- a/core/modules/views/tests/src/Unit/PluginBaseTest.php +++ b/core/modules/views/tests/src/Unit/PluginBaseTest.php @@ -320,4 +320,103 @@ public function providerTestFilterByDefinedOptions() { return $data; } + /** + * @covers ::hasArgumentToken + * @dataProvider providerHasArgumentToken + */ + public function testHasArgumentToken($value, $return) { + $this->assertEquals($this->testHelperPlugin->hasArgumentToken($value), $return); + } + + /** + * Data provider for testHasArgumentToken(). + * + * @return array + */ + public function providerHasArgumentToken() { + return array( + array('%', FALSE), + array('!', FALSE), + array('!', FALSE), + array('% 1', FALSE), + array('! 1', FALSE), + array('!1', TRUE), + array('%1', TRUE), + array(' !12345 ', TRUE), + array(' %54321 ', TRUE), + array('%elephant', FALSE), + array('!elephant', FALSE), + array('[old_token]', FALSE), + array('[other:token]', FALSE), + array('{{render_token}}', FALSE), + ); + } + + /** + * @covers ::hasRenderToken + * @dataProvider providerHasRenderToken + */ + public function testHasRenderToken($value, $return) { + $this->assertEquals($this->testHelperPlugin->hasRenderToken($value), $return); + } + + /** + * Data provider for testHasRenderToken(). + * + * @return array + */ + public function providerHasRenderToken() { + return array( + array('%1', FALSE), + array('!1', FALSE), + array('{', FALSE), + array('{{', FALSE), + array('}', FALSE), + array('}}', FALSE), + array('{{}}', FALSE), + array('{{{{}}', FALSE), + array('{{ elephant }}', TRUE), + array(' {{elephant_render_token_1}} ', TRUE), + ); + } + + /** + * @covers ::hasToken + * @dataProvider providerHasToken + */ + public function testHasToken($value, $return) { + $this->assertEquals($this->testHelperPlugin->hasToken($value), $return); + } + + /** + * Data provider for testHasToken(). + * + * @return array + */ + public function providerHasToken() { + return array( + array('%', FALSE), + array('!', FALSE), + array('!', FALSE), + array('% 1', FALSE), + array('! 1', FALSE), + array('!1', TRUE), + array('%1', TRUE), + array(' !12345 ', TRUE), + array(' %54321 ', TRUE), + array('%elephant', FALSE), + array('!elephant', FALSE), + array('{', FALSE), + array('{{', FALSE), + array('}', FALSE), + array('}}', FALSE), + array('{{}}', FALSE), + array('{{{{}}', FALSE), + array('{{ elephant }}', TRUE), + array(' {{elephant_render_token_1}} ', TRUE), + array('[old_token]', FALSE), + array('[other:token]', FALSE), + ); + } + } diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php index 2f3e417..3644313 100644 --- a/core/modules/views_ui/src/ViewUI.php +++ b/core/modules/views_ui/src/ViewUI.php @@ -1153,6 +1153,13 @@ public function getConfigDependencyName() { /** * {@inheritdoc} */ + public function getConfigTarget() { + return $this->storage->getConfigTarget(); + } + + /** + * {@inheritdoc} + */ public function onDependencyRemoval(array $dependencies) { return $this->storage->onDependencyRemoval($dependencies); }