diff --git a/config/optional/views.view.group_members.yml b/config/optional/views.view.group_members.yml index b76705f..71a7bb9 100644 --- a/config/optional/views.view.group_members.yml +++ b/config/optional/views.view.group_members.yml @@ -646,7 +646,7 @@ display: relationships: gc__user: id: gc__user - table: group_content_field_data + table: group_content__entity_id field: gc__user relationship: none group_type: group diff --git a/group.install b/group.install index c4cb24e..d05dbb1 100644 --- a/group.install +++ b/group.install @@ -12,6 +12,7 @@ use Drupal\Core\Entity\Sql\SqlContentEntityStorage; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\group\Entity\GroupContent; use Drupal\group\Entity\Storage\GroupContentStorageSchema; +use Drupal\group\Field\GroupContentReferenceDefinition; /** * Resave all GroupContent labels and remove orphaned entities. @@ -588,3 +589,136 @@ function group_update_8023() { $group_type->save(TRUE); } } + +/** + * Copy entity_id field to temporary table. + */ +function group_update_8024(&$sandbox) { + // Update hooks have been re-numbered (was 8016) so must check if run already. + $entity_id_field_exists = \Drupal::database()->schema()->fieldExists('group_content_field_data', 'entity_id'); + if ($entity_id_field_exists) { + $query = \Drupal::database() + ->select('group_content_field_data', 'g') + ->fields('g', ['id', 'entity_id']); + + // Initialize the update process, create a temporary table. + if (!isset($sandbox['total'])) { + $sandbox['total'] = $query->countQuery()->execute()->fetchField(); + $sandbox['current'] = 0; + + \Drupal::database() + ->schema() + ->createTable('group_content_entity_id_update', [ + 'fields' => [ + 'id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + 'entity_id' => [ + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + ], + 'primary key' => [ + 'id', + ], + 'description' => 'Temporary storage for group_content entity_id field migration', + ]); + } + + // We're doing one SELECT and one INSERT, so 500 rows at once should be fine. + $rows_per_operation = 500; + $query->condition('id', $sandbox['current'], '>'); + $query->range(0, $rows_per_operation); + $query->orderBy('id', 'ASC'); + + // We're not doing INSERT ... SELECT since I'm not sure how it would perform + // on highly loaded databases with much data. One such big query could even + // lock a database for a long time. + // Core is using the same approach essentially. + // @see taxonomy_update_8502() + $rows = $query->execute()->fetchAll(PDO::FETCH_ASSOC); + if ($rows) { + $insert = \Drupal::database() + ->insert('group_content_entity_id_update') + ->fields(['id', 'entity_id']); + + foreach ($rows as $row) { + $insert->values($row); + $sandbox['current'] = $row['id']; + } + + $insert->execute(); + $temp_count = \Drupal::database() + ->select('group_content_entity_id_update') + ->countQuery()->execute()->fetchField(); + $sandbox['#finished'] = ($temp_count / $sandbox['total']); + } + else { + $sandbox['#finished'] = 1; + } + + if ($sandbox['#finished'] >= 1) { + // Delete the field schema once data is copied. + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + if ($entity_definition_update_manager->getEntityType('group_content')) { + $storage_definition = $entity_definition_update_manager->getFieldStorageDefinition('entity_id', 'group_content'); + if ($storage_definition) { + $entity_definition_update_manager->uninstallFieldStorageDefinition($storage_definition); + } + } + } + } +} + +/** + * Install new group_content entity_id and entity_id_str fields schema. + */ +function group_update_8025(&$sandbox) { + $entity_id_definition = GroupContentReferenceDefinition::create('entity_reference') + ->setName('entity_id') + ->setTargetEntityTypeId('group_content') + ->setTargetBundle(NULL) + ->setLabel(t('Content')) + ->setDescription(t('The entity to add to the group.')) + ->setDisplayOptions('form', [ + 'type' => 'entity_reference_autocomplete', + 'weight' => 5, + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => '60', + 'placeholder' => '', + ], + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE) + ->setRequired(TRUE); + + \Drupal::entityDefinitionUpdateManager() + ->installFieldStorageDefinition('entity_id', 'group_content', 'group_content', $entity_id_definition); + + $entity_id_str_definition = GroupContentReferenceDefinition::create('entity_reference') + ->setName('entity_id_str') + ->setTargetEntityTypeId('group_content') + ->setTargetBundle(NULL) + ->setLabel(t('Content')) + ->setDescription(t('The entity to add to the group.')) + ->setSetting('target_type', 'menu') + ->setDisplayOptions('form', [ + 'type' => 'entity_reference_autocomplete', + 'weight' => 5, + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => '60', + 'placeholder' => '', + ], + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE) + ->setRequired(TRUE); + + \Drupal::entityDefinitionUpdateManager() + ->installFieldStorageDefinition('entity_id_str', 'group_content', 'group_content', $entity_id_str_definition); +} diff --git a/group.module b/group.module index 3b440cd..47970e3 100644 --- a/group.module +++ b/group.module @@ -11,6 +11,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; @@ -19,6 +20,7 @@ use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\group\Entity\GroupContent; +use Drupal\group\Field\GroupContentReferenceDefinition; use Drupal\group\QueryAccess\EntityQueryAlter; use Drupal\user\RoleInterface; use Drupal\views\Plugin\views\query\QueryPluginBase; @@ -359,12 +361,10 @@ function group_entity_access(EntityInterface $entity, $operation, AccountInterfa * Implements hook_entity_delete(). */ function group_entity_delete(EntityInterface $entity) { - if ($entity instanceof ContentEntityInterface) { - if ($group_contents = GroupContent::loadByEntity($entity)) { - /** @var \Drupal\group\Entity\GroupContent $group_content */ - foreach ($group_contents as $group_content) { - $group_content->delete(); - } + if ($group_contents = GroupContent::loadByEntity($entity)) { + /** @var \Drupal\group\Entity\GroupContent $group_content */ + foreach ($group_contents as $group_content) { + $group_content->delete(); } } } @@ -674,6 +674,17 @@ function group_content_entity_submit($form, FormStateInterface $form_state) { } } +/** + * Implements hook_entity_field_storage_info(). + */ +function group_entity_field_storage_info(EntityTypeInterface $entity_type) { + if ($entity_type->id() == 'group_content') { + $field_storages['entity_id'] = GroupContentReferenceDefinition::createNumericalReference(); + $field_storages['entity_id_str'] = GroupContentReferenceDefinition::createStringReference(); + return $field_storages; + } +} + /** * @defgroup group_access Group access rights * @{ diff --git a/group.post_update.php b/group.post_update.php index 7980634..0b6a74c 100644 --- a/group.post_update.php +++ b/group.post_update.php @@ -155,3 +155,79 @@ function group_post_update_make_group_revisionable(&$sandbox) { return new TranslatableMarkup('Groups have been converted to be revisionable.'); } + +/** + * Restore the data for group_content entity_id. + */ +function group_post_update_restore_entity_id_data(&$sandbox) { + $query = \Drupal::database() + ->select('group_content_entity_id_update', 'g') + ->fields('g', ['id', 'entity_id']); + + // Initialize the update process, install the field schema. + if (!isset($sandbox['total'])) { + $sandbox['total'] = $query->countQuery()->execute()->fetchField(); + $sandbox['current'] = 0; + } + + // We're now inserting new fields data which may be tricky. We're updating + // group_content entities instead of inserting fields data directly to make + // sure field data is stored correctly. + $rows_per_operation = 50; + $query->condition('id', $sandbox['current'], '>'); + $query->range(0, $rows_per_operation); + $query->orderBy('id', 'ASC'); + + $rows = $query->execute()->fetchAllKeyed(); + if ($rows) { + /** @var \Drupal\group\Entity\GroupContentInterface[] $group_contents */ + $group_contents = \Drupal::entityTypeManager() + ->getStorage('group_content') + ->loadMultiple(array_keys($rows)); + + foreach ($group_contents as $id => $group_content) { + $group_content->entity_id->target_id = $rows[$id]; + $group_content->save(); + } + + end($rows); + $sandbox['current'] = key($rows); + $moved_rows = Drupal::database() + ->select('group_content__entity_id') + ->countQuery()->execute()->fetchField(); + $sandbox['#finished'] = ($moved_rows / $sandbox['total']); + } + else { + $sandbox['#finished'] = 1; + } + + if ($sandbox['#finished'] >= 1) { + // Delete the temporary table once data is copied. + \Drupal::database()->schema()->dropTable('group_content_entity_id_update'); + } +} + +/** + * Fix existing group content views relationships. + */ +function group_post_update_fix_group_content_views_relations() { + $config_factory = \Drupal::configFactory(); + foreach ($config_factory->listAll('views.view.') as $name) { + $view = $config_factory->getEditable($name); + $changed = FALSE; + foreach ($view->get('display') as $display_id => $display) { + if (isset($display['display_options']['relationships'])) { + foreach ($display['display_options']['relationships'] as $relation_id => $relation) { + if ($relation['table'] == 'group_content_field_data') { + $trail = "display.$display_id.display_options.relationships.$relation_id.table"; + $view->set($trail, 'group_content__entity_id')->save(); + $changed = TRUE; + } + } + } + } + if ($changed) { + $view->save(); + } + } +} diff --git a/group.views.inc b/group.views.inc index 9fd0ef7..5a7e32d 100644 --- a/group.views.inc +++ b/group.views.inc @@ -5,6 +5,10 @@ * Contains Views hooks. */ +use Drupal\Core\Entity\Sql\SqlContentEntityStorage; +use Drupal\group\Entity\GroupContent; +use Drupal\group\Field\GroupContentReferenceDefinition; + /** * Implements hook_views_data_alter(). */ @@ -12,14 +16,22 @@ function group_views_data_alter(array &$data) { $entity_type_manager = \Drupal::entityTypeManager(); $entity_types = $entity_type_manager->getDefinitions(); - // Get the data table for GroupContent entities. - $data_table = $entity_types['group_content']->getDataTable(); + /** @var \Drupal\group\Entity\Storage\GroupContentStorageInterface $group_content_storage */ + $group_content_storage = $entity_type_manager->getStorage('group_content'); + if (!$group_content_storage instanceof SqlContentEntityStorage) { + // Unlike \Drupal\group\Entity\Views\GroupContentViewsData::getViewsData(), + // the entity storage is not guaranteed to use SQL backend. Skip processing + // in this case since the code below is SQL specific. + return; + } + + /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping */ + $table_mapping = $group_content_storage->getTableMapping(); /** @var \Drupal\group\Plugin\GroupContentEnablerManagerInterface $plugin_manager */ $plugin_manager = \Drupal::service('plugin.manager.group_content_enabler'); - // Add views data for all defined plugins so modules can provide default - // views even though their plugins may not have been installed yet. + /** @var \Drupal\group\Plugin\GroupContentEnablerInterface $plugin */ foreach ($plugin_manager->getAll() as $plugin) { $entity_type_id = $plugin->getEntityTypeId(); if (!isset($entity_types[$entity_type_id])) { @@ -28,24 +40,31 @@ function group_views_data_alter(array &$data) { $entity_type = $entity_types[$entity_type_id]; $entity_data_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); - // We only add one 'group_content' entry per entity type. if (isset($data[$entity_data_table]['group_content'])) { + // Skip further processing if the relationship is already defined by a + // different plugin. continue; } - $t_args = [ - '@entity_type' => $entity_type->getLabel(), - ]; + $field_name = GroupContent::getEntityFieldNameForEntityType($entity_type_id); + $field_definition = $field_name === 'entity_id' + ? GroupContentReferenceDefinition::createNumericalReference() + : GroupContentReferenceDefinition::createStringReference(); // This relationship will allow a content entity to easily map to the group // content entity that ties it to a group, optionally filtering by plugin. + $t_args = ['@entity_type' => $entity_type->getLabel()]; $data[$entity_data_table]['group_content'] = [ 'title' => t('Group content for @entity_type', $t_args), 'help' => t('Relates to the group content entities that represent the @entity_type.', $t_args), 'relationship' => [ 'group' => t('Group content'), - 'base' => $data_table, - 'base field' => 'entity_id', + 'base' => $group_content_storage->getDataTable(), + 'base field' => 'id', + 'entity_type' => 'group_content', + 'field table' => 'group_content__' . $field_name, + 'field field' => $table_mapping->getFieldColumnName($field_definition->getFieldStorageDefinition(), 'target_id'), + 'field_name' => $field_name, 'relationship field' => $entity_type->getKey('id'), 'id' => 'group_content_to_entity_reverse', 'label' => t('@entity_type group content', $t_args), diff --git a/src/Entity/Form/GroupContentForm.php b/src/Entity/Form/GroupContentForm.php index 713ea88..27918d9 100644 --- a/src/Entity/Form/GroupContentForm.php +++ b/src/Entity/Form/GroupContentForm.php @@ -4,6 +4,7 @@ namespace Drupal\group\Entity\Form; use Drupal\Core\Entity\ContentEntityForm; use Drupal\Core\Form\FormStateInterface; +use Drupal\group\Entity\GroupContent; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -51,7 +52,8 @@ class GroupContentForm extends ContentEntityForm { // Do not allow to edit the group content subject through the UI. Also hide // the field when we are on step 2 of a creation wizard. if ($this->operation !== 'add' || $form_state->get('group_wizard')) { - $form['entity_id']['#access'] = FALSE; + $entity_id_field = $this->getContentEntityField(); + $form[$entity_id_field]['#access'] = FALSE; } return $form; @@ -183,7 +185,7 @@ class GroupContentForm extends ContentEntityForm { $form_object->save($form, $form_state); // Add the newly saved entity's ID to the group content entity. - $property = $wizard_id == 'group_creator' ? 'gid' : 'entity_id'; + $property = $wizard_id == 'group_creator' ? 'gid' : $this->getContentEntityField(); $this->entity->set($property, $entity->id()); // We also clear the temp store so we can start fresh next time around. @@ -191,4 +193,15 @@ class GroupContentForm extends ContentEntityForm { $store->delete("$store_id:entity"); } + /** + * Returns the name of the group content entity reference field. + * + * @return string + * The name of the group content entity reference field. + */ + protected function getContentEntityField() { + $entity_type_id = $this->getContentPlugin()->getPluginDefinition()['entity_type_id']; + return GroupContent::getEntityFieldNameForEntityType($entity_type_id); + } + } diff --git a/src/Entity/Group.php b/src/Entity/Group.php index a615e84..eb9b4f4 100644 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -4,6 +4,10 @@ namespace Drupal\group\Entity; use Drupal\Core\Entity\EditorialContentEntityBase; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\ContentEntityBase; +use Drupal\Core\Entity\EntityChangedTrait; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityPublishedTrait; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -151,7 +155,7 @@ class Group extends EditorialContentEntityBase implements GroupInterface { /** * {@inheritdoc} */ - public function addContent(ContentEntityInterface $entity, $plugin_id, $values = []) { + public function addContent(EntityInterface $entity, $plugin_id, $values = []) { $storage = $this->groupContentStorage(); $group_content = $storage->createForEntityInGroup($entity, $this, $plugin_id, $values); $storage->save($group_content); @@ -168,7 +172,10 @@ class Group extends EditorialContentEntityBase implements GroupInterface { * {@inheritdoc} */ public function getContentByEntityId($plugin_id, $id) { - return $this->getContent($plugin_id, ['entity_id' => $id]); + $plugin = $this->getGroupType()->getContentPlugin($plugin_id); + $entity_type_id = $plugin->getPluginDefinition()['entity_type_id']; + $ref_field_name = GroupContent::getEntityFieldNameForEntityType($entity_type_id); + return $this->getContent($plugin_id, [$ref_field_name => $id]); } /** diff --git a/src/Entity/GroupContent.php b/src/Entity/GroupContent.php index 75583e5..13b19d2 100644 --- a/src/Entity/GroupContent.php +++ b/src/Entity/GroupContent.php @@ -4,13 +4,14 @@ namespace Drupal\group\Entity; use Drupal\Core\Cache\Cache; use Drupal\Core\Entity\ContentEntityBase; -use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityChangedTrait; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\group\Field\GroupContentReferenceDefinition; use Drupal\user\EntityOwnerTrait; -use Drupal\user\UserInterface; /** * Defines the Group content entity. @@ -97,7 +98,34 @@ class GroupContent extends ContentEntityBase implements GroupContentInterface { * {@inheritdoc} */ public function getEntity() { - return $this->entity_id->entity; + return $this->{$this->getEntityFieldName()}->entity; + } + + /** + * {@inheritdoc} + */ + public static function getEntityFieldNameForEntityType($entity_type_id) { + // If the entity type is fieldable and has a numeric ID field, use an entity + // reference field with an integer-based schema. + $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id); + if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) { + /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $base_fields */ + $base_fields = \Drupal::service('entity_field.manager')->getBaseFieldDefinitions($entity_type_id); + if ($base_fields[$entity_type->getKey('id')]->getType() === 'integer') { + return 'entity_id'; + } + } + + // Default to a string reference. + return 'entity_id_str'; + } + + /** + * {@inheritdoc} + */ + public function getEntityFieldName() { + $entity_type_id = $this->getContentPlugin()->getPluginDefinition()['entity_type_id']; + return static::getEntityFieldNameForEntityType($entity_type_id); } /** @@ -119,7 +147,7 @@ class GroupContent extends ContentEntityBase implements GroupContentInterface { /** * {@inheritdoc} */ - public static function loadByEntity(ContentEntityInterface $entity) { + public static function loadByEntity(EntityInterface $entity) { /** @var \Drupal\group\Entity\Storage\GroupContentStorageInterface $storage */ $storage = \Drupal::entityTypeManager()->getStorage('group_content'); return $storage->loadByEntity($entity); @@ -240,8 +268,9 @@ class GroupContent extends ContentEntityBase implements GroupContentInterface { public function getListCacheTagsToInvalidate() { $tags = parent::getListCacheTagsToInvalidate(); + $field_name = $this->getEntityFieldName(); $group_id = $this->get('gid')->target_id; - $entity_id = $this->get('entity_id')->target_id; + $entity_id = $this->get($field_name)->target_id; $plugin_id = $this->getGroupContentType()->getContentPluginId(); // A specific group gets any content, regardless of plugin used. @@ -281,24 +310,6 @@ class GroupContent extends ContentEntityBase implements GroupContentInterface { ->setReadOnly(TRUE) ->setRequired(TRUE); - // Borrowed this logic from the Comment module. - // Warning! May change in the future: https://www.drupal.org/node/2346347 - $fields['entity_id'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Content')) - ->setDescription(t('The entity to add to the group.')) - ->setDisplayOptions('form', [ - 'type' => 'entity_reference_autocomplete', - 'weight' => 5, - 'settings' => [ - 'match_operator' => 'CONTAINS', - 'size' => '60', - 'placeholder' => '', - ], - ]) - ->setDisplayConfigurable('view', TRUE) - ->setDisplayConfigurable('form', TRUE) - ->setRequired(TRUE); - $fields['label'] = BaseFieldDefinition::create('string') ->setLabel(t('Title')) ->setReadOnly(TRUE) @@ -345,37 +356,37 @@ class GroupContent extends ContentEntityBase implements GroupContentInterface { * {@inheritdoc} */ public static function bundleFieldDefinitions(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { - // Borrowed this logic from the Comment module. - // Warning! May change in the future: https://www.drupal.org/node/2346347 + /** @var \Drupal\group\Field\GroupContentReferenceDefinition[] $fields */ + $fields = []; + + // Depending on whether the entity type that can be grouped by this bundle + // has a numerical or string ID field, we add an entity reference field that + // has either an integer or varchar schema, respectively. + /** @var \Drupal\group\Entity\GroupContentTypeInterface $group_content_type */ if ($group_content_type = GroupContentType::load($bundle)) { $plugin = $group_content_type->getContentPlugin(); + if ($plugin) { - /** @var \Drupal\Core\Field\BaseFieldDefinition $original */ - $original = $base_field_definitions['entity_id']; - - // Recreated the original entity_id field so that it does not contain any - // data in its "propertyDefinitions" or "schema" properties because those - // were set based on the base field which had no clue what bundle to serve - // up until now. This is a bug in core because we can't simply unset those - // two properties, see: https://www.drupal.org/node/2346329 - $fields['entity_id'] = BaseFieldDefinition::create('entity_reference') - ->setLabel($plugin->getEntityReferenceLabel() ?: $original->getLabel()) - ->setDescription($plugin->getEntityReferenceDescription() ?: $original->getDescription()) - ->setConstraints($original->getConstraints()) - ->setDisplayOptions('view', $original->getDisplayOptions('view')) - ->setDisplayOptions('form', $original->getDisplayOptions('form')) - ->setDisplayConfigurable('view', $original->isDisplayConfigurable('view')) - ->setDisplayConfigurable('form', $original->isDisplayConfigurable('form')) - ->setRequired($original->isRequired()); - - foreach ($plugin->getEntityReferenceSettings() as $name => $setting) { - $fields['entity_id']->setSetting($name, $setting); - } + $field_name = static::getEntityFieldNameForEntityType($plugin->getEntityTypeId()); + $fields[$field_name] = $field_name === 'entity_id' + ? GroupContentReferenceDefinition::createNumericalReference() + : GroupContentReferenceDefinition::createStringReference(); + + if ($label = $plugin->getEntityReferenceLabel()) { + $fields[$field_name]->setLabel($label); + } + + if ($description = $plugin->getEntityReferenceDescription()) { + $fields[$field_name]->setDescription($description); + } - return $fields; + foreach ($plugin->getEntityReferenceSettings() as $name => $setting) { + $fields[$field_name]->setSetting($name, $setting); + } + } } - return []; + return $fields; } } diff --git a/src/Entity/GroupContentInterface.php b/src/Entity/GroupContentInterface.php index 28965aa..e2db46e 100644 --- a/src/Entity/GroupContentInterface.php +++ b/src/Entity/GroupContentInterface.php @@ -2,9 +2,10 @@ namespace Drupal\group\Entity; -use Drupal\user\EntityOwnerInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityChangedInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\user\EntityOwnerInterface; /** * Provides an interface defining a Group content entity. @@ -34,6 +35,27 @@ interface GroupContentInterface extends ContentEntityInterface, EntityOwnerInter */ public function getEntity(); + /** + * Returns the name of the entity reference field for a given entity type. + * + * @param string $entity_type_id + * The ID of the entity type to retrieve the field name for. + * + * @return string + * The name of the field referencing group content. + */ + public static function getEntityFieldNameForEntityType($entity_type_id); + + /** + * Returns the name of the group content entity reference field. + * + * @return string + * The name of the field referencing group content. + * + * @see \Drupal\group\Entity\GroupContentInterface::getEntityFieldNameForEntityType() + */ + public function getEntityFieldName(); + /** * Returns the content enabler plugin that handles the group content. * @@ -55,12 +77,12 @@ interface GroupContentInterface extends ContentEntityInterface, EntityOwnerInter /** * Loads group content entities which reference a given entity. * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * @param \Drupal\Core\Entity\EntityInterface $entity * An entity which may be within one or more groups. * * @return \Drupal\group\Entity\GroupContentInterface[] * An array of group content entities which reference the given entity. */ - public static function loadByEntity(ContentEntityInterface $entity); + public static function loadByEntity(EntityInterface $entity); } diff --git a/src/Entity/GroupInterface.php b/src/Entity/GroupInterface.php index bbfdee5..90cbf5f 100644 --- a/src/Entity/GroupInterface.php +++ b/src/Entity/GroupInterface.php @@ -2,12 +2,13 @@ namespace Drupal\group\Entity; -use Drupal\user\EntityOwnerInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityChangedInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\RevisionLogInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\user\EntityOwnerInterface; use Drupal\user\UserInterface; /** @@ -35,7 +36,7 @@ interface GroupInterface extends ContentEntityInterface, EntityOwnerInterface, E /** * Adds a content entity as a group content entity. * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * @param \Drupal\Core\Entity\EntityInterface $entity * The content entity to add to the group. * @param string $plugin_id * The ID of the content enabler plugin to add the entity with. @@ -43,7 +44,7 @@ interface GroupInterface extends ContentEntityInterface, EntityOwnerInterface, E * (optional) Extra values to add to the group content relationship. You * cannot overwrite the group ID (gid) or entity ID (entity_id). */ - public function addContent(ContentEntityInterface $entity, $plugin_id, $values = []); + public function addContent(EntityInterface $entity, $plugin_id, $values = []); /** * Retrieves all GroupContent entities for the group. diff --git a/src/Entity/Storage/GroupContentStorage.php b/src/Entity/Storage/GroupContentStorage.php index 490d3a6..130c0d0 100644 --- a/src/Entity/Storage/GroupContentStorage.php +++ b/src/Entity/Storage/GroupContentStorage.php @@ -2,9 +2,10 @@ namespace Drupal\group\Entity\Storage; -use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\Sql\SqlContentEntityStorage; +use Drupal\group\Entity\GroupContent; use Drupal\group\Entity\GroupInterface; /** @@ -25,7 +26,7 @@ class GroupContentStorage extends SqlContentEntityStorage implements GroupConten /** * {@inheritdoc} */ - public function createForEntityInGroup(ContentEntityInterface $entity, GroupInterface $group, $plugin_id, $values = []) { + public function createForEntityInGroup(EntityInterface $entity, GroupInterface $group, $plugin_id, $values = []) { // An unsaved entity cannot have any group content. if ($entity->id() === NULL) { throw new EntityStorageException("Cannot add an unsaved entity to a group."); @@ -50,11 +51,14 @@ class GroupContentStorage extends SqlContentEntityStorage implements GroupConten } } + // Retrieve the entity reference field name. + $field_name = GroupContent::getEntityFieldNameForEntityType($entity->getEntityTypeId()); + // Set the necessary keys for a valid GroupContent entity. $keys = [ 'type' => $plugin->getContentTypeConfigId(), 'gid' => $group->id(), - 'entity_id' => $entity->id(), + $field_name => $entity->id(), ]; // Return an unsaved GroupContent entity. @@ -85,7 +89,7 @@ class GroupContentStorage extends SqlContentEntityStorage implements GroupConten /** * {@inheritdoc} */ - public function loadByEntity(ContentEntityInterface $entity) { + public function loadByEntity(EntityInterface $entity) { // An unsaved entity cannot have any group content. $entity_id = $entity->id(); if ($entity_id === NULL) { @@ -100,13 +104,19 @@ class GroupContentStorage extends SqlContentEntityStorage implements GroupConten // Statically cache all group content IDs for the group content types. if (!empty($group_content_types)) { - // Use an optimized plain query to avoid the overhead of entity and SQL - // query builders. - $query = "SELECT id from {{$this->dataTable}} WHERE entity_id = :entity_id AND type IN (:types[])"; + // Retrieve the entity reference field name. + $field_name = GroupContent::getEntityFieldNameForEntityType($entity->getEntityTypeId()); + // Contruct the table name from the field name. + $table_name = 'group_content__' . $field_name; + // Add "_target_id" to the field name because that's the column where + // the id is stored. + $field_name = $field_name . '_target_id'; + + $query = "SELECT entity_id from {{$table_name}} WHERE {$field_name} = :{$field_name} AND bundle IN (:bundles[])"; $this->loadByEntityCache[$entity_type_id][$entity_id] = $this->database ->query($query, [ - ':entity_id' => $entity_id, - ':types[]' => array_keys($group_content_types), + $field_name => $entity_id, + ':bundles[]' => array_keys($group_content_types), ]) ->fetchCol(); } diff --git a/src/Entity/Storage/GroupContentStorageInterface.php b/src/Entity/Storage/GroupContentStorageInterface.php index f61001b..3f3a84b 100644 --- a/src/Entity/Storage/GroupContentStorageInterface.php +++ b/src/Entity/Storage/GroupContentStorageInterface.php @@ -2,8 +2,8 @@ namespace Drupal\group\Entity\Storage; -use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\ContentEntityStorageInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\group\Entity\GroupInterface; /** @@ -14,8 +14,8 @@ interface GroupContentStorageInterface extends ContentEntityStorageInterface { /** * Creates a GroupContent entity for placing a content entity in a group. * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * The content entity to add to the group. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to add to the group. * @param \Drupal\group\Entity\GroupInterface $group * The group to add the content entity to. * @param string $plugin_id @@ -26,7 +26,7 @@ interface GroupContentStorageInterface extends ContentEntityStorageInterface { * @return \Drupal\group\Entity\GroupContentInterface * A new GroupContent entity. */ - public function createForEntityInGroup(ContentEntityInterface $entity, GroupInterface $group, $plugin_id, $values = []); + public function createForEntityInGroup(EntityInterface $entity, GroupInterface $group, $plugin_id, $values = []); /** * Retrieves all GroupContent entities for a group. @@ -47,13 +47,13 @@ interface GroupContentStorageInterface extends ContentEntityStorageInterface { /** * Retrieves all GroupContent entities that represent a given entity. * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * @param \Drupal\Core\Entity\EntityInterface $entity * An entity which may be within one or more groups. * * @return \Drupal\group\Entity\GroupContentInterface[] * A list of GroupContent entities which refer to the given entity. */ - public function loadByEntity(ContentEntityInterface $entity); + public function loadByEntity(EntityInterface $entity); /** * Retrieves all GroupContent entities by their responsible plugin ID. diff --git a/src/Entity/Storage/GroupContentStorageSchema.php b/src/Entity/Storage/GroupContentStorageSchema.php index 46b644b..29907d9 100644 --- a/src/Entity/Storage/GroupContentStorageSchema.php +++ b/src/Entity/Storage/GroupContentStorageSchema.php @@ -18,7 +18,7 @@ class GroupContentStorageSchema extends SqlContentEntityStorageSchema { if ($data_table = $this->storage->getDataTable()) { $schema[$data_table]['indexes'] += [ - 'group_content__entity_fields' => ['type', 'entity_id'], + 'group_content__entity_fields' => ['type'], ]; } diff --git a/src/Entity/Views/GroupContentViewsData.php b/src/Entity/Views/GroupContentViewsData.php index b324b9d..c59a6cb 100644 --- a/src/Entity/Views/GroupContentViewsData.php +++ b/src/Entity/Views/GroupContentViewsData.php @@ -2,7 +2,18 @@ namespace Drupal\group\Entity\Views; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\Sql\SqlEntityStorageInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Field\FieldTypePluginManagerInterface; +use Drupal\Core\Render\Markup; +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\group\Entity\GroupContent; +use Drupal\group\Field\GroupContentReferenceDefinition; +use Drupal\group\Plugin\GroupContentEnablerManagerInterface; use Drupal\views\EntityViewsData; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -18,14 +29,53 @@ class GroupContentViewsData extends EntityViewsData { */ protected $pluginManager; + /** + * TODO. + * + * @var \Drupal\Core\Field\FieldTypePluginManagerInterface + */ + protected $fieldTypePluginManager; + + /** + * @inheritDoc + */ + public function __construct( + EntityTypeInterface $entity_type, + SqlEntityStorageInterface $storage_controller, + EntityTypeManagerInterface $entity_type_manager, + ModuleHandlerInterface $module_handler, + TranslationInterface $translation_manager, + EntityFieldManagerInterface $entity_field_manager, + GroupContentEnablerManagerInterface $content_enabler_manager, + FieldTypePluginManagerInterface $field_type_plugin_manager + ) { + parent::__construct( + $entity_type, + $storage_controller, + $entity_type_manager, + $module_handler, + $translation_manager, + $entity_field_manager + ); + + $this->pluginManager = $content_enabler_manager; + $this->fieldTypePluginManager = $field_type_plugin_manager; + } + /** * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { - /** @var static $views_data */ - $views_data = parent::createInstance($container, $entity_type); - $views_data->pluginManager = $container->get('plugin.manager.group_content_enabler'); - return $views_data; + return new static( + $entity_type, + $container->get('entity_type.manager')->getStorage($entity_type->id()), + $container->get('entity_type.manager'), + $container->get('module_handler'), + $container->get('string_translation'), + $container->get('entity_field.manager'), + $container->get('plugin.manager.group_content_enabler'), + $container->get('plugin.manager.field.field_type') + ); } /** @@ -41,14 +91,7 @@ class GroupContentViewsData extends EntityViewsData { 'numeric' => TRUE, ]; - // Get the data table for GroupContent entities. - $data_table = $this->entityType->getDataTable(); - - // Unset the 'entity_id' field relationship as we want a more powerful one. - // @todo Eventually, we may want to replace all of 'entity_id'. - unset($data[$data_table]['entity_id']['relationship']); - - /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ + $table_mapping = $this->storage->getTableMapping(); $entity_types = $this->entityTypeManager->getDefinitions(); // Add views data for all defined plugins so modules can provide default @@ -62,27 +105,41 @@ class GroupContentViewsData extends EntityViewsData { $entity_data_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); // Create a unique field name for this views field. - $field_name = 'gc__' . $entity_type_id; + // This is a Views field, not entity field! + $views_field_name = 'gc__' . $entity_type_id; + $field_name = GroupContent::getEntityFieldNameForEntityType($entity_type_id); + $field_table = $this->getFieldTableName($field_name); - // We only add one 'group_content' relationship per entity type. - if (isset($data[$entity_data_table][$field_name])) { + if (isset($data[$field_table][$views_field_name])) { + // Skip further processing if the relationship is already defined by a + // different plugin. continue; } - $t_args = [ - '@entity_type' => $entity_type->getLabel(), - ]; + $field_definition = $field_name === 'entity_id' + ? GroupContentReferenceDefinition::createNumericalReference() + : GroupContentReferenceDefinition::createStringReference(); + + // Avoid reload field default views data to preserve existing relations. + if (!isset($data[$field_table])) { + // Fill in default Views data for a field, just like Views does + // for fields defined via config entities. + $default_data = $this->defaultFieldViewsData($field_definition); + $data[$field_table] = $default_data[$field_table]; + } - // This relationship will allow a group content entity to easily map to a - // content entity that it ties to a group, optionally filtering by plugin. - $data[$data_table][$field_name] = [ + // This relationship will allow a content entity to easily map to the + // group content entity that ties it to a group, optionally filtering by + // plugin. + $t_args = ['@entity_type' => $entity_type->getLabel()]; + $data[$field_table][$views_field_name] = [ 'title' => $this->t('@entity_type from group content', $t_args), 'help' => $this->t('Relates to the @entity_type entity the group content represents.', $t_args), 'relationship' => [ 'group' => $entity_type->getLabel(), 'base' => $entity_data_table, 'base field' => $entity_type->getKey('id'), - 'relationship field' => 'entity_id', + 'relationship field' => $table_mapping->getFieldColumnName($field_definition->getFieldStorageDefinition(), 'target_id'), 'id' => 'group_content_to_entity', 'label' => $this->t('Group content @entity_type', $t_args), 'target_entity_type' => $entity_type_id, @@ -90,7 +147,342 @@ class GroupContentViewsData extends EntityViewsData { ]; } + // Add the entity type metadata to each table generated. + $entity_type_id = $this->entityType->id(); + array_walk($data, function (&$table_data) use ($entity_type_id) { + $table_data['table']['entity type'] = $entity_type_id; + $table_data['table']['entity revision'] = FALSE; + }); + + return $data; + } + + /** + * Defaults Views data implementation for the group content field. + * + * This method is mostly a copy-paste of views_field_default_views_data(). + * Unfortunately, we can't re-use is since it only accepts + * FieldStorageConfigInterface as an argument. + * + * Group content does not support revisions so everything related to + * revisions is stripped for simplicity. + * The group content entity reference field is not translatable so everything + * related to translations is stripped as well. + * + * @see views_field_default_views_data() + * + * @param \Drupal\group\Field\GroupContentReferenceDefinition $field_storage + * Group content reference field definition. + * + * @return array + * The default views data for the field. + * + * @TODO Review this code once https://www.drupal.org/node/3016026 lands. + */ + protected function defaultFieldViewsData(GroupContentReferenceDefinition $field_storage) { + $data = []; + + // Check the field type is available. + if (!$this->fieldTypePluginManager->hasDefinition($field_storage->getType())) { + return $data; + } + + $field_name = $field_storage->getName(); + $field_columns = $field_storage->getColumns(); + + // Grab information about the entity type tables. + // We need to join to both the base table and the data table, if available. + // Check whether the entity type storage is supported. + $storage = $this->entityTypeManager->getStorage($field_storage->getTargetEntityTypeId()); + if (!$storage) { + return $data; + } + + $entity_type_id = $field_storage->getTargetEntityTypeId(); + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + if (!$base_table = $entity_type->getBaseTable()) { + // We cannot do anything if for some reason there is no base table. + return $data; + } + // Some entities may not have a data table. + $data_table = $entity_type->getDataTable(); + + // Description of the field tables. + /** @var \Drupal\Core\Entity\Sql\TableMappingInterface $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $field_tables = [ + EntityStorageInterface::FIELD_LOAD_CURRENT => [ + 'table' => $table_mapping->getDedicatedDataTableName($field_storage), + 'alias' => "{$entity_type_id}__{$field_name}", + ], + ]; + + // Build the relationships between the field table and the entity tables. + $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_CURRENT]['alias']; + if ($data_table) { + // Tell Views how to join to the base table, via the data table. + $data[$table_alias]['table']['join'][$data_table] = [ + 'table' => $table_mapping->getDedicatedDataTableName($field_storage), + 'left_field' => $entity_type->getKey('id'), + 'field' => 'entity_id', + 'extra' => [ + ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], + ], + ]; + } + else { + // If there is no data table, just join directly. + $data[$table_alias]['table']['join'][$base_table] = [ + 'table' => $table_mapping->getDedicatedDataTableName($field_storage), + 'left_field' => $entity_type->getKey('id'), + 'field' => 'entity_id', + 'extra' => [ + ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE], + ], + ]; + } + + $group_name = $entity_type->getLabel(); + + // Build the list of additional fields to add to queries. + $add_fields = ['delta', 'langcode', 'bundle']; + foreach (array_keys($field_columns) as $column) { + $add_fields[] = $table_mapping->getFieldColumnName($field_storage, $column); + } + // Determine the label to use for the field. We don't have a label available + // at the field level, so we just go through all fields and take the one + // which is used the most frequently. + [$label, $all_labels] = views_entity_field_label($entity_type_id, $field_name); + + // Expose data for the field as a whole. + foreach ($field_tables as $type => $table_info) { + $table = $table_info['table']; + $table_alias = $table_info['alias']; + + if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) { + $group = $group_name; + $field_alias = $field_name; + } + else { + $group = t('@group (historical data)', ['@group' => $group_name]); + $field_alias = $field_name . '-revision_id'; + } + + $data[$table_alias][$field_alias] = [ + 'group' => $group, + 'title' => $label, + 'title short' => $label, + ]; + + // Go through and create a list of aliases for all possible combinations + // of entity type + name. + $aliases = []; + $also_known = []; + foreach ($all_labels as $label_name => $true) { + if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT && $label != $label_name) { + $aliases[] = [ + 'base' => $base_table, + 'group' => $group_name, + 'title' => $label_name, + 'help' => t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $label]), + ]; + $also_known[] = t('@group: @field', ['@group' => $group_name, '@field' => $label_name]); + } + } + + if ($aliases) { + $data[$table_alias][$field_alias]['aliases'] = $aliases; + // The $also_known variable contains markup that is HTML escaped and + // that loses safeness when imploded. The help text is used in + // #description and therefore XSS admin filtered by default. Escaped + // HTML is not altered by XSS filtering, therefore it is safe to just + // concatenate the strings. Afterwards we mark the entire string as + // safe, so it won't be escaped, no matter where it is used. + // Considering the dual use of this help data (both as metadata and as + // help text), other patterns such as use of #markup would not be + // correct here. + $data[$table_alias][$field_alias]['help'] = Markup::create($data[$table_alias][$field_alias]['help'] . ' ' . t('Also known as:') . ' ' . implode(', ', $also_known)); + } + + $keys = array_keys($field_columns); + $real_field = reset($keys); + $data[$table_alias][$field_alias]['field'] = [ + 'table' => $table, + 'id' => 'field', + 'field_name' => $field_name, + 'entity_type' => $entity_type_id, + // Provide a real field for group by. + 'real field' => $field_alias . '_' . $real_field, + 'additional fields' => $add_fields, + // Default the element type to div, let the UI change it if necessary. + 'element type' => 'div', + 'is revision' => $type == EntityStorageInterface::FIELD_LOAD_REVISION, + ]; + } + + // Expose data for each field property individually. + foreach ($field_columns as $column => $attributes) { + $allow_sort = TRUE; + + // Identify likely filters and arguments for each column based on field + // type. + switch ($attributes['type']) { + case 'int': + case 'mediumint': + case 'tinyint': + case 'bigint': + case 'serial': + case 'numeric': + case 'float': + $filter = 'numeric'; + $argument = 'numeric'; + $sort = 'standard'; + if ($field_storage->getType() == 'boolean') { + $filter = 'boolean'; + } + break; + + case 'text': + case 'blob': + // It does not make sense to sort by blob or text. + $allow_sort = FALSE; + + default: + $filter = 'string'; + $argument = 'string'; + $sort = 'standard'; + break; + } + + if (count($field_columns) == 1 || $column == 'value') { + $title = t('@label (@name)', ['@label' => $label, '@name' => $field_name]); + $title_short = $label; + } + else { + $title = t('@label (@name:@column)', [ + '@label' => $label, + '@name' => $field_name, + '@column' => $column, + ]); + $title_short = t('@label:@column', ['@label' => $label, '@column' => $column]); + } + + // Expose data for the property. + foreach ($field_tables as $type => $table_info) { + $table = $table_info['table']; + $table_alias = $table_info['alias']; + + if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) { + $group = $group_name; + } + else { + $group = t('@group (historical data)', ['@group' => $group_name]); + } + $column_real_name = $table_mapping->getFieldColumnName($field_storage, $column); + + // Load all the fields from the table by default. + $additional_fields = $table_mapping->getAllColumns($table); + + $data[$table_alias][$column_real_name] = [ + 'group' => $group, + 'title' => $title, + 'title short' => $title_short, + ]; + + // Go through and create a list of aliases for all possible + // combinations of entity type + name. + $aliases = []; + $also_known = []; + foreach ($all_labels as $label_name => $true) { + if ($label != $label_name) { + if (count($field_columns) == 1 || $column == 'value') { + $alias_title = t('@label (@name)', ['@label' => $label_name, '@name' => $field_name]); + } + else { + $alias_title = t('@label (@name:@column)', [ + '@label' => $label_name, + '@name' => $field_name, + '@column' => $column, + ]); + } + $aliases[] = [ + 'group' => $group_name, + 'title' => $alias_title, + 'help' => t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $title]), + ]; + $also_known[] = t('@group: @field', ['@group' => $group_name, '@field' => $title]); + } + } + if ($aliases) { + $data[$table_alias][$column_real_name]['aliases'] = $aliases; + // The $also_known variable contains markup that is HTML escaped and + // that loses safeness when imploded. The help text is used in + // #description and therefore XSS admin filtered by default. Escaped + // HTML is not altered by XSS filtering, therefore it is safe to just + // concatenate the strings. Afterwards we mark the entire string as + // safe, so it won't be escaped, no matter where it is used. + // Considering the dual use of this help data (both as metadata and as + // help text), other patterns such as use of #markup would not be + // correct here. + $data[$table_alias][$column_real_name]['help'] = Markup::create($data[$table_alias][$column_real_name]['help'] . ' ' . t('Also known as:') . ' ' . implode(', ', $also_known)); + } + + $data[$table_alias][$column_real_name]['argument'] = [ + 'field' => $column_real_name, + 'table' => $table, + 'id' => $argument, + 'additional fields' => $additional_fields, + 'field_name' => $field_name, + 'entity_type' => $entity_type_id, + 'empty field name' => t('- No value -'), + ]; + $data[$table_alias][$column_real_name]['filter'] = [ + 'field' => $column_real_name, + 'table' => $table, + 'id' => $filter, + 'additional fields' => $additional_fields, + 'field_name' => $field_name, + 'entity_type' => $entity_type_id, + 'allow empty' => TRUE, + ]; + if (!empty($allow_sort)) { + $data[$table_alias][$column_real_name]['sort'] = [ + 'field' => $column_real_name, + 'table' => $table, + 'id' => $sort, + 'additional fields' => $additional_fields, + 'field_name' => $field_name, + 'entity_type' => $entity_type_id, + ]; + } + + // Set click sortable if there is a field definition. + if (isset($data[$table_alias][$field_name]['field'])) { + $data[$table_alias][$field_name]['field']['click sortable'] = $allow_sort; + } + } + } + return $data; } + /** + * Returns the SQL table name for a given field. + * + * @param string $field_name + * The field name. + * + * @return string + * The field table name. + */ + protected function getFieldTableName($field_name) { + // The getTableMapping() method assumes field storage definitions are + // static. This is causing an error while installing the gnode module. + // As a workaround, we're field storage definitions from the entity manager + // to bypass cache. + // @see https://www.drupal.org/project/drupal/issues/3016059 + $table_mapping = $this->storage->getTableMapping($this->getFieldStorageDefinitions('group_content')); + return $table_mapping->getFieldTableName($field_name); + } + } diff --git a/src/Field/GroupContentReferenceDefinition.php b/src/Field/GroupContentReferenceDefinition.php new file mode 100644 index 0000000..e5e33e7 --- /dev/null +++ b/src/Field/GroupContentReferenceDefinition.php @@ -0,0 +1,99 @@ +setTargetEntityTypeId('group_content') + ->setTargetBundle(NULL) + ->setLabel(t('Content')) + ->setDescription(t('The entity to add to the group.')) + ->setDisplayOptions('form', [ + 'type' => 'entity_reference_autocomplete', + 'weight' => 5, + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => '60', + 'placeholder' => '', + ], + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE) + ->setRequired(TRUE); + } + + /** + * Creates a new group content reference field definition for numerical IDs. + * + * @return static + * A new group content reference field definition object. + */ + public static function createNumericalReference() { + return static::create('entity_reference')->setName('entity_id'); + } + + /** + * Creates a new group content reference field definition for string IDs. + * + * @return static + * A new group content reference field definition object. + */ + public static function createStringReference() { + return static::create('entity_reference') + ->setName('entity_id_str') + // This can be replaced by the right entity type when defining the group + // content bundle fields, but needs to be set to a string ID entity type + // for \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem to + // do the hard work for us. + ->setSetting('target_type', 'menu'); + } + + /** + * {@inheritdoc} + */ + public function isBaseField() { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getSchema() { + if (!isset($this->schema)) { + parent::getSchema(); + + // Entity reference items usually determine the schema column length by + // the target entity type. If it's a bundle entity type, the length is far + // shorter (32) than for other entity types (255). We can't know for sure + // what the target entity type will be, so we forcibly set it to 255. + // + // This is essentially also achieved by setting the target_type to menu in + // ::createStringReference(), but we can't rely on the fact that the menu + // entity type does not act as a bundle as that behavior might change in + // future Drupal releases. + if ($this->schema['columns']['target_id']['type'] == 'varchar_ascii') { + $this->schema['columns']['target_id']['length'] = 255; + } + } + + return $this->schema; + } + +} diff --git a/src/Plugin/Validation/Constraint/GroupContentCardinalityValidator.php b/src/Plugin/Validation/Constraint/GroupContentCardinalityValidator.php index 0994caf..e0c4a62 100644 --- a/src/Plugin/Validation/Constraint/GroupContentCardinalityValidator.php +++ b/src/Plugin/Validation/Constraint/GroupContentCardinalityValidator.php @@ -4,6 +4,8 @@ namespace Drupal\group\Plugin\Validation\Constraint; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\group\Entity\GroupContent; +use Drupal\group\Plugin\GroupContentEnablerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; @@ -78,13 +80,17 @@ class GroupContentCardinalityValidator extends ConstraintValidator implements Co return; } - // Get the entity_id field label for error messages. - $field_name = $group_content->getFieldDefinition('entity_id')->getLabel(); + // Get the content reference field name. May be either entity_id or + // entity_id_str depending on the referenced content type. + $ref_field = $this->getContentReferenceField($plugin); + + // Get the content reference field label for error messages. + $field_name = $group_content->getFieldDefinition($ref_field)->getLabel(); // Enforce the group cardinality if it's not set to unlimited. if ($group_cardinality > 0) { // Get the group content entities for this piece of content. - $properties = ['type' => $plugin->getContentTypeConfigId(), 'entity_id' => $entity->id()]; + $properties = ['type' => $plugin->getContentTypeConfigId(), $ref_field => $entity->id()]; $group_instances = $this->entityTypeManager ->getStorage('group_content') ->loadByProperties($properties); @@ -108,7 +114,7 @@ class GroupContentCardinalityValidator extends ConstraintValidator implements Co // We manually flag the entity reference field as the source of the // violation so form API will add a visual indicator of where the // validation failed. - ->atPath('entity_id.0') + ->atPath($ref_field . '.0') ->addViolation(); } } @@ -139,10 +145,24 @@ class GroupContentCardinalityValidator extends ConstraintValidator implements Co // We manually flag the entity reference field as the source of the // violation so form API will add a visual indicator of where the // validation failed. - ->atPath('entity_id.0') + ->atPath($ref_field . '.0') ->addViolation(); } } } + /** + * Returns the name of the group content entity reference field. + * + * @param \Drupal\group\Plugin\GroupContentEnablerInterface $plugin + * Group content enable plugin. + * + * @return string + * The name of the content reference field. + */ + protected function getContentReferenceField(GroupContentEnablerInterface $plugin) { + $entity_type_id = $plugin->getPluginDefinition()['entity_type_id']; + return GroupContent::getEntityFieldNameForEntityType($entity_type_id); + } + } diff --git a/src/Plugin/views/relationship/GroupContentToEntity.php b/src/Plugin/views/relationship/GroupContentToEntity.php index 1cfeec9..2ab8c51 100644 --- a/src/Plugin/views/relationship/GroupContentToEntity.php +++ b/src/Plugin/views/relationship/GroupContentToEntity.php @@ -2,13 +2,6 @@ namespace Drupal\group\Plugin\views\relationship; -use Drupal\Core\Form\FormStateInterface; -use Drupal\group\Entity\GroupContentType; -use Drupal\group\Plugin\GroupContentEnablerManagerInterface; -use Drupal\views\Plugin\views\relationship\RelationshipPluginBase; -use Drupal\views\Plugin\ViewsHandlerManager; -use Symfony\Component\DependencyInjection\ContainerInterface; - /** * A relationship handler for group content entity references. * @@ -31,8 +24,47 @@ class GroupContentToEntity extends GroupContentToEntityBase { /** * {@inheritdoc} */ - protected function getJoinFieldType() { - return 'left_field'; + public function query() { + $this->ensureMyTable(); + + // Build the join definition. + $def = $this->definition; + $def['table'] = $this->definition['base']; + $def['field'] = $this->definition['base field']; + $def['left_table'] = $this->tableAlias; + $def['left_field'] = $this->realField; + $def['adjusted'] = TRUE; + + // Change the join to INNER if the relationship is required. + if (!empty($this->options['required'])) { + $def['type'] = 'INNER'; + } + + // If there were extra join conditions added in the definition, use them. + if (!empty($this->definition['extra'])) { + $def['extra'] = $this->definition['extra']; + } + + // Then add our own join condition, namely the group content type IDs. + $def['extra'][] = [ + 'left_field' => 'bundle', + 'value' => $this->getGroupContentTypesValue(), + ]; + + // Use the standard join plugin unless instructed otherwise. + $join_id = !empty($def['join_id']) ? $def['join_id'] : 'standard'; + $join = $this->joinManager->createInstance($join_id, $def); + + // Add the join using a more verbose alias. + $alias = $def['table'] . '_' . $this->table; + $this->alias = $this->query->addRelationship($alias, $join, $this->definition['base'], $this->relationship); + + // Add access tags if the base table provides it. + $table_data = $this->viewsData->get($def['table']); + if (empty($this->query->options['disable_sql_rewrite']) && isset($table_data['table']['base']['access query tag'])) { + $access_tag = $table_data['table']['base']['access query tag']; + $this->query->addTag($access_tag); + } } } diff --git a/src/Plugin/views/relationship/GroupContentToEntityBase.php b/src/Plugin/views/relationship/GroupContentToEntityBase.php index 63dea76..2cd1e16 100644 --- a/src/Plugin/views/relationship/GroupContentToEntityBase.php +++ b/src/Plugin/views/relationship/GroupContentToEntityBase.php @@ -31,6 +31,12 @@ abstract class GroupContentToEntityBase extends RelationshipPluginBase { /** * Constructs an GroupContentToEntityBase object. * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. * @param \Drupal\views\Plugin\ViewsHandlerManager $join_manager * The views plugin join manager. * @param \Drupal\group\Plugin\GroupContentEnablerManagerInterface $plugin_manager @@ -64,17 +70,7 @@ abstract class GroupContentToEntityBase extends RelationshipPluginBase { * @return string * The target entity type ID. */ - protected abstract function getTargetEntityType(); - - /** - * Retrieves type of join field to use. - * - * Can be either 'field' or 'left_field'. - * - * @return string - * The type of join field to use. - */ - protected abstract function getJoinFieldType(); + abstract protected function getTargetEntityType(); /** * {@inheritdoc} @@ -110,61 +106,6 @@ abstract class GroupContentToEntityBase extends RelationshipPluginBase { ]; } - /** - * {@inheritdoc} - */ - public function query() { - $this->ensureMyTable(); - - // Build the join definition. - $def = $this->definition; - $def['table'] = $this->definition['base']; - $def['field'] = $this->definition['base field']; - $def['left_table'] = $this->tableAlias; - $def['left_field'] = $this->realField; - $def['adjusted'] = TRUE; - - // Change the join to INNER if the relationship is required. - if (!empty($this->options['required'])) { - $def['type'] = 'INNER'; - } - - // If there were extra join conditions added in the definition, use them. - if (!empty($this->definition['extra'])) { - $def['extra'] = $this->definition['extra']; - } - - // We can't run an IN-query on an empty array. So if there are no group - // content types yet, we need to make sure the JOIN does not return any GCT - // that does not serve the entity type that was configured for this handler - // instance. - $group_content_type_ids = $this->getGroupContentTypeIds(); - if (empty($group_content_type_ids)) { - $group_content_type_ids = ['***']; - } - - // Then add our own join condition, namely the group content type IDs. - $def['extra'][] = [ - $this->getJoinFieldType() => 'type', - 'value' => $group_content_type_ids, - ]; - - // Use the standard join plugin unless instructed otherwise. - $join_id = !empty($def['join_id']) ? $def['join_id'] : 'standard'; - $join = $this->joinManager->createInstance($join_id, $def); - - // Add the join using a more verbose alias. - $alias = $def['table'] . '_' . $this->table; - $this->alias = $this->query->addRelationship($alias, $join, $this->definition['base'], $this->relationship); - - // Add access tags if the base table provides it. - $table_data = $this->viewsData->get($def['table']); - if (empty($this->query->options['disable_sql_rewrite']) && isset($table_data['table']['base']['access query tag'])) { - $access_tag = $table_data['table']['base']['access query tag']; - $this->query->addTag($access_tag); - } - } - /** * Returns the group content types this relationship should filter on. * @@ -193,4 +134,23 @@ abstract class GroupContentToEntityBase extends RelationshipPluginBase { return $plugin_ids ? $group_content_type_ids : array_keys(GroupContentType::loadByEntityTypeId($this->getTargetEntityType())); } + /** + * Returns the list of group content types for a query. + * + * We can't run an IN-query on an empty array. So if there are no group + * content types yet, we need to make sure the JOIN does not return any GCT + * that does not serve the entity type that was configured for this handler + * instance. + * + * @return array + * The list of group content types to be used as extra JOIN condition. + */ + protected function getGroupContentTypesValue() { + $group_content_type_ids = $this->getGroupContentTypeIds(); + if (empty($group_content_type_ids)) { + $group_content_type_ids = ['***']; + } + return $group_content_type_ids; + } + } diff --git a/src/Plugin/views/relationship/GroupContentToEntityReverse.php b/src/Plugin/views/relationship/GroupContentToEntityReverse.php index dfad5b9..31d5f44 100644 --- a/src/Plugin/views/relationship/GroupContentToEntityReverse.php +++ b/src/Plugin/views/relationship/GroupContentToEntityReverse.php @@ -2,16 +2,15 @@ namespace Drupal\group\Plugin\views\relationship; -use Drupal\Core\Form\FormStateInterface; -use Drupal\group\Entity\GroupContentType; -use Drupal\group\Plugin\GroupContentEnablerManagerInterface; -use Drupal\views\Plugin\views\relationship\RelationshipPluginBase; -use Drupal\views\Plugin\ViewsHandlerManager; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\views\Views; /** * A relationship handler which reverses group content entity references. * + * This handler is mostly a copy-paste of core "entity_reverse" relationship + * handler. We are not extending the EntityReverse class because we'd have to + * replace the query() method with a 90% copy of the method so it's worthless. + * * @ingroup views_relationship_handlers * * @ViewsRelationship("group_content_to_entity_reverse") @@ -28,8 +27,73 @@ class GroupContentToEntityReverse extends GroupContentToEntityBase { /** * {@inheritdoc} */ - protected function getJoinFieldType() { - return 'field'; + public function query() { + $this->ensureMyTable(); + // First, relate our base table to the current base table to the + // field, using the base table's id field to the field's column. + $views_data = Views::viewsData()->get($this->table); + $left_field = $views_data['table']['base']['field']; + + $first = [ + 'left_table' => $this->tableAlias, + 'left_field' => $left_field, + 'table' => $this->definition['field table'], + 'field' => $this->definition['field field'], + 'adjusted' => TRUE, + ]; + if (!empty($this->options['required'])) { + $first['type'] = 'INNER'; + } + + if (!empty($this->definition['join_extra'])) { + $first['extra'] = $this->definition['join_extra']; + } + + // Add our own join condition, namely the group content type IDs. + // This is the only thing which differs this handler from core + // "entity_reverse". + $first['extra'][] = [ + 'field' => 'bundle', + 'value' => $this->getGroupContentTypesValue(), + ]; + + if (!empty($def['join_id'])) { + $id = $def['join_id']; + } + else { + $id = 'standard'; + } + $first_join = $this->joinManager->createInstance($id, $first); + + $this->first_alias = $this->query->addTable($this->definition['field table'], $this->relationship, $first_join); + + // Second, relate the field table to the entity specified using + // the entity id on the field table and the entity's id field. + $second = [ + 'left_table' => $this->first_alias, + 'left_field' => 'entity_id', + 'table' => $this->definition['base'], + 'field' => $this->definition['base field'], + 'adjusted' => TRUE, + ]; + + if (!empty($this->options['required'])) { + $second['type'] = 'INNER'; + } + + if (!empty($def['join_id'])) { + $id = $def['join_id']; + } + else { + $id = 'standard'; + } + $second_join = $this->joinManager->createInstance($id, $second); + $second_join->adjusted = TRUE; + + // Use a short alias. + $alias = $this->definition['field_name'] . '_' . $this->table; + + $this->alias = $this->query->addRelationship($alias, $second_join, $this->definition['base'], $this->relationship); } } diff --git a/src/QueryAccess/EntityQueryAlter.php b/src/QueryAccess/EntityQueryAlter.php index a0c060b..c9763d0 100644 --- a/src/QueryAccess/EntityQueryAlter.php +++ b/src/QueryAccess/EntityQueryAlter.php @@ -228,10 +228,17 @@ class EntityQueryAlter implements ContainerInjectionInterface { break; } } + $query->leftJoin( + 'group_content__entity_id', + 'gcfdss', + "$base_table.$id_key=gcfdss.entity_id_target_id AND gcfdss.bundle IN (:group_content_type_ids_in_use[])", + [':group_content_type_ids_in_use[]' => $group_content_type_ids_in_use] + ); + $query->leftJoin( 'group_content_field_data', 'gcfd', - "$base_table.$id_key=gcfd.entity_id AND gcfd.type IN (:group_content_type_ids_in_use[])", + "gcfdss.entity_id=gcfd.id AND gcfd.type IN (:group_content_type_ids_in_use[])", [':group_content_type_ids_in_use[]' => $group_content_type_ids_in_use] ); @@ -331,7 +338,7 @@ class EntityQueryAlter implements ContainerInjectionInterface { // If no group type or group gave access, we deny access altogether. if (empty($allowed_any_ids) && empty($allowed_own_ids) && empty($allowed_any_by_status_ids) && empty($allowed_own_by_status_ids)) { - $query->isNull('gcfd.entity_id'); + $query->isNull('gcfd.id'); return; } @@ -340,7 +347,7 @@ class EntityQueryAlter implements ContainerInjectionInterface { // access checks apply. $query->condition( $query->orConditionGroup() - ->isNull('gcfd.entity_id') + ->isNull('gcfd.id') ->condition($group_conditions = $query->orConditionGroup()) ); diff --git a/tests/modules/group_test_content/config/schema/group_test_content.schema.yml b/tests/modules/group_test_content/config/schema/group_test_content.schema.yml new file mode 100644 index 0000000..72b1557 --- /dev/null +++ b/tests/modules/group_test_content/config/schema/group_test_content.schema.yml @@ -0,0 +1,9 @@ +# Schema for the configuration files of the Configuration Test module. + +group_test_content.group_test_config_entity_string.*: + type: config_entity + label: 'Config test dynamic settings' + mapping: + id: + type: string + label: 'ID' diff --git a/tests/modules/group_test_content/group_test_content.info.yml b/tests/modules/group_test_content/group_test_content.info.yml new file mode 100644 index 0000000..eac3bad --- /dev/null +++ b/tests/modules/group_test_content/group_test_content.info.yml @@ -0,0 +1,8 @@ +name: 'Group test content' +description: 'Provides entity types and plugins for Group content tests.' +package: 'Testing' +type: 'module' +version: VERSION +core_version_requirement: ^8.8 || ^9 +dependencies: + - 'group' diff --git a/tests/modules/group_test_content/src/Entity/IntegerContent.php b/tests/modules/group_test_content/src/Entity/IntegerContent.php new file mode 100644 index 0000000..79a178d --- /dev/null +++ b/tests/modules/group_test_content/src/Entity/IntegerContent.php @@ -0,0 +1,23 @@ +getKey('id')] = BaseFieldDefinition::create('string') + ->setLabel(new TranslatableMarkup('String ID')) + ->setSetting('is_ascii', TRUE); + + return $fields; + } + +} diff --git a/tests/modules/group_test_content/src/Plugin/GroupContentEnabler/IntegerContentEntityAsContent.php b/tests/modules/group_test_content/src/Plugin/GroupContentEnabler/IntegerContentEntityAsContent.php new file mode 100644 index 0000000..578dafb --- /dev/null +++ b/tests/modules/group_test_content/src/Plugin/GroupContentEnabler/IntegerContentEntityAsContent.php @@ -0,0 +1,21 @@ +entityTypeManager->getStorage('group_content_type'); $storage->createFromPlugin($group_type, 'user_as_content')->save(); $storage->createFromPlugin($group_type, 'group_as_content')->save(); + + $this->installConfig(['group_test_content']); + $storage->createFromPlugin($group_type, 'integer_content_entity_as_content')->save(); + $storage->createFromPlugin($group_type, 'string_config_entity_as_content')->save(); + $storage->createFromPlugin($group_type, 'string_content_entity_as_content')->save(); + + $this->installEntitySchema('group_test_content_entity_int'); + $this->installEntitySchema('group_test_config_entity_string'); + $this->installEntitySchema('group_test_content_entity_string'); } /** @@ -209,4 +220,110 @@ class GroupContentStorageTest extends GroupKernelTestBase { $this->assertCount(1, $this->storage->loadByContentPluginId('group_membership'), 'Managed to load the group creator membership by plugin ID.'); } + /** + * Tests the loading of GroupContent entities for a group. + * + * @dataProvider testEntityTypesDataProvider + * @covers ::loadByGroup + */ + public function testLoadContentByGroup($entity_type, $plugin_id) { + $group = $this->createGroup(); + $entity = $this->createTestEntity($entity_type); + $this->storage->createForEntityInGroup($entity, $group, $plugin_id)->save(); + $loaded_entities = $this->storage->loadByGroup($group); + $this->assertCount(2, $loaded_entities, 'Managed to load the group contents by group.'); + $loaded_users_contents = array_filter($loaded_entities, function (GroupContentInterface $group_content) { + return $group_content->getEntity()->getEntityTypeId() == 'user'; + }); + $loaded_group_contents = array_filter($loaded_entities, function (GroupContentInterface $group_content) use ($entity_type) { + return $group_content->getEntity()->getEntityTypeId() == $entity_type; + }); + $this->assertCount(1, $loaded_users_contents); + $this->assertCount(1, $loaded_group_contents); + + $group_content = reset($loaded_group_contents); + $this->assertSameEntity($entity, $group_content->getEntity()); + } + + /** + * Tests the loading of GroupContent entities for an entity. + * + * @dataProvider testEntityTypesDataProvider + * @covers ::loadByEntity + */ + public function testLoadContentByEntity($entity_type, $plugin_id) { + $group = $this->createGroup(); + $entity = $this->createTestEntity($entity_type); + $this->storage->createForEntityInGroup($entity, $group, $plugin_id)->save(); + $loaded_entities = $this->storage->loadByEntity($entity); + $this->assertCount(1, $loaded_entities, 'Managed to load the group content by entity.'); + $group_content = reset($loaded_entities); + $this->assertSameEntity($entity, $group_content->getEntity()); + } + + /** + * Tests the loading of GroupContent entities for an entity. + * + * @dataProvider testEntityTypesDataProvider + * @covers ::loadByContentPluginId + */ + public function testLoadContentByContentPluginId($entity_type, $plugin_id) { + $group = $this->createGroup(); + $entity = $this->createTestEntity($entity_type); + $this->storage->createForEntityInGroup($entity, $group, $plugin_id)->save(); + $loaded_entities = $this->storage->loadByContentPluginId($plugin_id); + $this->assertCount(1, $loaded_entities, 'Managed to load the group content by plugin ID.'); + $group_content = reset($loaded_entities); + $this->assertSameEntity($entity, $group_content->getEntity()); + } + + /** + * Data provider returning test entity types and corresponding plugins. + */ + public function testEntityTypesDataProvider() { + return [ + ['group_test_content_entity_int', 'integer_content_entity_as_content'], + ['group_test_content_entity_string', 'string_content_entity_as_content'], + ['group_test_config_entity_string', 'string_config_entity_as_content'], + ]; + } + + /** + * Creates test entity to be used as a test group content. + * + * @param string $entity_type + * Entity type. + * + * @return \Drupal\Core\Entity\EntityInterface + * The new saved entity of a given type. + */ + protected function createTestEntity($entity_type) { + $storage = $this->entityTypeManager->getStorage($entity_type); + $values = []; + + switch ($entity_type) { + case 'group_test_content_entity_string': + case 'group_test_config_entity_string': + $values['id'] = $this->randomMachineName(); + break; + } + + $entity = $storage->create($values); + $entity->save(); + return $entity; + } + + /** + * Asserts two entity objects are same. + * + * @param \Drupal\Core\Entity\EntityInterface $expected + * The entity which was expected. + * @param \Drupal\Core\Entity\EntityInterface $actual + * The entity which was retrieved during the test. + */ + protected function assertSameEntity(EntityInterface $expected, EntityInterface $actual) { + $this->assertEquals($expected->getEntityTypeId(), $actual->getEntityTypeId()); + $this->assertEquals($expected->id(), $actual->id()); + } + } diff --git a/tests/src/Kernel/GroupContentTest.php b/tests/src/Kernel/GroupContentTest.php index cae780f..7bdac5b 100644 --- a/tests/src/Kernel/GroupContentTest.php +++ b/tests/src/Kernel/GroupContentTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\group\Kernel; use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\group\Entity\GroupContent; /** * Tests for the GroupContent entity. @@ -16,7 +17,18 @@ class GroupContentTest extends GroupKernelTestBase { /** * {@inheritdoc} */ - protected static $modules = ['group_test', 'group_test_plugin']; + protected static $modules = ['group_test', 'group_test_plugin', 'group_test_content']; + + /** + * Tests getting entity reference field name. + * + * @covers ::getEntityFieldNameForEntityType + */ + public function testGetGroupContentTypeField() { + $this->assertEquals('entity_id', GroupContent::getEntityFieldNameForEntityType('group_test_content_entity_int')); + $this->assertEquals('entity_id_str', GroupContent::getEntityFieldNameForEntityType('group_test_content_entity_string')); + $this->assertEquals('entity_id_str', GroupContent::getEntityFieldNameForEntityType('group_test_config_entity_string')); + } /** * Tests that entity url templates are functional.