diff --git a/group.views.inc b/group.views.inc new file mode 100644 index 0000000..d01c3df --- /dev/null +++ b/group.views.inc @@ -0,0 +1,60 @@ +getDefinitions(); + + // Get the data table for GroupContent entities. + $data_table = $entity_types['group_content']->getDataTable(); + + /** @var \Drupal\group\Plugin\GroupContentEnablerManagerInterface $plugin_manager */ + $plugin_manager = \Drupal::service('plugin.manager.group_content_enabler'); + + // Retrieve all installed content enabler plugins. + $installed = $plugin_manager->getInstalledIds(); + + // Add views data for each installed plugin. + $group_content_plugins = $plugin_manager->getAll(); + foreach ($group_content_plugins as $plugin_id => $plugin) { + // Skip plugins that have not been installed anywhere. + if (!in_array($plugin_id, $installed)) { + continue; + } + + $entity_type_id = $plugin->getEntityTypeId(); + $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'])) { + continue; + } + + $t_args = [ + '@entity_type' => $entity_type->getLabel(), + ]; + + // 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. + $data[$entity_data_table]['group_content'] = array( + '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' => array( + 'group' => t('Group content'), + 'base' => $data_table, + 'base field' => 'entity_id', + '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/GroupContentType.php b/src/Entity/GroupContentType.php index 67a3f6c..19ae65d 100644 --- a/src/Entity/GroupContentType.php +++ b/src/Entity/GroupContentType.php @@ -136,7 +136,7 @@ class GroupContentType extends ConfigEntityBundleBase implements GroupContentTyp */ public static function loadByEntityTypeId($entity_type_id) { $plugin_ids = []; - + /** @var \Drupal\group\Plugin\GroupContentEnablerManagerInterface $plugin_manager */ $plugin_manager = \Drupal::service('plugin.manager.group_content_enabler'); @@ -159,6 +159,31 @@ class GroupContentType extends ConfigEntityBundleBase implements GroupContentTyp /** * {@inheritdoc} */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + + if (!$update) { + // When a new GroupContentType is saved, we clear the views data cache to + // make sure that all of the views data which relies on group content + // types is up to date. + \Drupal::service('views.views_data')->clear(); + + /** @var \Drupal\group\Plugin\GroupContentEnablerManagerInterface $plugin_manager */ + $plugin_manager = \Drupal::service('plugin.manager.group_content_enabler'); + + // We also need to reset the cache that maps plugin IDs to group content + // type IDs as this one needs to be added to it. + $plugin_manager->clearCachedGroupContentTypeIdMap(); + + // A previously unused plugin may be in use now, so clear the installed + // plugin ID cache. + $plugin_manager->clearCachedInstalledIds(); + } + } + + /** + * {@inheritdoc} + */ public static function postDelete(EntityStorageInterface $storage, array $entities) { // In case the group content type got deleted by uninstalling the providing // module, we still need to uninstall it on the group type. @@ -170,6 +195,21 @@ class GroupContentType extends ConfigEntityBundleBase implements GroupContentTyp $group_type->save(); } } + + // When a GroupContentType is deleted, we clear the views data cache to make + // sure that all of the views data which relies on group content types is up + // to date. + \Drupal::service('views.views_data')->clear(); + + /** @var \Drupal\group\Plugin\GroupContentEnablerManagerInterface $plugin_manager */ + $plugin_manager = \Drupal::service('plugin.manager.group_content_enabler'); + + // We also need to reset the cache that maps plugin IDs to group content + // type IDs as this one needs to be removed from it. + $plugin_manager->clearCachedGroupContentTypeIdMap(); + + // A plugin may no longer be in use now, so clear the installed ID cache. + $plugin_manager->clearCachedInstalledIds(); } /** diff --git a/src/Entity/Views/GroupContentViewsData.php b/src/Entity/Views/GroupContentViewsData.php index 0aac908..8d5828b 100644 --- a/src/Entity/Views/GroupContentViewsData.php +++ b/src/Entity/Views/GroupContentViewsData.php @@ -18,8 +18,6 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides the views data for the group content entity type. - * - * @todo replace the GroupContent -> Entity relationship with one of our own. */ class GroupContentViewsData extends EntityViewsData { @@ -31,6 +29,13 @@ class GroupContentViewsData extends EntityViewsData { protected $pluginManager; /** + * The entity manager set but not declared in the parent class. + * + * @var \Drupal\Core\Entity\EntityManagerInterface; + */ + protected $entityManager; + + /** * Constructs a GroupContentViewsData object. * * @param \Drupal\group\Plugin\GroupContentEnablerManagerInterface $plugin_manager @@ -64,17 +69,33 @@ class GroupContentViewsData extends EntityViewsData { // Get the data table for GroupContent entities. $data_table = $this->entityType->getDataTable(); - // Relate entities to group content. + // 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 */ $entity_types = $this->entityManager->getDefinitions(); + // Retrieve all installed content enabler plugins. + $installed = $this->pluginManager->getInstalledIds(); + + // Add views data for each installed plugin. $group_content_plugins = $this->pluginManager->getAll(); - foreach ($group_content_plugins as $plugin) { + foreach ($group_content_plugins as $plugin_id => $plugin) { + // Skip plugins that have not been installed anywhere. + if (!in_array($plugin_id, $installed)) { + continue; + } + $entity_type_id = $plugin->getEntityTypeId(); $entity_type = $entity_types[$entity_type_id]; $entity_data_table = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); - // We only add one 'group_content' reverse relationship per entity type. - if (isset($data[$entity_data_table]['group_content'])) { + // Create a unique field name for this views field. + $field_name = 'gc__' . $entity_type_id; + + // We only add one 'group_content' relationship per entity type. + if (isset($data[$entity_data_table][$field_name])) { continue; } @@ -82,16 +103,19 @@ class GroupContentViewsData extends EntityViewsData { '@entity_type' => $entity_type->getLabel(), ]; - $data[$entity_data_table]['group_content'] = array( - 'title' => t('@entity_type group content', $t_args), - 'help' => t('Relate @entity_type to the entity that represents its relationship to a Group.', $t_args), + // 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] = array( + 'title' => t('@entity_type from group content', $t_args), + 'help' => t('Relates to the @entity_type entity the group content represents.', $t_args), 'relationship' => array( - 'group' => $this->t('Group content'), - 'base' => $data_table, - 'base field' => 'entity_id', - 'relationship field' => $entity_type->getKey('id'), - 'id' => 'group_content_to_entity_reverse', - 'label' => t('@entity_type group content', $t_args), + 'group' => $entity_type->getLabel(), + 'base' => $entity_data_table, + 'base field' => $entity_type->getKey('id'), + 'relationship field' => 'entity_id', + 'id' => 'group_content_to_entity', + 'label' => t('Group content @entity_type', $t_args), + 'target_entity_type' => $entity_type_id, ), ); } diff --git a/src/Plugin/GroupContentEnablerManager.php b/src/Plugin/GroupContentEnablerManager.php index 72b7704..3a1d997 100644 --- a/src/Plugin/GroupContentEnablerManager.php +++ b/src/Plugin/GroupContentEnablerManager.php @@ -7,6 +7,7 @@ namespace Drupal\group\Plugin; +use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; @@ -43,7 +44,28 @@ class GroupContentEnablerManager extends DefaultPluginManager implements GroupCo * * @var string[] */ - protected $installedPluginIds; + protected $installedIds; + + /** + * The cache key for the installed content enabler plugin IDs. + * + * @var string + */ + protected $installedIdsCacheKey; + + /** + * An static cache of group content type IDs per plugin ID. + * + * @var array[] + */ + protected $groupContentTypeIdMap; + + /** + * The cache key for the group content type ID map. + * + * @var string + */ + protected $groupContentTypeIdMapCacheKey; /** * Constructs a GroupContentEnablerManager object. @@ -63,9 +85,10 @@ class GroupContentEnablerManager extends DefaultPluginManager implements GroupCo $this->alterInfo('group_content_info'); $this->setCacheBackend($cache_backend, 'group_content_enablers'); $this->entityTypeManager = $entity_type_manager; + $this->installedIdsCacheKey = $this->cacheKey . '_installed'; + $this->groupContentTypeIdMapCacheKey = $this->cacheKey . '_GCT_map'; } - /** * {@inheritdoc} */ @@ -89,7 +112,9 @@ class GroupContentEnablerManager extends DefaultPluginManager implements GroupCo * {@inheritdoc} */ public function getInstalledIds() { - if (!isset($this->installedPluginIds)) { + $plugin_ids = $this->getCachedInstalledIDs(); + + if (!isset($plugin_ids)) { $plugin_ids = []; // Installed plugins can only live on a group type. By retrieving all of @@ -101,10 +126,47 @@ class GroupContentEnablerManager extends DefaultPluginManager implements GroupCo $plugin_ids = array_merge($plugin_ids, array_keys($group_type->get('content'))); } - $this->installedPluginIds = array_unique($plugin_ids); + $plugin_ids = array_unique($plugin_ids); + $this->setCachedInstalledIDs($plugin_ids); } - return $this->installedPluginIds; + return $plugin_ids; + } + + /** + * {@inheritdoc} + */ + public function clearCachedInstalledIDs() { + if ($this->cacheBackend) { + $this->cacheBackend->delete($this->installedIdsCacheKey); + } + $this->installedIds = NULL; + } + + /** + * Returns the cached installed plugin IDs. + * + * @return array|null + * On success this will return the installed plugin ID list. On failure this + * should return NULL, indicating to other methods that this has not yet + * been defined. Success with no values should return as an empty array. + */ + protected function getCachedInstalledIDs() { + if (!isset($this->installedIds) && $cache = $this->cacheGet($this->installedIdsCacheKey)) { + $this->installedIds = $cache->data; + } + return $this->installedIds; + } + + /** + * Sets a cache of the installed plugin IDs. + * + * @param array $plugin_ids + * The installed plugin IDs to store in cache. + */ + protected function setCachedInstalledIDs($plugin_ids) { + $this->cacheSet($this->installedIdsCacheKey, $plugin_ids, Cache::PERMANENT); + $this->installedIds = $plugin_ids; } /** @@ -139,9 +201,69 @@ class GroupContentEnablerManager extends DefaultPluginManager implements GroupCo /** * {@inheritdoc} */ - public function reset() { + public function getGroupContentTypeIds($plugin_id) { + $map = $this->getCachedGroupContentTypeIdMap(); + + if (!isset($map)) { + $map = []; + + /** @var \Drupal\group\Entity\GroupContentTypeInterface[] $group_content_types */ + $group_content_types = $this->entityTypeManager->getStorage('group_content_type')->loadMultiple(); + foreach ($group_content_types as $group_content_type) { + $map[$group_content_type->getContentPluginId()][] = $group_content_type->id(); + } + + $this->setCachedGroupContentTypeIdMap($map); + } + + return isset($map[$plugin_id]) ? $map[$plugin_id] : []; + } + + /** + * {@inheritdoc} + */ + public function clearCachedGroupContentTypeIdMap() { + if ($this->cacheBackend) { + $this->cacheBackend->delete($this->groupContentTypeIdMapCacheKey); + } + $this->groupContentTypeIdMap = NULL; + } + + /** + * Returns the cached group content type ID map. + * + * @return array|null + * On success this will return the group content ID map (array). On failure + * this should return NULL, indicating to other methods that this has not + * yet been defined. Success with no values should return as an empty array. + */ + protected function getCachedGroupContentTypeIdMap() { + if (!isset($this->groupContentTypeIdMap) && $cache = $this->cacheGet($this->groupContentTypeIdMapCacheKey)) { + $this->groupContentTypeIdMap = $cache->data; + } + return $this->groupContentTypeIdMap; + } + + /** + * Sets a cache of the group content type ID map. + * + * @param array $map + * The group content type ID map to store in cache. + */ + protected function setCachedGroupContentTypeIdMap($map) { + $this->cacheSet($this->groupContentTypeIdMapCacheKey, $map, Cache::PERMANENT); + $this->groupContentTypeIdMap = $map; + } + + /** + * {@inheritdoc} + */ + public function clearCachedDefinitions() { + parent::clearCachedDefinitions(); + + // The collection of all plugins should only change if the plugin + // definitions change, so we can safely reset that here. $this->allPlugins = NULL; - $this->installedPluginIds = []; } } diff --git a/src/Plugin/GroupContentEnablerManagerInterface.php b/src/Plugin/GroupContentEnablerManagerInterface.php index ed60a8e..7e62649 100644 --- a/src/Plugin/GroupContentEnablerManagerInterface.php +++ b/src/Plugin/GroupContentEnablerManagerInterface.php @@ -42,6 +42,11 @@ interface GroupContentEnablerManagerInterface extends PluginManagerInterface { public function getInstalledIds(); /** + * Clears static and persistent installed plugin ID caches. + */ + public function clearCachedInstalledIds(); + + /** * Installs all plugins which are marked as enforced. * * @param \Drupal\group\Entity\GroupTypeInterface $group_type @@ -49,12 +54,21 @@ interface GroupContentEnablerManagerInterface extends PluginManagerInterface { * run the installation process for all group types. */ public function installEnforced(GroupTypeInterface $group_type = NULL); - + /** - * Resets the static properties on this class. + * Retrieves all of the group content types for a plugin. + * + * @param $plugin_id + * The ID of the plugin to retrieve GroupContentType entity IDs for. * - * @todo Incorporate in other cache invalidation? + * @return string[] + * An array of GroupContentType IDs. */ - public function reset(); - + public function getGroupContentTypeIds($plugin_id); + + /** + * Clears static and persistent group content type ID map caches. + */ + public function clearCachedGroupContentTypeIdMap(); + } diff --git a/src/Plugin/views/relationship/GroupContentToEntity.php b/src/Plugin/views/relationship/GroupContentToEntity.php new file mode 100644 index 0000000..179b60e --- /dev/null +++ b/src/Plugin/views/relationship/GroupContentToEntity.php @@ -0,0 +1,178 @@ +joinManager = $join_manager; + $this->pluginManager = $plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('plugin.manager.views.join'), + $container->get('plugin.manager.group_content_enabler') + ); + } + + /** + * {@inheritdoc} + */ + protected function defineOptions() { + $options = parent::defineOptions(); + $options['group_content_plugins']['default'] = []; + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + // Retrieve all of the plugins that can handle this entity type. + /** @var \Drupal\group\Plugin\GroupContentEnablerInterface $plugin */ + $options = []; + foreach ($this->pluginManager->getAll() as $plugin_id => $plugin) { + if ($plugin->getEntityTypeId() === $this->definition['target_entity_type']) { + $options[$plugin_id] = $plugin->getLabel(); + } + } + + $form['group_content_plugins'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Filter by plugin'), + '#description' => $this->t('Refine the result by plugin. Leave empty to select all plugins, including those that could be added after this relationship was configured.'), + '#options' => $options, + '#weight' => -2, + '#default_value' => $this->options['group_content_plugins'], + ]; + } + + /** + * {@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']; + } + + // Then add our own join condition, namely the group content type IDs. + $def['extra'][] = [ + 'left_field' => 'type', + 'value' => $this->getGroupContentTypeIds(), + ]; + + // 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. + * + * This checks if any plugins were selected on the option form and, in that + * case, loads only those group content types available to the selected + * plugins. Otherwise, all possible group content types for the relationship's + * entity type are loaded. + * + * This needs to happen live to cover the use case where a group content + * plugin is installed on a group type after this relationship has been + * configured on a view without any plugins selected. + * + * @todo Could be cached even more, I guess. + * + * @return string[] + * The group content type IDs to filter on. + */ + protected function getGroupContentTypeIds() { + $plugin_ids = array_filter($this->options['group_content_plugins']); + + $group_content_type_ids = []; + foreach ($plugin_ids as $plugin_id) { + $group_content_type_ids = array_merge($group_content_type_ids, $this->pluginManager->getGroupContentTypeIds($plugin_id)); + } + + return $group_content_type_ids ?: array_keys(GroupContentType::loadByEntityTypeId($this->definition['target_entity_type'])); + } + +} diff --git a/src/Plugin/views/relationship/GroupContentToEntityReverse.php b/src/Plugin/views/relationship/GroupContentToEntityReverse.php index eaa2d63..7f7c155 100644 --- a/src/Plugin/views/relationship/GroupContentToEntityReverse.php +++ b/src/Plugin/views/relationship/GroupContentToEntityReverse.php @@ -15,7 +15,7 @@ use Drupal\views\Plugin\ViewsHandlerManager; use Symfony\Component\DependencyInjection\ContainerInterface; /** - * A relationship handlers which reverses group content entity references. + * A relationship handler which reverses group content entity references. * * @ingroup views_relationship_handlers * @@ -69,16 +69,7 @@ class GroupContentToEntityReverse extends RelationshipPluginBase { */ protected function defineOptions() { $options = parent::defineOptions(); - - // Set the defaults for the checkboxes which allow you to filter by plugin. $options['group_content_plugins']['default'] = []; - - // Set default values that mimic those submitted by checkboxes to set which - // group content types we're actually filtering on. This defaults to all of - // the ones that could be used by the entity type set on the handler. - $group_content_type_ids = array_keys(GroupContentType::loadByEntityTypeId($this->definition['entity_type'])); - $options['group_content_types']['default'] = array_combine($group_content_type_ids, $group_content_type_ids); - return $options; } @@ -87,59 +78,24 @@ class GroupContentToEntityReverse extends RelationshipPluginBase { */ public function buildOptionsForm(&$form, FormStateInterface $form_state) { parent::buildOptionsForm($form, $form_state); - $options = []; // Retrieve all of the plugins that can handle this entity type. /** @var \Drupal\group\Plugin\GroupContentEnablerInterface $plugin */ + $options = []; foreach ($this->pluginManager->getAll() as $plugin_id => $plugin) { if ($plugin->getEntityTypeId() === $this->definition['entity_type']) { $options[$plugin_id] = $plugin->getLabel(); } } - if (count($options) > 1) { - $form['group_content_plugins'] = [ - '#type' => 'checkboxes', - '#title' => $this->t('Filter by plugin'), - '#description' => $this->t('Refine the result by plugin. Leave empty to fetch all results.'), - '#options' => $options, - '#weight' => -2, - '#default_value' => $this->options['group_content_plugins'], - ]; - } - } - - /** - * We save the group content types this handler will filter on based off of - * which plugins were selected in the options form. This will allow us to - * easily filter the query on group content types without having to run an - * extra query or add a complicated join to the view query. - * - * {@inheritdoc} - */ - public function validateOptionsForm(&$form, FormStateInterface $form_state) { - // Retrieve the selected group content plugins. - $options = $form_state->getValue('options'); - $plugin_ids = array_filter($options['group_content_plugins']); - - // If no plugin was selected, we use all of the possible group content types - // for the entity type this handler was selected for. - $group_content_types = empty($plugin_ids) - ? GroupContentType::loadByEntityTypeId($this->definition['entity_type']) - : GroupContentType::loadByContentPluginId($plugin_ids); - - // Retrieve the "selected" group content type IDs as valid options. - $group_content_type_ids = array_keys($group_content_types); - $selected = array_combine($group_content_type_ids, $group_content_type_ids); - - // Create an array containing all options as unselected so we can add it to - // the array containing the selected options. This will result in an array - // which only has the actually selected options with non-zero values. - $unselected = array_fill_keys($this->options['group_content_types'], 0); - - // Save the options as if they were coming from a checkboxes element. - $options['group_content_types'] = $selected + $unselected; - $form_state->setValue('options', $options); + $form['group_content_plugins'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Filter by plugin'), + '#description' => $this->t('Refine the result by plugin. Leave empty to select all plugins, including those that could be added after this relationship was configured.'), + '#options' => $options, + '#weight' => -2, + '#default_value' => $this->options['group_content_plugins'], + ]; } /** @@ -169,7 +125,7 @@ class GroupContentToEntityReverse extends RelationshipPluginBase { // Then add our own join condition, namely the group content type IDs. $def['extra'][] = [ 'field' => 'type', - 'value' => array_filter($this->options['group_content_types']), + 'value' => $this->getGroupContentTypeIds(), ]; // Use the standard join plugin unless instructed otherwise. @@ -188,4 +144,32 @@ class GroupContentToEntityReverse extends RelationshipPluginBase { } } + /** + * Returns the group content types this relationship should filter on. + * + * This checks if any plugins were selected on the option form and, in that + * case, loads only those group content types available to the selected + * plugins. Otherwise, all possible group content types for the relationship's + * entity type are loaded. + * + * This needs to happen live to cover the use case where a group content + * plugin is installed on a group type after this relationship has been + * configured on a view without any plugins selected. + * + * @todo Could be cached even more, I guess. + * + * @return string[] + * The group content type IDs to filter on. + */ + protected function getGroupContentTypeIds() { + $plugin_ids = array_filter($this->options['group_content_plugins']); + + $group_content_type_ids = []; + foreach ($plugin_ids as $plugin_id) { + $group_content_type_ids = array_merge($group_content_type_ids, $this->pluginManager->getGroupContentTypeIds($plugin_id)); + } + + return $group_content_type_ids ?: array_keys(GroupContentType::loadByEntityTypeId($this->definition['entity_type'])); + } + }