farm-2.x-dev/modules/asset/group/src/GroupMembership.php
modules/asset/group/src/GroupMembership.php
<?php namespace Drupal\farm_group; use Drupal\asset\Entity\AssetInterface; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\farm_log\LogQueryFactoryInterface; use Drupal\log\Entity\LogInterface; /** * Asset group membership logic. */ class GroupMembership implements GroupMembershipInterface { /** * The name of the log group reference field. * * @var string */ const LOG_FIELD_GROUP = 'group'; /** * Log query factory. * * @var \Drupal\farm_log\LogQueryFactoryInterface */ protected LogQueryFactoryInterface $logQueryFactory; /** * Entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected EntityTypeManagerInterface $entityTypeManager; /** * The time service. * * @var \Drupal\Component\Datetime\TimeInterface */ protected $time; /** * The database service. * * @var \Drupal\Core\Database\Connection */ protected $database; /** * Class constructor. * * @param \Drupal\farm_log\LogQueryFactoryInterface $log_query_factory * Log query factory. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity type manager. * @param \Drupal\Component\Datetime\TimeInterface $time * The time service. * @param \Drupal\Core\Database\Connection $database * The database service. */ public function __construct(LogQueryFactoryInterface $log_query_factory, EntityTypeManagerInterface $entity_type_manager, TimeInterface $time, Connection $database) { $this->logQueryFactory = $log_query_factory; $this->entityTypeManager = $entity_type_manager; $this->time = $time; $this->database = $database; } /** * {@inheritdoc} */ public function hasGroup(AssetInterface $asset, $timestamp = NULL): bool { // Load the group assignment log. Bail if empty. $log = $this->getGroupAssignmentLog($asset, $timestamp); if (empty($log)) { return FALSE; } // Return emptiness of the group references. return !$log->get(static::LOG_FIELD_GROUP)->isEmpty(); } /** * {@inheritdoc} */ public function getGroup(AssetInterface $asset, $timestamp = NULL): array { // Load the group assignment log. Bail if empty. $log = $this->getGroupAssignmentLog($asset, $timestamp); if (empty($log)) { return []; } // Return referenced entities. return $log->{static::LOG_FIELD_GROUP}->referencedEntities() ?? []; } /** * {@inheritdoc} */ public function getGroupAssignmentLog(AssetInterface $asset, $timestamp = NULL): ?LogInterface { // If the asset is new, no group assignment logs will reference it. if ($asset->isNew()) { return NULL; } // If $timestamp is NULL, use the current time. if (is_null($timestamp)) { $timestamp = $this->time->getRequestTime(); } // Query for group assignment logs that reference the asset. // We do not check access on the logs to ensure that none are filtered out. $options = [ 'asset' => $asset, 'timestamp' => $timestamp, 'status' => 'done', 'limit' => 1, ]; $query = $this->logQueryFactory->getQuery($options); $query->condition('is_group_assignment', TRUE); $query->accessCheck(FALSE); $log_ids = $query->execute(); // Bail if no logs are found. if (empty($log_ids)) { return NULL; } // Load the first log. /** @var \Drupal\log\Entity\LogInterface $log */ $log = $this->entityTypeManager->getStorage('log')->load(reset($log_ids)); // Return the log, if available. if (!is_null($log)) { return $log; } // Otherwise, return NULL. return NULL; } /** * {@inheritdoc} */ public function getGroupMembers(array $groups, bool $recurse = TRUE, $timestamp = NULL): array { // Get group ids. $group_ids = array_map(function (AssetInterface $group) { return $group->id(); }, $groups); // Bail if there are no group ids. if (empty($group_ids)) { return []; } // If $timestamp is NULL, use the current time. if (is_null($timestamp)) { $timestamp = $this->time->getRequestTime(); } // Build query for group members. $query = " -- Select asset IDs from the asset base table. SELECT a.id FROM {asset} a -- Inner join logs that reference the assets. INNER JOIN {asset_field_data} afd ON afd.id = a.id INNER JOIN {log__asset} la ON a.id = la.asset_target_id AND la.deleted = 0 INNER JOIN {log_field_data} lfd ON lfd.id = la.entity_id -- Inner join group assets referenced by the logs. INNER JOIN {log__group} lg ON lg.entity_id = lfd.id AND lg.deleted = 0 -- Left join ANY future group assignment logs for the same asset. -- In the WHERE clause we'll exclude all records that have future logs, -- leaving only the 'current' log entry. LEFT JOIN ( {log_field_data} lfd2 INNER JOIN {log__asset} la2 ON la2.entity_id = lfd2.id AND la2.deleted = 0 ) ON lfd2.is_group_assignment = 1 AND la2.asset_target_id = a.id -- Future log entries have either a higher timestamp, or an equal timestamp and higher log ID. AND (lfd2.timestamp > lfd.timestamp OR (lfd2.timestamp = lfd.timestamp AND lfd2.id > lfd.id)) -- Don't include future logs beyond the given timestamp. -- These conditions should match the values in the WHERE clause. AND (lfd2.status = 'done') AND (lfd2.timestamp <= :timestamp) -- Limit results to completed membership assignment logs to the desired -- group that took place before the given timestamp. WHERE (lfd.is_group_assignment = 1) AND (lfd.status = 'done') AND (lfd.timestamp <= :timestamp) AND (lg.group_target_id IN (:group_ids[])) -- Exclude records with future log entries. AND lfd2.id IS NULL"; $args = [ ':timestamp' => $timestamp, ':group_ids[]' => $group_ids, ]; $result = $this->database->query($query, $args)->fetchAll(); $asset_ids = []; foreach ($result as $row) { if (!empty($row->id)) { $asset_ids[] = $row->id; } } if (empty($asset_ids)) { return []; } $asset_ids = array_unique($asset_ids); /** @var \Drupal\asset\Entity\AssetInterface[] $assets */ $assets = $this->entityTypeManager->getStorage('asset')->loadMultiple($asset_ids); if ($recurse) { // Iterate through the assets to check if any of them are groups. $groups = array_filter($assets, function (AssetInterface $asset) { return $asset->bundle() === 'group'; }); // Use array_replace so that numeric keys are preserved. $assets = array_replace($assets, $this->getGroupMembers($groups, $recurse, $timestamp)); } return $assets; } }