diff --git a/core/lib/Drupal/Core/Entity/EntityType.php b/core/lib/Drupal/Core/Entity/EntityType.php index 37ed343..5d7145e 100644 --- a/core/lib/Drupal/Core/Entity/EntityType.php +++ b/core/lib/Drupal/Core/Entity/EntityType.php @@ -306,6 +306,14 @@ public function hasKey($key) { /** * {@inheritdoc} */ + public function setKey($key, $value) { + $this->entity_keys[$key] = $value; + return $this; + } + + /** + * {@inheritdoc} + */ public function id() { return $this->id; } diff --git a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php index a1c1bc9..d23e15c 100644 --- a/core/lib/Drupal/Core/Entity/EntityTypeInterface.php +++ b/core/lib/Drupal/Core/Entity/EntityTypeInterface.php @@ -141,6 +141,18 @@ public function getKey($key); public function hasKey($key); /** + * Sets a specific entity key. + * + * @param string $key + * The name of the entity key. + * @param string $value + * The new value of the key. + * + * @return $this + */ + public function setKey($key, $value); + + /** * Indicates whether entities should be statically cached. * * @return bool diff --git a/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php b/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php index 830d2ab..9bbde56 100644 --- a/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php +++ b/core/lib/Drupal/Core/Entity/Schema/SqlContentEntityStorageSchema.php @@ -26,7 +26,7 @@ class SqlContentEntityStorageSchema implements EntitySchemaHandlerInterface { /** * The storage field definitions for this entity type. * - * @var \Drupal\Core\Field\FieldDefinitionInterface[] + * @var \Drupal\Core\Field\FieldStorageDefinitionInterface[] */ protected $fieldStorageDefinitions; diff --git a/core/modules/comment/src/Tests/CommentTestBase.php b/core/modules/comment/src/Tests/CommentTestBase.php index 05f139d..bb4e55e 100644 --- a/core/modules/comment/src/Tests/CommentTestBase.php +++ b/core/modules/comment/src/Tests/CommentTestBase.php @@ -82,7 +82,7 @@ protected function setUp() { $this->container->get('comment.manager')->addDefaultField('node', 'article'); // Create a test node authored by the web user. - $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'uid' => $this->web_user->id())); + $this->node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'uid' => $this->web_user->id(), 'langcode' => 'en')); } /** diff --git a/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php b/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php index 61fa85e..c4150f3 100644 --- a/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php +++ b/core/modules/contextual/src/Tests/ContextualDynamicContextTest.php @@ -50,9 +50,9 @@ function testDifferentPermissions() { // - An article, which should be user-editable. // - A page, which should not be user-editable. // - A second article, which should also be user-editable. - $node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); - $node2 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1)); - $node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); + $node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'langcode' => 'en')); + $node2 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 1, 'langcode' => 'en')); + $node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'langcode' => 'en')); // Now, on the front page, all article nodes should have contextual links // placeholders, as should the view that contains them. diff --git a/core/modules/file/src/FileViewsData.php b/core/modules/file/src/FileViewsData.php index 1c6e524..5a977a2 100644 --- a/core/modules/file/src/FileViewsData.php +++ b/core/modules/file/src/FileViewsData.php @@ -7,118 +7,47 @@ namespace Drupal\file; -use Drupal\views\EntityViewsDataInterface; +use Drupal\views\EntityViewsData; /** * Provides views data for the file entity type. */ -class FileViewsData implements EntityViewsDataInterface { +class FileViewsData extends EntityViewsData { /** * {@inheritdoc} */ public function getViewsData() { - $data = array(); - // Sets 'group' index for file_managed table. - $data['file_managed']['table']['group'] = t('File'); + $data = parent::getViewsData(); - // Advertise this table as a possible base table. - $data['file_managed']['table']['base'] = array( - 'field' => 'fid', - 'title' => t('File'), - 'help' => t("Files maintained by Drupal and various modules."), - 'defaults' => array( - 'field' => 'filename' - ), - ); - $data['file_managed']['table']['entity type'] = 'file'; + // @TODO There is no corresponding information in entity metadata. + $data['file_managed']['table']['base']['help'] = t('Files maintained by Drupal and various modules.'); + $data['file_managed']['table']['base']['defaults']['field'] = 'filename'; $data['file_managed']['table']['wizard_id'] = 'file_managed'; - // Describes fid field in file_managed table. - $data['file_managed']['fid'] = array( - 'title' => t('File ID'), - 'help' => t('The ID of the file.'), - 'field' => array( - 'id' => 'file', - ), - 'argument' => array( - 'id' => 'file_fid', - // The field to display in the summary. - 'name field' => 'filename', - 'numeric' => TRUE, - ), - 'filter' => array( - 'id' => 'numeric', - ), - 'sort' => array( - 'id' => 'standard', - ), - 'relationship' => array( - 'title' => t('File usage'), - 'help' => t('Relate file entities to their usage.'), - 'id' => 'standard', - 'base' => 'file_usage', - 'base field' => 'fid', - 'field' => 'fid', - 'label' => t('File usage'), - ), + $data['file_managed']['fid']['field']['id'] ='file'; + $data['file_managed']['fid']['argument'] = array( + 'id' => 'file_fid', + // The field to display in the summary. + 'name field' => 'filename', + 'numeric' => TRUE, ); - - // Describes filename field in file_managed table. - $data['file_managed']['filename'] = array( - 'title' => t('Name'), - 'help' => t('The name of the file.'), - 'field' => array( - 'id' => 'file', - ), - 'sort' => array( - 'id' => 'standard', - ), - 'filter' => array( - 'id' => 'string', - ), - 'argument' => array( - 'id' => 'string', - ), + $data['file_managed']['fid']['relationship'] = array( + 'title' => t('File usage'), + 'help' => t('Relate file entities to their usage.'), + 'id' => 'standard', + 'base' => 'file_usage', + 'base field' => 'fid', + 'field' => 'fid', + 'label' => t('File usage'), ); - // Describes uri field in file_managed table. - $data['file_managed']['uri'] = array( - 'title' => t('Path'), - 'help' => t('The path of the file.'), - 'field' => array( - 'id' => 'file_uri', - ), - 'sort' => array( - 'id' => 'standard', - ), - 'filter' => array( - 'id' => 'string', - ), - 'argument' => array( - 'id' => 'string', - ), - ); + $data['file_managed']['filename']['field']['id'] = 'file'; - // Describes filemime field in file_managed table. - $data['file_managed']['filemime'] = array( - 'title' => t('Mime type'), - 'help' => t('The mime type of the file.'), - 'field' => array( - 'id' => 'file_filemime', - ), - 'sort' => array( - 'id' => 'standard', - ), - 'filter' => array( - 'id' => 'string', - ), - 'argument' => array( - 'id' => 'string', - ), - ); + $data['file_managed']['uri']['field']['id'] = 'file_uri'; + + $data['file_managed']['filemime']['field']['id'] = 'file_filemime'; - // Describes extension field in file_managed table. $data['file_managed']['extension'] = array( 'title' => t('Extension'), 'help' => t('The extension of the file.'), @@ -129,79 +58,14 @@ public function getViewsData() { ), ); - // Describes filesize field in file_managed table. - $data['file_managed']['filesize'] = array( - 'title' => t('Size'), - 'help' => t('The size of the file.'), - 'field' => array( - 'id' => 'file_size', - ), - 'sort' => array( - 'id' => 'standard', - ), - 'filter' => array( - 'id' => 'numeric', - ), - ); - - // Describes status field in file_managed table. - $data['file_managed']['status'] = array( - 'title' => t('Status'), - 'help' => t('The status of the file.'), - 'field' => array( - 'id' => 'file_status', - ), - 'sort' => array( - 'id' => 'standard', - ), - 'filter' => array( - 'id' => 'file_status', - ), - ); - - // Describes created field in file_managed table. - $data['file_managed']['created'] = array( - 'title' => t('Upload date'), - 'help' => t('The date the file was uploaded.'), - 'field' => array( - 'id' => 'date', - ), - 'sort' => array( - 'id' => 'date', - ), - 'filter' => array( - 'id' => 'date', - ), - ); + $data['file_managed']['filesize']['field']['id'] = 'file_size'; - // Describes changed field in file_managed table. - $data['file_managed']['changed'] = array( - 'title' => t('Modified date'), - 'help' => t('The date the file was last changed.'), - 'field' => array( - 'id' => 'date', - ), - 'sort' => array( - 'id' => 'date', - ), - 'filter' => array( - 'id' => 'date', - ), - ); + $data['file_managed']['status']['field']['id'] = 'file_status'; + $data['file_managed']['status']['filter']['id'] = 'file_status'; - // Describes uid field in file_managed table. - $data['file_managed']['uid'] = array( - 'title' => t('User who uploaded'), - 'help' => t('The user that uploaded the file.'), - 'relationship' => array( - 'title' => t('User who uploaded'), - 'label' => t('User who uploaded'), - 'base' => 'users', - 'base field' => 'uid', - ), - ); + $data['file_managed']['uid']['relationship']['title'] = t('User who uploaded'); + $data['file_managed']['uid']['relationship']['label'] = t('User who uploaded'); - // Sets 'group' index for file_usage table. $data['file_usage']['table']['group'] = t('File Usage'); // Provide field-type-things to several base tables; on the core files table @@ -499,4 +363,3 @@ public function getViewsData() { } } - diff --git a/core/modules/file/src/Tests/FileFieldRSSContentTest.php b/core/modules/file/src/Tests/FileFieldRSSContentTest.php index db265c2..42b88ce 100644 --- a/core/modules/file/src/Tests/FileFieldRSSContentTest.php +++ b/core/modules/file/src/Tests/FileFieldRSSContentTest.php @@ -51,7 +51,7 @@ function testFileFieldRSSContent() { // Create a new node with a file field set. Promote to frontpage // needs to be set so this node will appear in the RSS feed. - $node = $this->drupalCreateNode(array('type' => $type_name, 'promote' => 1)); + $node = $this->drupalCreateNode(array('type' => $type_name, 'promote' => 1, 'langcode' => 'en')); $test_file = $this->getTestFile('text'); // Create a new node with the uploaded file. diff --git a/core/modules/node/src/NodeViewsData.php b/core/modules/node/src/NodeViewsData.php index 6d69a58..008d175 100644 --- a/core/modules/node/src/NodeViewsData.php +++ b/core/modules/node/src/NodeViewsData.php @@ -7,169 +7,48 @@ namespace Drupal\node; +use Drupal\views\EntityViewsData; use Drupal\views\EntityViewsDataInterface; /** * Provides the views data for the node entity type. */ -class NodeViewsData implements EntityViewsDataInterface { +class NodeViewsData extends EntityViewsData implements EntityViewsDataInterface { /** * {@inheritdoc} */ public function getViewsData() { - // Define the base group of this table. Fields that don't have a group defined - // will go into this field by default. - $data['node']['table']['group'] = t('Content'); + $data = parent::getViewsData(); - // Advertise this table as a possible base table. - $data['node']['table']['base'] = array( - 'field' => 'nid', - 'title' => t('Content'), - 'weight' => -10, - 'access query tag' => 'node_access', - 'defaults' => array( - 'field' => 'title', - ), - ); - $data['node']['table']['entity type'] = 'node'; + $data['node']['table']['base']['weight'] = -10; + $data['node']['table']['base']['access query tag'] = 'node_access'; $data['node']['table']['wizard_id'] = 'node'; - $data['node_field_data']['table']['group'] = t('Content'); - $data['node_field_data']['table']['entity type'] = 'node'; - $data['node_field_data']['table']['join']['node'] = array( - 'type' => 'INNER', - 'left_field' => 'nid', - 'field' => 'nid', - ); + $data['node']['nid']['field']['id'] = 'node'; + $data['node']['nid']['field']['argument'] = [ + 'id' => 'node_nid', + 'name field' => 'title', + 'numeric' => TRUE, + 'validate type' => 'nid', + ]; - $data['node']['nid'] = array( - 'title' => t('Nid'), - 'help' => t('The node ID.'), - 'field' => array( - 'id' => 'node', - ), - 'argument' => array( - 'id' => 'node_nid', - 'name field' => 'title', - 'numeric' => TRUE, - 'validate type' => 'nid', - ), - 'filter' => array( - 'id' => 'numeric', - ), - 'sort' => array( - 'id' => 'standard', - ), - ); + $data['node_field_data']['title']['field']['id'] = 'node'; + $data['node_field_data']['title']['field']['link_to_node default'] = TRUE; - // This definition has more items in it than it needs to as an example. - $data['node_field_data']['title'] = array( - 'title' => t('Title'), - 'help' => t('The content title.'), - 'field' => array( - // This is the real field which could be left out since it is the same. - 'field' => 'title', - // This is the UI group which could be left out since it is the same. - 'group' => t('Content'), - 'id' => 'node', - 'link_to_node default' => TRUE, - ), - 'sort' => array( - 'id' => 'standard', - ), - 'filter' => array( - 'id' => 'string', - ), - 'argument' => array( - 'id' => 'string', - ), - ); + $data['node_field_data']['type']['field']['id'] = 'node_type'; + $data['node_field_data']['type']['argument']['id'] = 'node_type'; - $data['node_field_data']['created'] = array( - 'title' => t('Post date'), - 'help' => t('The date the content was posted.'), - 'field' => array( - 'id' => 'date', - ), - 'sort' => array( - 'id' => 'date' - ), - 'filter' => array( - 'id' => 'date', - ), - ); + $data['node_field_data']['langcode']['help'] = t('The language of the content or translation.'); + $data['node_field_data']['langcode']['field']['id'] = 'node_language'; - $data['node_field_data']['changed'] = array( - 'title' => t('Updated date'), - 'help' => t('The date the content was last updated.'), - 'field' => array( - 'id' => 'date', - ), - 'sort' => array( - 'id' => 'date' - ), - 'filter' => array( - 'id' => 'date', - ), - ); - - $data['node_field_data']['type'] = array( - 'title' => t('Type'), - 'help' => t('The content type (for example, "blog entry", "forum post", "story", etc).'), - 'field' => array( - 'id' => 'node_type', - ), - 'sort' => array( - 'id' => 'standard', - ), - 'filter' => array( - 'id' => 'bundle', - ), - 'argument' => array( - 'id' => 'node_type', - ), - ); - - if (\Drupal::moduleHandler()->moduleExists('language')) { - $data['node_field_data']['langcode'] = array( - 'title' => t('Translation language'), - 'help' => t('The language of the content or translation.'), - 'field' => array( - 'id' => 'node_language', - ), - 'filter' => array( - 'id' => 'language', - ), - 'argument' => array( - 'id' => 'language', - ), - 'sort' => array( - 'id' => 'standard', - ), - ); - } - - $data['node_field_data']['status'] = array( - 'title' => t('Published status'), - 'help' => t('Whether or not the content is published.'), - 'field' => array( - 'id' => 'boolean', - 'output formats' => array( - 'published-notpublished' => array(t('Published'), t('Not published')), - ), - ), - 'filter' => array( - 'id' => 'boolean', - 'label' => t('Published status'), - 'type' => 'yes-no', - // Use status = 1 instead of status <> 0 in WHERE statement. - 'use_equal' => TRUE, - ), - 'sort' => array( - 'id' => 'standard', - ), - ); + $data['node_field_data']['status']['field']['output formats'] = [ + 'published-notpublished' => array(t('Published'), t('Not published')), + ]; + $data['node_field_data']['status']['filter']['label'] = t('Published status'); + $data['node_field_data']['status']['filter']['type'] = 'yes-no'; + // Use status = 1 instead of status <> 0 in WHERE statement. + $data['node_field_data']['status']['filter']['use_equal'] = TRUE; $data['node_field_data']['status_extra'] = array( 'title' => t('Published status or admin user'), @@ -181,55 +60,27 @@ public function getViewsData() { ), ); - $data['node_field_data']['promote'] = array( - 'title' => t('Promoted to front page status'), - 'help' => t('Whether or not the content is promoted to the front page.'), - 'field' => array( - 'id' => 'boolean', - 'output formats' => array( - 'promoted-notpromoted' => array(t('Promoted'), t('Not promoted')), - ), - ), - 'filter' => array( - 'id' => 'boolean', - 'label' => t('Promoted to front page status'), - 'type' => 'yes-no', - ), - 'sort' => array( - 'id' => 'standard', - ), - ); + $data['node_field_data']['promote']['field']['output formats'] = [ + 'promoted-notpromoted' => array(t('Promoted'), t('Not promoted')), + ]; + $data['node_field_data']['promote']['filter']['label'] = t('Promoted to front page status'); + $data['node_field_data']['promote']['filter']['type'] = 'yes-no'; + + $data['node_field_data']['sticky']['field']['output formats'] = [ + 'sticky' => array(t('Sticky'), t('Not sticky')), + ]; + $data['node_field_data']['sticky']['filter']['label'] = t('Sticky status'); + $data['node_field_data']['sticky']['filter']['type'] = 'yes-no'; + $data['node_field_data']['sticky']['sort']['help'] = t('Whether or not the content is sticky. To list sticky content first, set this to descending.'); - $data['node_field_data']['sticky'] = array( - 'title' => t('Sticky status'), - 'help' => t('Whether or not the content is sticky.'), + $data['node']['translation_link'] = array( + 'title' => t('Translation link'), + 'help' => t('Provide a link to the translations overview for nodes.'), 'field' => array( - 'id' => 'boolean', - 'output formats' => array( - 'sticky' => array(t('Sticky'), t('Not sticky')), - ), - ), - 'filter' => array( - 'id' => 'boolean', - 'label' => t('Sticky status'), - 'type' => 'yes-no', - ), - 'sort' => array( - 'id' => 'standard', - 'help' => t('Whether or not the content is sticky. To list sticky content first, set this to descending.'), + 'id' => 'content_translation_link', ), ); - if (\Drupal::moduleHandler()->moduleExists('content_translation')) { - $data['node']['translation_link'] = array( - 'title' => t('Translation link'), - 'help' => t('Provide a link to the translations overview for nodes.'), - 'field' => array( - 'id' => 'content_translation_link', - ), - ); - } - $data['node']['view_node'] = array( 'field' => array( 'title' => t('Link to content'), @@ -264,6 +115,7 @@ public function getViewsData() { // Bogus fields for aliasing purposes. + // @Todo Add similar support to any date field. $data['node_field_data']['created_fulldate'] = array( 'title' => t('Created date'), 'help' => t('Date in the form of CCYYMMDD.'), @@ -372,27 +224,12 @@ public function getViewsData() { ), ); - $data['node_field_data']['uid'] = array( - 'title' => t('Author uid'), - 'help' => t('The user authoring the content. If you need more fields than the uid add the content: author relationship'), - 'relationship' => array( - 'title' => t('Content author'), - 'help' => t('Relate content to the user who created it.'), - 'id' => 'standard', - 'base' => 'users', - 'field' => 'uid', - 'label' => t('author'), - ), - 'filter' => array( - 'id' => 'user_name', - ), - 'argument' => array( - 'id' => 'numeric', - ), - 'field' => array( - 'id' => 'user', - ), - ); + $data['node_field_data']['uid']['help'] = t('The user authoring the content. If you need more fields than the uid add the content: author relationship'); + $data['node_field_data']['uid']['filter']['id'] = 'user_name'; + $data['node_field_data']['uid']['field']['id'] = 'user'; + $data['node_field_data']['uid']['relationship']['title'] = t('Content author'); + $data['node_field_data']['uid']['relationship']['help'] = t('Relate content to the user who created it.'); + $data['node_field_data']['uid']['relationship']['label'] = t('author'); $data['node']['node_listing_empty'] = array( 'title' => t('Empty Node Frontpage behavior'), @@ -402,83 +239,36 @@ public function getViewsData() { ), ); - $data['node_field_data']['uid_revision'] = array( - 'title' => t('User has a revision'), - 'help' => t('All nodes where a certain user has a revision'), - 'real field' => 'nid', - 'filter' => array( - 'id' => 'node_uid_revision', - ), - 'argument' => array( - 'id' => 'node_uid_revision', - ), - ); + $data['node_field_data']['uid_revision']['title'] = t('User has a revision'); + $data['node_field_data']['uid_revision']['help'] = t('All nodes where a certain user has a revision'); + $data['node_field_data']['uid_revision']['real field'] = 'nid'; + $data['node_field_data']['uid_revision']['filter']['id'] = 'node_uid_revision'; + $data['node_field_data']['uid_revision']['argument']['id'] = 'node_uid_revision'; - $data['node_revision']['table']['entity type'] = 'node'; - // Define the base group of this table. Fields that don't have a group defined - // will go into this field by default. - $data['node_revision']['table']['group'] = t('Content revision'); $data['node_revision']['table']['wizard_id'] = 'node_revision'; // Advertise this table as a possible base table. - $data['node_revision']['table']['base'] = array( - 'field' => 'vid', - 'title' => t('Content revision'), - 'help' => t('Content revision is a history of changes to content.'), - 'defaults' => array( - 'field' => 'title', - ), - ); - - // For other base tables, explain how we join. - $data['node_revision']['table']['join'] = array( - 'node' => array( - 'left_field' => 'vid', - 'field' => 'vid', - ), - ); - - $data['node_revision']['nid'] = array( - 'title' => t('Nid'), - 'help' => t('The revision NID of the content revision.'), - 'field' => array( - 'id' => 'standard', - ), - 'argument' => array( - 'id' => 'node_nid', - 'numeric' => TRUE, - ), - 'filter' => array( - 'id' => 'numeric', - ), - 'sort' => array( - 'id' => 'standard', - ), - 'relationship' => array( - 'id' => 'standard', - 'base' => 'node', - 'base field' => 'nid', - 'title' => t('Content'), - 'label' => t('Get the actual content from a content revision.'), - ), - ); + $data['node_revision']['table']['base']['help'] = t('Content revision is a history of changes to content.'); + $data['node_revision']['table']['base']['defaults']['title'] = 'title'; + + $data['node_revision']['nid']['argument'] = [ + 'id' => 'node_nid', + 'numeric' => TRUE, + ]; + // @todo the NID field needs different behaviour on revision/non-revision + // tables. It would be neat if this could be encoded in the base field + // definition. + $data['node_revision']['nid']['relationship']['id'] = 'standard'; + $data['node_revision']['nid']['relationship']['base'] = 'node'; + $data['node_revision']['nid']['relationship']['base field'] = 'nid'; + $data['node_revision']['nid']['relationship']['title'] = t('Content'); + $data['node_revision']['nid']['relationship']['label'] = t('Get the actual content from a content revision.'); $data['node_revision']['vid'] = array( - 'title' => t('Vid'), - 'help' => t('The revision ID of the content revision.'), - 'field' => array( - 'id' => 'standard', - ), 'argument' => array( 'id' => 'node_vid', 'numeric' => TRUE, ), - 'filter' => array( - 'id' => 'numeric', - ), - 'sort' => array( - 'id' => 'standard', - ), 'relationship' => array( 'id' => 'standard', 'base' => 'node', @@ -486,119 +276,29 @@ public function getViewsData() { 'title' => t('Content'), 'label' => t('Get the actual content from a content revision.'), ), - ); + ) + $data['node_revision']['vid']; - if (\Drupal::moduleHandler()->moduleExists('language')) { - $data['node_revision']['langcode'] = array( - 'title' => t('Original language'), - 'help' => t('The language the original content is in.'), - 'field' => array( - 'id' => 'node_language', - ), - 'filter' => array( - 'id' => 'language', - ), - 'argument' => array( - 'id' => 'language', - ), - 'sort' => array( - 'id' => 'standard', - ), - ); - } + $data['node_revision']['langcode']['help'] = t('The language the original content is in.'); + $data['node_revision']['langcode']['field']['id'] = 'node_language'; - $data['node_revision']['revision_log'] = array( - 'title' => t('Log message'), - 'help' => t('The log message entered when the revision was created.'), - 'field' => array( - 'id' => 'xss', - ), - 'filter' => array( - 'id' => 'string', - ), - ); + $data['node_revision']['revision_log']['field']['id'] = 'xss'; - $data['node_revision']['revision_uid'] = array( - 'title' => t('User'), - 'help' => t('Relate a content revision to the user who created the revision.'), - 'relationship' => array( - 'id' => 'standard', - 'base' => 'users', - 'base field' => 'uid', - 'label' => t('revision user'), - ), - ); + $data['node_revision']['revision_uid']['help'] = t('Relate a content revision to the user who created the revision.'); + $data['node_revision']['revision_uid']['relationship']['label'] = t('revision user'); - $data['node_field_revision']['table']['entity type'] = 'node'; - // Define the base group of this table. Fields that don't have a group defined - // will go into this field by default. - $data['node_field_revision']['table']['group'] = t('Content revision'); $data['node_field_revision']['table']['wizard_id'] = 'node_field_revision'; - // For other base tables, explain how we join. - $data['node_field_revision']['table']['join'] = array( - 'node' => array( - 'left_field' => 'vid', - 'field' => 'vid', - ), - 'node_revision' => array( - 'left_field' => 'vid', - 'field' => 'vid', - ), - ); + $data['node_field_revision']['table']['join']['node']['left_field'] = 'vid'; + $data['node_field_revision']['table']['join']['node']['field'] = 'vid'; - $data['node_field_revision']['status'] = array( - 'title' => t('Published'), - 'help' => t('Whether or not the content is published.'), - 'field' => array( - 'id' => 'boolean', - 'output formats' => array( - 'published-notpublished' => array(t('Published'), t('Not published')), - ), - ), - 'filter' => array( - 'id' => 'boolean', - 'label' => t('Published'), - 'type' => 'yes-no', - // Use status = 1 instead of status <> 0 in WHERE statement. - 'use_equal' => TRUE, - ), - 'sort' => array( - 'id' => 'standard', - ), - ); - - $data['node_field_revision']['title'] = array( - 'title' => t('Title'), - 'help' => t('The content title.'), - 'field' => array( - 'field' => 'title', - 'id' => 'node_revision', - ), - 'sort' => array( - 'id' => 'standard', - ), - 'filter' => array( - 'id' => 'string', - ), - 'argument' => array( - 'id' => 'string', - ), - ); + $data['node_field_revision']['status']['field']['output formats'] = [ + 'published-notpublished' => [t('Published'), t('Not published')], + ]; + $data['node_field_revision']['status']['filter']['label'] = t('Published'); + $data['node_field_revision']['status']['filter']['type'] = 'yes-no'; + $data['node_field_revision']['status']['filter']['use_equal'] = TRUE; - $data['node_field_revision']['changed'] = array( - 'title' => t('Updated date'), - 'help' => t('The date the content was last updated.'), - 'field' => array( - 'id' => 'date', - ), - 'sort' => array( - 'id' => 'date' - ), - 'filter' => array( - 'id' => 'date', - ), - ); + $data['node_field_revision']['title']['field']['id'] = 'node_revision'; $data['node_revision']['link_to_revision'] = array( 'field' => array( @@ -727,6 +427,4 @@ public function getViewsData() { return $data; } - } - diff --git a/core/modules/node/src/Tests/NodeLoadMultipleTest.php b/core/modules/node/src/Tests/NodeLoadMultipleTest.php index 3cea6a1..fbeb16b 100644 --- a/core/modules/node/src/Tests/NodeLoadMultipleTest.php +++ b/core/modules/node/src/Tests/NodeLoadMultipleTest.php @@ -31,10 +31,10 @@ protected function setUp() { * Creates four nodes and ensures that they are loaded correctly. */ function testNodeMultipleLoad() { - $node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); - $node2 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); - $node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 0)); - $node4 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 0)); + $node1 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'langcode' => 'en')); + $node2 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'langcode' => 'en')); + $node3 = $this->drupalCreateNode(array('type' => 'article', 'promote' => 0, 'langcode' => 'en')); + $node4 = $this->drupalCreateNode(array('type' => 'page', 'promote' => 0, 'langcode' => 'en')); // Confirm that promoted nodes appear in the default node listing. $this->drupalGet('node'); diff --git a/core/modules/node/src/Tests/NodeRSSContentTest.php b/core/modules/node/src/Tests/NodeRSSContentTest.php index e4b7cb3..6a1c830 100644 --- a/core/modules/node/src/Tests/NodeRSSContentTest.php +++ b/core/modules/node/src/Tests/NodeRSSContentTest.php @@ -40,7 +40,7 @@ protected function setUp() { */ function testNodeRSSContent() { // Create a node. - $node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1)); + $node = $this->drupalCreateNode(array('type' => 'article', 'promote' => 1, 'langcode' => 'en')); $this->drupalGet('rss.xml'); diff --git a/core/modules/system/entity.api.php b/core/modules/system/entity.api.php index f4bbfd2..f7411c6 100644 --- a/core/modules/system/entity.api.php +++ b/core/modules/system/entity.api.php @@ -310,7 +310,8 @@ * config entities will use \Drupal\Core\Config\Entity\ConfigEntityStorage. * You can extend one of these classes to provide custom behavior. * - views_data: A class implementing \Drupal\views\EntityViewsDataInterface - * to provide views data for the entity type. + * to provide views data for the entity type. You can autogenerate most of + * the views data by extending \Drupal\views\EntityViewsData. * - For content entities, the annotation will refer to a number of database * tables and their fields. These annotation properties, such as 'base_table', * 'data_table', 'entity_keys', etc., are documented on diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php index 3993dcf..1b065af 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTest.php @@ -29,7 +29,8 @@ * "default" = "Drupal\entity_test\EntityTestForm", * "delete" = "Drupal\entity_test\EntityTestDeleteForm" * }, - * "translation" = "Drupal\content_translation\ContentTranslationHandler" + * "translation" = "Drupal\content_translation\ContentTranslationHandler", + * "views_data" = "Drupal\views\EntityViewsData" * }, * base_table = "entity_test", * fieldable = TRUE, diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php index fa66635..4e03fd1 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMul.php @@ -24,7 +24,8 @@ * "default" = "Drupal\entity_test\EntityTestForm", * "delete" = "Drupal\entity_test\EntityTestDeleteForm" * }, - * "translation" = "Drupal\content_translation\ContentTranslationHandler" + * "translation" = "Drupal\content_translation\ContentTranslationHandler", + * "views_data" = "Drupal\views\EntityViewsData" * }, * base_table = "entity_test_mul", * data_table = "entity_test_mul_property_data", diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php index 50afa32..13aca32 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulRev.php @@ -23,7 +23,8 @@ * "default" = "Drupal\entity_test\EntityTestForm", * "delete" = "Drupal\entity_test\EntityTestDeleteForm" * }, - * "translation" = "Drupal\content_translation\ContentTranslationHandler" + * "translation" = "Drupal\content_translation\ContentTranslationHandler", + * "views_data" = "Drupal\views\EntityViewsData" * }, * base_table = "entity_test_mulrev", * data_table = "entity_test_mulrev_property_data", diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php index ebb39f2..f97d2fd 100644 --- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php +++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestRev.php @@ -23,7 +23,8 @@ * "default" = "Drupal\entity_test\EntityTestForm", * "delete" = "Drupal\entity_test\EntityTestDeleteForm" * }, - * "translation" = "Drupal\content_translation\ContentTranslationHandler" + * "translation" = "Drupal\content_translation\ContentTranslationHandler", + * "views_data" = "Drupal\views\EntityViewsData" * }, * base_table = "entity_test_rev", * revision_table = "entity_test_rev_revision", diff --git a/core/modules/views/src/EntityViewsData.php b/core/modules/views/src/EntityViewsData.php new file mode 100644 index 0000000..9a86f77 --- /dev/null +++ b/core/modules/views/src/EntityViewsData.php @@ -0,0 +1,444 @@ +entityType = $entity_type; + $this->entityManager = $entity_manager; + $this->storage = $storage_controller; + $this->moduleHandler = $module_handler; + $this->setStringTranslation($translation_manager); + $this->typedDataManager = $typed_data_manager; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('entity.manager')->getStorage($entity_type->id()), + $container->get('entity.manager'), + $container->get('module_handler'), + $container->get('string_translation'), + $container->get('typed_data_manager') + ); + } + + /** + * Gets the field storage definitions. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface[] + */ + protected function getFieldStorageDefinitions() { + if (!isset($this->fieldStorageDefinitions)) { + $this->fieldStorageDefinitions = $this->entityManager->getFieldStorageDefinitions($this->entityType->id()); + } + return $this->fieldStorageDefinitions; + } + + /** + * {@inheritdoc} + */ + public function getViewsData() { + $data = []; + + // @todo In theory we should use the data table as base table, as this would + // save one pointless join (and one more for every relationship). + $base_table = $this->entityType->getBaseTable(); + $base_field = $this->entityType->getKey('id'); + $data_table = $this->entityType->getDataTable(); + $revision_table = $this->entityType->getRevisionTable(); + $revision_data_table = $this->entityType->getRevisionDataTable(); + $revision_field = $this->entityType->getKey('revision'); + + // Setup base information of the views data. + $data[$base_table]['table']['entity type'] = $this->entityType->id(); + $data[$base_table]['table']['group'] = $this->entityType->getLabel(); + $data[$base_table]['table']['base'] = [ + 'field' => $base_field, + 'title' => $this->entityType->getLabel(), + ]; + + if ($label_key = $this->entityType->getKey('label')) { + if ($data_table) { + $data[$base_table]['table']['base']['defaults'] = array( + 'field' => $label_key, + 'table' => $data_table, + ); + } + else { + $data[$base_table]['table']['base']['defaults'] = array( + 'field' => $label_key, + ); + } + } + + // Setup relations to the revisions/property data. + if ($data_table) { + $data[$data_table]['table']['join'][$base_table] = [ + 'left_field' => $base_field, + 'field' => $base_field, + 'type' => 'INNER' + ]; + $data[$data_table]['table']['entity type'] = $this->entityType->id(); + $data[$data_table]['table']['group'] = $this->entityType->getLabel(); + } + if ($revision_table) { + $data[$revision_table]['table']['entity type'] = $this->entityType->id(); + $data[$revision_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]); + $data[$revision_table]['table']['base'] = array( + 'field' => $revision_field, + 'title' => $this->t('@entity_type revisions', array('@entity_type' => $this->entityType->getLabel())), + ); + // Join the revision table to the base table. + $data[$revision_table]['table']['join'][$base_table] = array( + 'left_field' => $revision_field, + 'field' => $revision_field, + 'type' => 'INNER', + ); + + if ($revision_data_table) { + $data[$revision_data_table]['table']['entity type'] = $this->entityType->id(); + $data[$revision_data_table]['table']['group'] = $this->t('@entity_type revision', ['@entity_type' => $this->entityType->getLabel()]); + + $data[$revision_data_table]['table']['join'][$revision_table] = array( + 'left_field' => $revision_field, + 'field' => $revision_field, + 'type' => 'INNER', + ); + } + } + + // Load all typed data definitions of all fields. This should cover each of + // the entity base, revision, data tables. + $field_definitions = $this->entityManager->getBaseFieldDefinitions($this->entityType->id()); + if ($table_mapping = $this->storage->getTableMapping()) { + // Iterate over each table we have so far and collect field data for each. + // Based on whether the field is in the field_definitions provided by the + // entity manager. + // @todo We should better just rely on information coming from the entity + // storage. + foreach ($table_mapping->getTableNames() as $table) { + foreach ($table_mapping->getFieldNames($table) as $field_name) { + $this->mapFieldDefinition($table, $field_name, $field_definitions[$field_name], $table_mapping, $data[$table]); + } + } + } + + return $data; + } + + /** + * Puts the views data for a single field onto the views data. + * + * @param string $field_name + * The name of the field to handle. + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The field definition defined in Entity::baseFieldDefinitions() + * @param \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping + * The table mapping information + * @param array $table_data + * A reference to a specific entity table (for example data_table) inside + * the views data. + */ + protected function mapFieldDefinition($table, $field_name, FieldDefinitionInterface $field_definition, TableMappingInterface $table_mapping, &$table_data) { + // Create a dummy instance to retrieve property definitions. + $field_column_mapping = $table_mapping->getColumnNames($field_name); + $field_schema = $this->getFieldStorageDefinitions()[$field_name]->getSchema(); + + $field_definition_type = $field_definition->getType(); + // Add all properties to views table data. + $first = TRUE; + foreach ($field_column_mapping as $field_column_name => $schema_field_name) { + $schema = $field_schema['columns'][$field_column_name]; + // We want to both have an entry in the views data for the actual field, + // but also each additional schema field, for example the file + // description. + // @todo Introduce a concept of the "main" schema field for a field item. + // This would be the FID for a file reference for example. + if ($first) { + $first = FALSE; + $table_data[$field_name] = $this->mapSingleFieldViewsData($table, $field_name, $field_definition_type, $schema_field_name, $field_definition, TRUE); + } + else { + $table_data["$field_name.$field_column_name"] = $this->mapSingleFieldViewsData($table, $field_name, $schema['type'], $schema_field_name, $field_definition, FALSE); + } + } + } + + /** + * Provides the views data for a given data type and schema field. + * + * @param string $data_type + * The data type to generate views data for, for example "int". The data + * type comes directly from the schema definition of each field item. + * @param string $schema_field_name + * The schema field name. + * @param bool $first + * Is it the first column of the schema. + * + * @return array + * The modified views data field definition. + */ + protected function mapSingleFieldViewsData($table, $field_name, $data_type, $schema_field_name, FieldDefinitionInterface $field_definition, $first) { + $views_field = array(); + + // Provide a nicer, less verbose label for the first field. + if ($first) { + $views_field['title'] = $field_definition->getLabel(); + } + else { + $views_field['title'] = $field_definition->getLabel() . " ($schema_field_name)"; + } + + if ($description = $field_definition->getDescription()) { + $views_field['help'] = $description; + } + + // @todo Allow field types to customize this. + switch ($data_type) { + case 'int': + case 'integer': + case 'smallint': + case 'tinyint': + case 'mediumint': + case 'float': + case 'double': + case 'decimal': + $views_field['field']['id'] = 'numeric'; + $views_field['argument']['id'] = 'numeric'; + $views_field['filter']['id'] = 'numeric'; + $views_field['sort']['id'] = 'standard'; + break; + case 'char': + case 'string': + case 'varchar': + case 'tinytext': + case 'text': + case 'mediumtext': + case 'longtext': + $views_field['field']['id'] = 'standard'; + $views_field['argument']['id'] = 'string'; + $views_field['filter']['id'] = 'string'; + $views_field['sort']['id'] = 'standard'; + break; + case 'boolean': + $views_field['field']['id'] = 'boolean'; + $views_field['argument']['id'] = 'numeric'; + $views_field['filter']['id'] = 'boolean'; + $views_field['sort']['id'] = 'standard'; + break; + case 'uuid': + $views_field['field']['id'] = 'standard'; + $views_field['argument']['id'] = 'string'; + $views_field['filter']['id'] = 'string'; + $views_field['sort']['id'] = 'standard'; + break; + case 'language': + $views_field['field']['id'] = 'language'; + $views_field['argument']['id'] = 'language'; + $views_field['filter']['id'] = 'language'; + $views_field['sort']['id'] = 'standard'; + break; + case 'created': + case 'changed': + $views_field['field']['id'] = 'date'; + $views_field['argument']['id'] = 'date'; + $views_field['filter']['id'] = 'date'; + $views_field['sort']['id'] = 'date'; + break; + case 'entity_reference': + // @todo Should the actual field handler respect that this is just renders a number + // @todo Create an optional entity field handler, that can render the + // entity. + $views_field['field']['id'] = 'standard'; + $views_field['argument']['id'] = 'standard'; + $views_field['filter']['id'] = 'standard'; + $views_field['sort']['id'] = 'standard'; + break; + case 'uri': + $views_field['field']['id'] = 'standard'; + $views_field['argument']['id'] = 'string'; + $views_field['filter']['id'] = 'string'; + $views_field['sort']['id'] = 'standard'; + break; + default: + $views_field['field']['id'] = 'standard'; + $views_field['argument']['id'] = 'standard'; + $views_field['filter']['id'] = 'standard'; + $views_field['sort']['id'] = 'standard'; + } + + $process_method = 'processViewsDataFor' . Container::camelize($data_type); + if (method_exists($this, $process_method)) { + $this->{$process_method}($table, $field_name, $field_definition, $views_field); + } + + return $views_field; + } + + /** + * Processes the views data for a language field. + * + * @param string $table + * The table the language field is added to. + * @param string $field_name + * The field name of the language field. + * @param array $views_field + * The views field data. + */ + protected function processViewsDataForLanguage($table, $field_name, FieldDefinitionInterface $field_definition, array &$views_field) { + // Apply special titles for the langcode field. + if ($field_name == 'langcode') { + if ($table == $this->entityType->getDataTable() || $table == $this->entityType->getBaseTable()) { + $views_field['title'] = $this->t('Translation language'); + } + if ($table == $this->entityType->getRevisionDataTable() || $table == $this->entityType->getRevisionTable()) { + $views_field['title'] = $this->t('Original language'); + } + } + } + + /** + * Processes the views data for an entity reference field. + * + * @param string $table + * The table the language field is added to. + * @param string $field_name + * The field name of the language field. + * @param array $views_field + * The views field data. + */ + protected function processViewsDataForEntityReference($table, $field_name, FieldDefinitionInterface $field_definition, array &$views_field) { + if ($entity_type_id = $field_definition->getItemDefinition()->getSetting('target_type')) { + $entity_type = $this->entityManager->getDefinition($entity_type_id); + if ($entity_type instanceof ContentEntityType) { + $views_field['relationship'] = [ + 'base' => $this->getViewsTableForEntityType($entity_type), + 'base field' => $entity_type->getKey('id'), + 'label' => $entity_type->getLabel(), + 'title' => $entity_type->getLabel(), + 'id' => 'standard', + ]; + $views_field['field']['id'] = 'numeric'; + $views_field['argument']['id'] = 'numeric'; + $views_field['filter']['id'] = 'numeric'; + $views_field['sort']['id'] = 'standard'; + } + else { + $views_field['field']['id'] = 'standard'; + $views_field['argument']['id'] = 'string'; + $views_field['filter']['id'] = 'string'; + $views_field['sort']['id'] = 'standard'; + } + } + + if ($field_name == $this->entityType->getKey('bundle')) { + // @todo Use the other bundle handlers, once + // https://www.drupal.org/node/2322949 is in. + $views_field['filter']['id'] = 'bundle'; + } + } + + /** + * Gets the table of an entity type to be used as base table in views. + * + * @todo Given that the base_table is pretty much useless as you often have to + * join to the data table anyway, it could make a lot of sense to start with + * the data table right from the beginning. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return string + * The name of the base table in views. + */ + protected function getViewsTableForEntityType(EntityTypeInterface $entity_type) { + return $entity_type->getBaseTable(); + } + +} diff --git a/core/modules/views/tests/Drupal/views/Tests/EntityViewsDataTest.php b/core/modules/views/tests/Drupal/views/Tests/EntityViewsDataTest.php new file mode 100644 index 0000000..4d6c8c8 --- /dev/null +++ b/core/modules/views/tests/Drupal/views/Tests/EntityViewsDataTest.php @@ -0,0 +1,626 @@ +entityStorage = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityDatabaseStorage') + ->disableOriginalConstructor() + ->getMock(); + $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface'); + + $this->baseEntityType = new EntityType([ + 'base_table' => 'entity_test', + 'id' => 'entity_test', + 'label' => 'Entity test', + 'entity_keys' => ['id' => 'id'], + ]); + + $this->translationManager = $this->getStringTranslationStub(); + $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $this->typedDataManager = $this->getMockBuilder('Drupal\Core\TypedData\TypedDataManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->viewsData = new TestEntityViewsData($this->baseEntityType, $this->entityStorage, $this->entityManager, $this->moduleHandler, $this->translationManager, $this->typedDataManager); + + $field_type_manager = $this->getMockBuilder('Drupal\Core\Field\FieldTypePluginManager') + ->disableOriginalConstructor() + ->getMock(); + $field_type_manager->expects($this->any()) + ->method('getDefaultSettings') + ->willReturn([]); + $field_type_manager->expects($this->any()) + ->method('getDefaultInstanceSettings') + ->willReturn([]); + + $container = new ContainerBuilder(); + $container->set('plugin.manager.field.field_type', $field_type_manager); + $container->set('entity.manager', $this->entityManager); + \Drupal::setContainer($container); + } + + /** + * Tests base tables. + */ + public function testBaseTables() { + $data = $this->viewsData->getViewsData(); + + $this->assertEquals('entity_test', $data['entity_test']['table']['entity type']); + $this->assertEquals('Entity test', $data['entity_test']['table']['group']); + + $this->assertEquals('id', $data['entity_test']['table']['base']['field']); + $this->assertEquals('Entity test', $data['entity_test']['table']['base']['title']); + + $this->assertFalse(isset($data['entity_test']['table']['defaults'])); + + $this->assertFalse(isset($data['entity_test_mul_property_data'])); + $this->assertFalse(isset($data['revision_table'])); + $this->assertFalse(isset($data['revision_data_table'])); + } + + + /** + * Tests data_table support. + */ + public function testDataTable() { + $entity_type = $this->baseEntityType->set('data_table', 'entity_test_mul_property_data') + ->set('id', 'entity_test_mul') + ->setKey('label', 'label'); + + $this->viewsData->setEntityType($entity_type); + + // Tests the join definition between the base and the data table. + $data = $this->viewsData->getViewsData(); + $field_views_data = $data['entity_test_mul_property_data']; + + $this->assertEquals('entity_test_mul', $data['entity_test_mul_property_data']['table']['entity type']); + $this->assertEquals('Entity test', $data['entity_test_mul_property_data']['table']['group']); + $this->assertEquals(['field' => 'label', 'table' => 'entity_test_mul_property_data'], $data['entity_test']['table']['base']['defaults']); + + // Ensure the join information is set up properly. + $this->assertCount(1, $field_views_data['table']['join']); + $this->assertEquals(['entity_test' => ['left_field' => 'id', 'field' => 'id', 'type' => 'INNER']], $field_views_data['table']['join']); + $this->assertFalse(isset($data['revision_table'])); + $this->assertFalse(isset($data['revision_data_table'])); + } + + /** + * Tests revision table support. + */ + public function testRevisionTable() { + $entity_type = $this->baseEntityType + ->set('revision_table', 'entity_test_mulrev_revision') + ->set('revision_data_table', 'entity_test_mulrev_property_revision') + ->set('id', 'entity_test_mulrev') + ->setKey('revision', 'revision_id') + ; + $this->viewsData->setEntityType($entity_type); + + $data = $this->viewsData->getViewsData(); + + $this->assertEquals('entity_test_mulrev', $data['entity_test_mulrev_revision']['table']['entity type']); + $this->assertEquals('entity_test_mulrev', $data['entity_test_mulrev_property_revision']['table']['entity type']); + $this->assertEquals('Entity test revision', $data['entity_test_mulrev_revision']['table']['group']); + + // Ensure the join information is set up properly. + // Tests the join definition between the base and the revision table. + $revision_data = $data['entity_test_mulrev_revision']; + $this->assertCount(1, $revision_data['table']['join']); + $this->assertEquals(['entity_test' => ['left_field' => 'revision_id', 'field' => 'revision_id', 'type' => 'INNER']], $revision_data['table']['join']); + $revision_data = $data['entity_test_mulrev_property_revision']; + $this->assertCount(1, $revision_data['table']['join']); + $this->assertEquals(['entity_test_mulrev_revision' => ['left_field' => 'revision_id', 'field' => 'revision_id', 'type' => 'INNER']], $revision_data['table']['join']); + $this->assertFalse(isset($data['data_table'])); + } + + /** + * Helper method to mock all store definitions. + */ + protected function setupFieldStorageDefinition() { + $id_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $id_field_storage_definition->expects($this->any()) + ->method('getSchema') + ->willReturn(IntegerItem::schema($id_field_storage_definition)); + $uuid_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $uuid_field_storage_definition->expects($this->any()) + ->method('getSchema') + ->willReturn(UuidItem::schema($uuid_field_storage_definition)); + $type_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $type_field_storage_definition->expects($this->any()) + ->method('getSchema') + ->willReturn(StringItem::schema($type_field_storage_definition)); + $langcode_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $langcode_field_storage_definition->expects($this->any()) + ->method('getSchema') + ->willReturn(LanguageItem::schema($langcode_field_storage_definition)); + $name_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $name_field_storage_definition->expects($this->any()) + ->method('getSchema') + ->willReturn(StringItem::schema($name_field_storage_definition)); + + // Setup the user_id entity reference field. + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->willReturnMap([ + ['user', TRUE, static::userEntityInfo()], + ] + ); + $user_id_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $user_id_field_storage_definition->expects($this->any()) + ->method('getSetting') + ->with('target_type') + ->willReturn('user'); + $user_id_field_storage_definition->expects($this->any()) + ->method('getSchema') + ->willReturn(EntityReferenceItem::schema($user_id_field_storage_definition)); + + $revision_id_field_storage_definition = $this->getMock('Drupal\Core\Field\FieldStorageDefinitionInterface'); + $revision_id_field_storage_definition->expects($this->any()) + ->method('getSchema') + ->willReturn(IntegerItem::schema($revision_id_field_storage_definition)); + + $this->entityManager->expects($this->any()) + ->method('getFieldStorageDefinitions') + ->willReturn([ + 'id' => $id_field_storage_definition, + 'uuid' => $uuid_field_storage_definition, + 'type' => $type_field_storage_definition, + 'langcode' => $langcode_field_storage_definition, + 'name' => $name_field_storage_definition, + 'user_id' => $user_id_field_storage_definition, + 'revision_id' => $revision_id_field_storage_definition, + ]); + } + + /** + * Tests fields on the base table. + */ + public function testBaseTableFields() { + $base_field_definitions = EntityTest::baseFieldDefinitions($this->baseEntityType); + $this->entityManager->expects($this->once()) + ->method('getBaseFieldDefinitions') + ->with('entity_test') + ->willReturn($base_field_definitions); + + // Setup the table mapping. + $table_mapping = $this->getMock('Drupal\Core\Entity\Sql\TableMappingInterface'); + $table_mapping->expects($this->any()) + ->method('getTableNames') + ->willReturn(['entity_test']); + $table_mapping->expects($this->any()) + ->method('getColumnNames') + ->willReturnMap([ + ['id', ['value' => 'id']], + ['uuid', ['value' => 'uuid']], + ['type', ['value' => 'type']], + ['langcode', ['value' => 'langcode']], + ['name', ['value' => 'name']], + ['user_id', ['target_id' => 'user_id']], + ]); + $table_mapping->expects($this->any()) + ->method('getFieldNames') + ->willReturnMap([ + ['entity_test', ['id', 'uuid', 'type', 'langcode', 'name', 'user_id']] + ]); + + $this->entityStorage->expects($this->once()) + ->method('getTableMapping') + ->willReturn($table_mapping); + + $this->setupFieldStorageDefinition(); + + $this->viewsData->setSchemaFields(['entity_test' => ['id', 'uuid', 'type', 'langcode', 'name', 'user_id']]); + $data = $this->viewsData->getViewsData(); + + $this->assertNumericField($data['entity_test']['id']); + $this->assertUuidField($data['entity_test']['uuid']); + $this->assertStringField($data['entity_test']['type']); + + $this->assertLanguageField($data['entity_test']['langcode']); + $this->assertEquals('Translation language', $data['entity_test']['langcode']['title']); + + $this->assertStringField($data['entity_test']['name']); + + $this->assertEntityReferenceField($data['entity_test']['user_id']); + $relationship = $data['entity_test']['user_id']['relationship']; + $this->assertEquals('users', $relationship['base']); + $this->assertEquals('uid', $relationship['base field']); + } + + /** + * Tests fields on the data table. + */ + public function testDataTableFields() { + $entity_type = $this->baseEntityType + ->set('data_table', 'entity_test_mul_property_data') + ->set('base_table', 'entity_test_mul') + ->set('id', 'entity_test_mul') + ->setKey('bundle', 'type') + ; + $base_field_definitions = EntityTestMul::baseFieldDefinitions($this->baseEntityType); + $base_field_definitions['type'] = BaseFieldDefinition::create('entity_reference') + ->setLabel('entity test type') + ->setSettings(array('target_type' => 'entity_test_bundle')) + ->setTranslatable(TRUE); + $entity_test_type = new ConfigEntityType(['id' => 'entity_test_bundle']); + $user_entity_type = new ContentEntityType(['id' => 'user', 'base_table' => 'users', 'entity_keys' => ['id' => 'uid']]); + $this->entityManager->expects($this->any()) + ->method('getDefinition') + ->willReturnMap([ + ['entity_test_bundle', TRUE, $entity_test_type], + ['user', TRUE, $user_entity_type], + ]); + + $this->entityManager->expects($this->once()) + ->method('getBaseFieldDefinitions') + ->with('entity_test_mul') + ->willReturn($base_field_definitions); + + $this->viewsData->setSchemaFields([ + 'entity_test_mul' => ['id', 'uuid', 'type', 'langcode'], + 'entity_test_mul_property_data' => ['id', 'langcode', 'name', 'user_id'], + ]); + + ; + $this->viewsData->setEntityType($entity_type); + + // Setup the table mapping. + $table_mapping = $this->getMock('Drupal\Core\Entity\Sql\TableMappingInterface'); + $table_mapping->expects($this->any()) + ->method('getTableNames') + ->willReturn(['entity_test_mul', 'entity_test_mul_property_data']); + $table_mapping->expects($this->any()) + ->method('getColumnNames') + ->willReturnMap([ + ['id', ['value' => 'id']], + ['uuid', ['value' => 'uuid']], + ['type', ['value' => 'type']], + ['langcode', ['value' => 'langcode']], + ['name', ['value' => 'name']], + ['user_id', ['target_id' => 'user_id']], + ]); + $table_mapping->expects($this->any()) + ->method('getFieldNames') + ->willReturnMap([ + ['entity_test_mul', ['id', 'uuid', 'type', 'langcode']], + ['entity_test_mul_property_data', ['id', 'langcode', 'name', 'user_id']], + ]); + + $this->entityStorage->expects($this->once()) + ->method('getTableMapping') + ->willReturn($table_mapping); + + $this->setupFieldStorageDefinition(); + + $data = $this->viewsData->getViewsData(); + + // Check the base fields. + $this->assertNumericField($data['entity_test_mul']['id']); + $this->assertUuidField($data['entity_test_mul']['uuid']); + + $this->assertBundleField($data['entity_test_mul']['type']); + $this->assertFalse(isset($data['entity_test_mul']['type']['relationship'])); + + $this->assertLanguageField($data['entity_test_mul']['langcode']); + // Also ensure that field_data only fields don't appear on the base table. + $this->assertFalse(isset($data['entity_test_mul']['name'])); + $this->assertFalse(isset($data['entity_test_mul']['user_id'])); + + // Check the data fields. + $this->assertNumericField($data['entity_test_mul_property_data']['id']); + + $this->assertLanguageField($data['entity_test_mul_property_data']['langcode']); + $this->assertEquals('Translation language', $data['entity_test_mul_property_data']['langcode']['title']); + + $this->assertStringField($data['entity_test_mul_property_data']['name']); + + $this->assertEntityReferenceField($data['entity_test_mul_property_data']['user_id']); + $relationship = $data['entity_test_mul_property_data']['user_id']['relationship']; + $this->assertEquals('users', $relationship['base']); + $this->assertEquals('uid', $relationship['base field']); + } + + /** + * Tests fields on the revision table. + */ + public function testRevisionTableFields() { + $entity_type = $this->baseEntityType + ->set('base_table', 'entity_test_mulrev') + ->set('revision_table', 'entity_test_mulrev_revision') + ->set('data_table', 'entity_test_mulrev_property_data') + ->set('revision_data_table', 'entity_test_mulrev_property_revision') + ->set('id', 'entity_test_mulrev'); + $base_field_definitions = EntityTestMulRev::baseFieldDefinitions($this->baseEntityType); + $this->entityManager->expects($this->once()) + ->method('getBaseFieldDefinitions') + ->with('entity_test_mulrev') + ->willReturn($base_field_definitions); + + $this->viewsData->setSchemaFields([ + 'entity_test_mulrev' => ['id', 'revision_id', 'uuid', 'type'], + 'entity_test_mulrev_revision' => ['id', 'revision_id', 'langcode'], + 'entity_test_mulrev_property_data' => ['id', 'revision_id', 'langcode', 'name', 'user_id'], + 'entity_test_mulrev_property_revision' => ['id', 'revision_id', 'langcode', 'name', 'user_id'], + ]); + + ; + $this->viewsData->setEntityType($entity_type); + + // Setup the table mapping. + $table_mapping = $this->getMock('Drupal\Core\Entity\Sql\TableMappingInterface'); + $table_mapping->expects($this->any()) + ->method('getTableNames') + ->willReturn(['entity_test_mulrev', 'entity_test_mulrev_revision', 'entity_test_mulrev_property_data', 'entity_test_mulrev_property_revision']); + $table_mapping->expects($this->any()) + ->method('getColumnNames') + ->willReturnMap([ + ['id', ['value' => 'id']], + ['uuid', ['value' => 'uuid']], + ['type', ['value' => 'type']], + ['langcode', ['value' => 'langcode']], + ['name', ['value' => 'name']], + ['user_id', ['target_id' => 'user_id']], + ['revision_id', ['value' => 'id']], + ]); + $table_mapping->expects($this->any()) + ->method('getFieldNames') + ->willReturnMap([ + ['entity_test_mulrev', ['id', 'revision_id', 'uuid', 'type']], + ['entity_test_mulrev_revision', ['id', 'revision_id', 'langcode']], + ['entity_test_mulrev_property_data', ['id', 'revision_id', 'langcode', 'name', 'user_id']], + ['entity_test_mulrev_property_revision', ['id', 'revision_id', 'langcode', 'name', 'user_id']], + ]); + + $this->entityStorage->expects($this->once()) + ->method('getTableMapping') + ->willReturn($table_mapping); + + $this->setupFieldStorageDefinition(); + + $data = $this->viewsData->getViewsData(); + + // Check the base fields. + $this->assertNumericField($data['entity_test_mulrev']['id']); + $this->assertNumericField($data['entity_test_mulrev']['revision_id']); + $this->assertUuidField($data['entity_test_mulrev']['uuid']); + $this->assertStringField($data['entity_test_mulrev']['type']); + + // Also ensure that field_data only fields don't appear on the base table. + $this->assertFalse(isset($data['entity_test_mulrev']['name'])); + $this->assertFalse(isset($data['entity_test_mulrev']['langcode'])); + $this->assertFalse(isset($data['entity_test_mulrev']['user_id'])); + + // Check the revision fields. + $this->assertNumericField($data['entity_test_mulrev_revision']['id']); + $this->assertNumericField($data['entity_test_mulrev_revision']['revision_id']); + + $this->assertLanguageField($data['entity_test_mulrev_revision']['langcode']); + $this->assertEquals('Original language', $data['entity_test_mulrev_revision']['langcode']['title']); + + // Also ensure that field_data only fields don't appear on the revision table. + $this->assertFalse(isset($data['entity_test_mulrev_revision']['name'])); + $this->assertFalse(isset($data['entity_test_mulrev_revision']['user_id'])); + + // Check the data fields. + $this->assertNumericField($data['entity_test_mulrev_property_data']['id']); + $this->assertLanguageField($data['entity_test_mulrev_property_data']['langcode']); + $this->assertStringField($data['entity_test_mulrev_property_data']['name']); + + $this->assertEntityReferenceField($data['entity_test_mulrev_property_data']['user_id']); + $relationship = $data['entity_test_mulrev_property_data']['user_id']['relationship']; + $this->assertEquals('users', $relationship['base']); + $this->assertEquals('uid', $relationship['base field']); + + // Check the property data fields. + $this->assertNumericField($data['entity_test_mulrev_property_revision']['id']); + + $this->assertLanguageField($data['entity_test_mulrev_property_revision']['langcode']); + $this->assertEquals('Original language', $data['entity_test_mulrev_property_revision']['langcode']['title']); + + $this->assertStringField($data['entity_test_mulrev_property_revision']['name']); + + $this->assertEntityReferenceField($data['entity_test_mulrev_property_revision']['user_id']); + $relationship = $data['entity_test_mulrev_property_revision']['user_id']['relationship']; + $this->assertEquals('users', $relationship['base']); + $this->assertEquals('uid', $relationship['base field']); + } + + /** + * Tests views data for a string field. + * + * @param $data + * The views data to check. + */ + protected function assertStringField($data) { + $this->assertEquals('standard', $data['field']['id']); + $this->assertEquals('string', $data['filter']['id']); + $this->assertEquals('string', $data['argument']['id']); + $this->assertEquals('standard', $data['sort']['id']); + } + + /** + * Tests views data for a UUID field. + * + * @param array $data + * The views data to check. + */ + protected function assertUuidField($data) { + // @todo Can we provide additional support for UUIDs in views? + $this->assertEquals('standard', $data['field']['id']); + $this->assertEquals('string', $data['filter']['id']); + $this->assertEquals('string', $data['argument']['id']); + $this->assertEquals('standard', $data['sort']['id']); + } + + /** + * Tests views data for a numeric field. + * + * @param array $data + * The views data to check. + */ + protected function assertNumericField($data) { + $this->assertEquals('numeric', $data['field']['id']); + $this->assertEquals('numeric', $data['filter']['id']); + $this->assertEquals('numeric', $data['argument']['id']); + $this->assertEquals('standard', $data['sort']['id']); + } + + /** + * Tests views data for a language field. + * + * @param array $data + * The views data to check. + */ + protected function assertLanguageField($data) { + $this->assertEquals('language', $data['field']['id']); + $this->assertEquals('language', $data['filter']['id']); + $this->assertEquals('language', $data['argument']['id']); + $this->assertEquals('standard', $data['sort']['id']); + } + + /** + * Tests views data for a entity reference field. + */ + protected function assertEntityReferenceField($data) { + $this->assertEquals('numeric', $data['field']['id']); + $this->assertEquals('numeric', $data['filter']['id']); + $this->assertEquals('numeric', $data['argument']['id']); + $this->assertEquals('standard', $data['sort']['id']); + } + + /** + * Tests views data for a bundle field. + */ + protected function assertBundleField($data) { + $this->assertEquals('standard', $data['field']['id']); + $this->assertEquals('bundle', $data['filter']['id']); + $this->assertEquals('string', $data['argument']['id']); + $this->assertEquals('standard', $data['sort']['id']); + } + + /** + * Returns entity info for the user entity. + * + * @return array + */ + protected static function userEntityInfo() { + return new ContentEntityType([ + 'id' => 'user', + 'class' => 'Drupal\user\Entity\User', + 'label' => 'User', + 'base_table' => 'users', + 'entity_keys' => [ + 'id' => 'uid', + 'uuid' => 'uuid', + ], + ]); + } + +} + +class TestEntityViewsData extends EntityViewsData { + + protected $schemaFields = []; + + public function setSchemaFields($fields) { + $this->schemaFields = $fields; + } + + /** + * {@inheritdoc} + */ + protected function drupalSchemaFieldsSql($table) { + return isset($this->schemaFields[$table]) ? $this->schemaFields[$table] : []; + } + + public function setEntityType(EntityTypeInterface $entity_type) { + $this->entityType = $entity_type; + } + +} + +} + +namespace { + use Drupal\Component\Utility\String; + + if (!function_exists('t')) { + function t($string, array $args = []) { + return String::format($string, $args); + } + } +} diff --git a/core/modules/views/views.api.php b/core/modules/views/views.api.php index c7e5133..5534df6 100644 --- a/core/modules/views/views.api.php +++ b/core/modules/views/views.api.php @@ -26,9 +26,10 @@ * by implementing hook_views_data_alter(). To provide views data for an * entity, create a class implementing * \Drupal\views\EntityViewsDataInterface and reference this in the - * "views_data" annotation in the entity class. See the - * @link entity_api Entity API topic @endlink for more information about - * entities. + * "views_data" annotation in the entity class. You can autogenerate big parts + * of the ingration if you extend the \Drupal\views\EntityViewsData base + * class. See the @link entity_api Entity API topic @endlink for more + * information about entities. * - Implement hooks: A few operations in Views can be influenced by hooks. * See the @link Views hooks topic @endlink for a list. * - Theming: See the @link views_templates Views templates topic @endlink diff --git a/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php index 4ff2f23..7109a62 100644 --- a/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php +++ b/core/tests/Drupal/Tests/Core/Entity/EntityTypeTest.php @@ -76,6 +76,19 @@ public function providerTestGetKeys() { } /** + * Tests the setKey() method. + * + * @covers ::setKey + */ + public function testSetKey() { + $entity_type = $this->setUpEntityType([]); + + $this->assertFalse($entity_type->getKey('id')); + $this->assertSame($entity_type, $entity_type->setKey('id', 'new_value')); + $this->assertEquals('new_value', $entity_type->getKey('id')); + } + + /** * Tests the isRevisionable() method. */ public function testIsRevisionable() {