association-1.0.0-alpha2/association.module
association.module
<?php /** * @file * Drupal module hooks and global functions for the entity assocation module. */ use Drupal\association\Entity\AssociationInterface; use Drupal\association\EntityUpdater\PathAliasUpdater; use Drupal\association\EntityUpdater\SearchApiUpdater; use Drupal\association\Event\AssociatedEntityFormEvent; use Drupal\association\Event\AssociatedEntityUpdaterAlterEvent; use Drupal\association\Event\AssociationEvents; use Drupal\association\Field\AssociationReferenceItemList; use Drupal\Core\Access\AccessResult; use Drupal\Core\Cache\Cache; use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Entity\EntityFormInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Core\Utility\Error; use Drupal\toolshed\Strategy\Exception\StrategyException; use Drupal\views\Plugin\views\query\QueryPluginBase; use Drupal\views\Plugin\views\query\Sql; use Drupal\views\ViewExecutable; /** * Implements hook_module_implements_alter(). */ function association_module_implements_alter(&$implementations, $hook) { // Ensure that the association hook_entity_update() runs last. if ($hook === 'entity_update') { $callback = $implementations['association']; unset($implementations['association']); $implementations['association'] = $callback; } } /** * Implements hook_data_type_info_alter(). */ function association_data_type_info_alter(&$data_types) { $entity_type_manager = \Drupal::entityTypeManager(); $link_type_def = $entity_type_manager->getDefinition('association_link'); if ($link_type_def && !empty($data_types['entity:association_link'])) { /** @var \Drupal\association\EntityAdapterManagerInterface $adapter_manager */ $adapter_manager = \Drupal::service('plugin.manager.association.entity_adapter'); $data_type_def = $data_types['entity:association_link']; // Prefer the bundle definition as the base if it is available. if (!empty($data_types['entity:association_link:association_link'])) { $data_type_def = $data_types['entity:association_link:association_link']; unset($data_types['entity:association_link:association_link']); } foreach ($adapter_manager->getEntityTypes() as $entity_type_id) { $type_def = $entity_type_manager->getDefinition(($entity_type_id)); $data_type_id = 'entity:association_link:' . $entity_type_id; $data_types[$data_type_id] = $data_type_def; $data_types[$data_type_id]['label'] = new TranslatableMarkup('@label association link', [ '@label' => $type_def ? $type_def->getLabel() : 'Content', ]); } } } /** * Implements hook_condition_info_alter(). */ function association_condition_info_alter(&$conditions) { // Remove CTools derived entity condition plugin for entity associations as // these cause confusion and do not handle "none" condition correctly. if (isset($conditions['entity_bundle:association'])) { unset($conditions['entity_bundle:association']); } } /** * Implements hook_entity_bundle_field_info(). */ function association_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) { $adapter_manager = \Drupal::service('plugin.manager.association.entity_adapter'); $fields = []; // If a supported entity type is the entity being worked with, add a // association back reference field. if ($adapter_manager->isAssociableType($entity_type->id())) { $fields['associations'] = BaseFieldDefinition::create('entity_reference') ->setTargetEntityTypeId($entity_type->id()) ->setName(t('Associations')) ->setSetting('target_type', 'association_link') ->setLabel(t('Associations for @entity_type_label entities.', [ '@entity_type_label' => $entity_type->getLabel(), ])) ->setDescription(t('A list of all entity associations this entity belongs to.')) ->setRevisionable(FALSE) ->setTranslatable(FALSE) ->setComputed(TRUE) ->setCardinality(1) ->setClass(AssociationReferenceItemList::class) ->setDisplayConfigurable('view', TRUE); } return $fields; } /** * Implements hook_entity_access(). */ function association_entity_access(EntityInterface $entity, $op, AccountInterface $account) { try { /** @var \Drupal\association\EntityAdapterManagerInterface $adapter_manager */ $adapter_manager = \Drupal::service('plugin.manager.association.entity_adapter'); if ($adapter = $adapter_manager->getAdapter($entity)) { return $adapter->checkAccess($entity, $op, $account); } } catch (StrategyException) { // Not an associatable entity type. } return AccessResult::neutral(); } /** * Implements hook_query_TAG_alter() for entity_query. */ function association_query_entity_query_alter(AlterableInterface $query) { $adapter_manager = \Drupal::service('plugin.manager.association.entity_adapter'); $entity_type_id = $query->getMetaData('entity_type'); if ($query->hasTag($entity_type_id . '_access') && $adapter_manager->isAssociableType($entity_type_id)) { $assoc_type = \Drupal::entityTypeManager()->getDefinition('association'); $account = $query->getMetaData('account') ?: \Drupal::currentUser(); $op = $query->getMetaData('op') ?: 'view'; // If user account has permission to bypass association access checks, // we can skip the query alter as no additional restrictions are needed. if (!$account->hasPermission($assoc_type->getAdminPermission())) { if ($adapter = $adapter_manager->getAdapterByEntityType($entity_type_id)) { // Alter the query to respect permissions from entity associations. $adapter->accessQueryAlter($query, $op, $account); } } } } /** * Implements hook_query_TAG_alter() for views_entity_query. */ function association_query_views_entity_query_alter(AlterableInterface $query) { $entity_type_id = $query->getMetaData('entity_type'); $query->addTag($entity_type_id . '_access'); association_query_entity_query_alter($query); } /** * Implements hook_views_query_alter(). * * Borrowed from the "group" module, and is skipped if the group module is * also installed since it will take care of this. Included here so association * module can be used without requiring the groups module. */ function association_views_query_alter(ViewExecutable $view, QueryPluginBase $query) { if (\Drupal::moduleHandler()->moduleExists('group')) { // The group module does this for us if it's installed and running skip. return; } if ($query instanceof Sql && empty($query->options['disable_sql_rewrite'])) { $tables = $query->getEntityTableInfo(); $base_table = reset($tables); if (empty($base_table['entity_type']) || $base_table['relationship_id'] != 'none') { return; } // Add a custom tag so we don't trigger all other 'entity_query' tag alters. $entity_type_id = $base_table['entity_type']; $query->addTag('views_entity_query'); // Build the Views query already so we can access the DB query. $query->build($view); $view->built = TRUE; // Add metadata to the DB query. $query = $view->build_info['query']; $count_query = $view->build_info['count_query']; $query->addMetaData('entity_type', $entity_type_id); $count_query->addMetaData('entity_type', $entity_type_id); } } /** * Implements hook_form_alter(). * * Check if form is an entity form which belongs to an entity association, and * if it is apply association specific form alter hooks. */ function association_form_alter(array &$form, $form_state, $form_id) { $formObj = $form_state->getFormObject(); if ($formObj instanceof EntityFormInterface) { $entity = $formObj->getEntity(); $operation = $formObj->getOperation(); $events = AssociatedEntityFormEvent::getEventNames(); $negotiator = \Drupal::service('association.negotiator'); if (!empty($events[$operation]) && ($association = $negotiator->byEntity($entity))) { $event = new AssociatedEntityFormEvent($operation, $form, $form_state, $entity, $association); \Drupal::service('event_dispatcher')->dispatch($event, $event->getEventName()); } } } /** * Implements hook_form_FORM_ID_alter() for pathauto_pattern_form. */ function association_form_pathauto_pattern_form_alter(array &$form, FormStateInterface $form_state) { $formObj = $form_state->getFormObject(); if ($formObj instanceof EntityFormInterface) { $entity = $formObj->getEntity(); // Alters the pathauto pattern to include entity association conditions // to target content based on the associations they belong to. \Drupal::service('association.pathauto.helper')->alterForm($entity, $form, $form_state); } } /** * Implements hook_entity_update(). */ function association_entity_update(EntityInterface $entity) { // Check if associated content needs to get updated. Depending on the // number of items being updated, this update hook attempts to run them // in-place and queuing any additional items to cron if the number is large. if ($entity instanceof AssociationInterface) { $updaters = []; if (PathAliasUpdater::applies($entity)) { $updaters['path_alias'] = PathAliasUpdater::class; } if (SearchApiUpdater::applies($entity)) { $updaters['search_api'] = SearchApiUpdater::class; } $event = new AssociatedEntityUpdaterAlterEvent($updaters, $entity); \Drupal::service('event_dispatcher') ->dispatch($event, AssociationEvents::ENTITY_UPDATER_ALTER); if ($updaters) { $queueItem = new \stdClass(); $queueItem->current = 0; $queueItem->association = $entity->id(); $queueItem->updaters = $updaters; try { // Run one iteration of the queue worker. If exceeds maximum // batch size, future updates will happen on the next CRON run. \Drupal::service('plugin.manager.queue_worker') ->createInstance('association_linked_content_updater') ->processItem($queueItem); } catch (\Exception $e) { // If a processing error occurs, we'll be unable to used this queue // so just exit log the error. $logger = \Drupal::logger('association_update'); Error::logException($logger, $e); } } } $adapter_manager = \Drupal::service('plugin.manager.association.entity_adapter'); if ($adapter_manager->isAssociable($entity)) { /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ $tags = []; // Invalidate association cache tags when the associated entity is updated. // Ensures entity updates propagate to other associated entities and blocks. /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface */ foreach ($entity->get('associations') as $item) { if ($link = $item->entity) { $tags = Cache::mergeTags($tags, $link->getCacheTagsToInvalidate()); } } if ($tags) { Cache::invalidateTags($tags); } } } /** * Implements hook_entity_delete(). */ function association_entity_delete(EntityInterface $entity) { $adapter_manager = \Drupal::service('plugin.manager.association.entity_adapter'); if ($adapter_manager->isAssociable($entity)) { /** @var \Drupal\association\Entity\Storage\AssociationLinkStorageInterface */ $link_storage = \Drupal::entityTypeManager() ->getStorage('association_link'); if ($links = $link_storage->loadByTarget($entity)) { // Content is about to be deleted, don't allow association_link entity // to clear the target entity during it's clean-up, because this entity // is already being deleted. foreach ($links as $link) { $link->set('target', NULL); } $link_storage->delete($links); } } } /** * Implements hook_theme(). */ function association_theme($existing, $type, $theme, $path) { return [ 'association' => [ 'render element' => 'element', 'pattern' => 'association__', 'file' => 'association.theme', ], 'association_manifest_section' => [ 'render element' => 'element', ], ]; }