entity_agree-2.0.x-dev/src/AgreementManager.php
src/AgreementManager.php
<?php namespace Drupal\entity_agree; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Session\AccountInterface; use Drupal\entity_agree\Plugin\Field\FieldType\AgreementItem; use Kerasai\DrupalCacheHelper\CacheHelperTrait; use Symfony\Component\HttpFoundation\RequestStack; /** * Manage agreements. */ class AgreementManager { use CacheHelperTrait; /** * The current request. * * @var \Symfony\Component\HttpFoundation\Request */ protected $currentRequest; /** * The current user. * * @var \Drupal\Core\Session\AccountInterface */ protected $currentUser; /** * Entity type manager service. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * Constructs an AgreementManager. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager * The entity type manager service. * @param \Drupal\Core\Session\AccountInterface $currentUser * The current user. * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack * The request stack service. */ public function __construct(EntityTypeManagerInterface $entityTypeManager, AccountInterface $currentUser, RequestStack $requestStack) { $this->entityTypeManager = $entityTypeManager; $this->currentUser = $currentUser; $this->currentRequest = $requestStack->getCurrentRequest(); } /** * Agree to an agreement. * * @param \Drupal\Core\Entity\RevisionableInterface $entity * The entity being agreed to. * @param \Drupal\Core\Session\AccountInterface|null $account * The user who is agreeing. Optional, defaults to the current user. * * @return \Drupal\entity_agree\AgreementInterface * The agreement. */ public function agree(RevisionableInterface $entity, AccountInterface $account = NULL) { if (!$account) { $account = $this->currentUser; } $agreement = $this->entityTypeManager->getStorage('entity_agree_agreement') ->create([ 'uid' => $account->id(), 'entity_type_id' => $entity->getEntityTypeId(), 'entity_id' => $entity->id(), 'entity_vid' => $entity->getRevisionId(), 'ip_address' => $this->currentRequest->getClientIp(), ]); $agreement->save(); return $agreement; } /** * Determines agreements that user needs to agree-to. * * Note, this does not take account applies handlers. Use * `::getOutstandingAgreementEntities` to load full entities accounting for * applies handling. * * @param \Drupal\Core\Session\AccountInterface|null $account * The account being analyzed. Optional, defaults to the current user. * * @return array * Multidimensional array containing agreements the user needs to agree to. * Outer keys are entity type IDs, and values are arrays of current * revision IDs keyed by IDs. */ public function getOutstandingAgreements(AccountInterface $account = NULL) { if (!$account) { $account = $this->currentUser; } // No support for anonymous users. if ($account->isAnonymous()) { return []; } if ($account->hasPermission('bypass entity agreements')) { return []; } $user_agreements = $this->getUserAgreements($account); $data = []; foreach ($this->getAgreementRevisions() as $entity_type => $agreements) { $data[$entity_type] = array_diff($agreements, $user_agreements[$entity_type] ?? []); } return array_filter($data); } /** * Gets all entities that the user needs to agree-to. * * @param \Drupal\Core\Session\AccountInterface|null $account * The account being analyzed. Optional, defaults to the current user. * @param string|null $op * Entity operation to check for access. Optional, defauls to "view". To * bypass access check, use NULL/FALSE. * * @return array * Multidimensional array containing agreements the user needs to agree to. * Outer keys are entity type IDs, and values fully-loaded entities keyed * keyed by IDs. */ public function getOutstandingAgreementEntities(AccountInterface $account = NULL, $op = 'view') { if (!$account) { $account = $this->currentUser; } // Load entity IDs from cache if possible. These are cached after access // has been checked and applies handlers have ran, so we can just load and // return the entities. $cid = __METHOD__ . ($op ? ":{$op}" : '') . ":user:{$account->id()}"; if ($data = $this->getCacheHelper()->get($cid, 'entity_agree')) { return $this->loadEntities($data->data); } $data = []; $cache = new CacheableMetadata(); foreach ($this->loadEntities($this->getOutstandingAgreements()) as $entity_type => $agreements) { // Grab caching info from agreements. foreach ($agreements as $agreement) { $cache->addCacheableDependency($agreement); } // Ensure agreements are applicable. $data[$entity_type] = array_filter($agreements, [ $this, 'agreementApplies', ]); // Check access to the entities for the specified operation, unless $op // is NULL/FALSE. if ($op) { $data[$entity_type] = array_filter($data[$entity_type], function ($entity) use ($op, $account) { return $entity->access($op, $account); }); } } // Pull the IDs from the outstanding entities and cache. $cache->addCacheTags(['entity_agree', "entity_agree:user:{$account->id()}"]); $this->getCacheHelper()->set($cid, $this->getEntityIds($data), 'entity_agree', Cache::PERMANENT, $cache->getCacheTags()); return array_filter($data); } /** * Gets agreement information for a user. * * @return array * The user's current agreements. */ public function getUserAgreements(AccountInterface $account) { $cid = __METHOD__ . ":user:{$account->id()}"; if ($data = $this->getCacheHelper()->get($cid, 'entity_agree')) { return $data->data; } $data = []; // Query out all the user's agreements, collate by entity type and entity // ID. // @todo Convert this to SQL query to allow grouping and determine latest // (MAX) revision ID? /** @var \Drupal\entity_agree\AgreementInterface[] $agreements */ $agreements = $this->entityTypeManager->getStorage('entity_agree_agreement') ->loadByProperties([ 'uid' => $account->id(), ]); foreach ($agreements as $agreement) { $data[$agreement->getTargetEntityTypeId()][$agreement->getTargetEntityId()][] = $agreement->getTargetEntityRevisionId(); } // Determine the latest/highest revision ID for each agreement. foreach ($data as $entity_type => $entities) { foreach ($entities as $entity_id => $revisions) { $data[$entity_type][$entity_id] = max($data[$entity_type][$entity_id]); } } $tags = ['entity_agree', "entity_agree:user:{$account->id()}"]; $this->getCacheHelper()->set($cid, $data, 'entity_agree', Cache::PERMANENT, $tags); return $data; } /** * Get the current revisions for each agreement. * * @return array * And array of entity revision IDs for agreements. Multidimensional array * keyed by entity type and entity ID. */ public function getAgreementRevisions() { if ($data = $this->getCacheHelper()->get(__METHOD__)) { return $data->data; } $data = []; foreach ($this->getAgreementFields() as $entity_type => $fields) { $query = $this->entityTypeManager->getStorage($entity_type) ->getQuery('OR'); foreach ($fields as $field) { $query->condition($field, AgreementItem::ENTITY_AGREE_STATUS_ACTIVE); } $data[$entity_type] = array_flip($query->accessCheck(FALSE)->execute()); } $this->getCacheHelper()->set(__METHOD__, $data, 'default', Cache::PERMANENT, ['entity_agree']); return $data; } /** * Gets information about entity types with agreement fields. * * @return array * An array of agreement fields per entity type. Multidimensional array * keyed by entity type. */ public function getAgreementFields() { if ($data = $this->getCacheHelper()->get(__METHOD__)) { return $data->data; } $data = []; /** @var \Drupal\field\FieldStorageConfigInterface[] $fields */ $fields = $this->entityTypeManager->getStorage('field_storage_config') ->loadByProperties(['type' => 'entity_agree']); foreach ($fields as $field) { $data[$field->getTargetEntityTypeId()][] = $field->getName(); } $this->getCacheHelper()->set(__METHOD__, $data, 'default', Cache::PERMANENT, ['entity_field_info']); return $data; } /** * Determines if an entity has an agreement field. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. * * @return bool * Indicates if an entity has an agreement field. */ public static function entityHasAgreementField(EntityInterface $entity) { if (!$entity instanceof FieldableEntityInterface) { return FALSE; } foreach ($entity->getFieldDefinitions() as $field) { if ($field->getType() == 'entity_agree') { return TRUE; } } return FALSE; } /** * Determines if an entity applies as an agreement. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. * * @return bool * Indicates if an entity applies as an agreement. */ public function agreementApplies(EntityInterface $entity) { $result = TRUE; $fields = AgreementItem::getEntityAgreementPlugins($entity); foreach ($fields as $plugins) { foreach ($plugins as $plugin) { if (!$plugin instanceof AgreementHandlerAppliesInterface) { continue; } if ($plugin->applies()) { return TRUE; } else { $result = FALSE; } } } // Either no applies plugins, or there were applies plugins but none // returned TRUE. return $result; } /** * Gets the user's current agreement for the specified entity. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. * @param \Drupal\Core\Session\AccountInterface|null $account * The account being analyzed. Optional, defaults to the current user. * * @return \Drupal\entity_agree\AgreementInterface|null * The user's current agreement for the specified entity, or NULL if not * available. */ public function userCurrentAgreement(EntityInterface $entity, AccountInterface $account = NULL) { if (!$entity instanceof RevisionableInterface) { return NULL; } if (!$account) { $account = $this->currentUser; } $agreement_ids = $this->entityTypeManager->getStorage('entity_agree_agreement') ->getQuery() ->condition('uid', $account->id()) ->condition('entity_type_id', $entity->getEntityTypeId()) ->condition('entity_id', $entity->id()) ->condition('entity_vid', $entity->getRevisionId()) ->execute(); if (!$agreement_id = reset($agreement_ids)) { return NULL; } return $this->entityTypeManager->getStorage('entity_agree_agreement') ->load($agreement_id); } /** * Determines if the user needs to agree to the specified entity. * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity. * @param \Drupal\Core\Session\AccountInterface|null $account * The account being analyzed. Optional, defaults to the current user. * * @return bool * Indicates if the user needs to agree to the entity. */ public function userNeedsAgreement(EntityInterface $entity, AccountInterface $account = NULL) { if (!$account) { $account = $this->currentUser; } return !empty($this->getOutstandingAgreements($account)[$entity->getEntityTypeId()][$entity->id()]); } /** * Loads entity data. * * @param array $data * A multidimensional array. Outer keys are entity type IDs, values are * arrays of revision IDs, keyed by entity ID. * * @return array * A multidimensional array of entities. Outer keys are entity type IDs, * values are arrays of entities, keyed by entity ID. */ public function loadEntities(array $data) { $result = []; foreach ($data as $entity_type => $vids) { $result[$entity_type] = $this->entityTypeManager->getStorage($entity_type) ->loadMultiple(array_keys($vids)); } return array_filter($result); } /** * Obtains entity ID and revision info from a set of entities. * * @param array $data * A multidimensional array of entities. Outer keys are entity type IDs, * values are arrays of entities, keyed by entity ID. * * @return array * A multidimensional array. Outer keys are entity type IDs, values are * arrays of revision IDs, keyed by entity ID. */ public function getEntityIds(array $data) { $result = []; /** @var \Drupal\Core\Entity\EntityInterface[] $entities */ foreach ($data as $entity_type => $entities) { foreach ($entities as $entity) { if (!$entity instanceof RevisionableInterface) { continue; } $result[$entity_type][$entity->id()] = $entity->getRevisionId(); } } return $result; } }