diff --git a/src/EntityUsageTrackBase.php b/src/EntityUsageTrackBase.php index 8b4b31894198d7447bfd676911392692d9c937e5..2d9167209e5bba3dcd753d5ba3686f27e5b52e24 100644 --- a/src/EntityUsageTrackBase.php +++ b/src/EntityUsageTrackBase.php @@ -483,4 +483,32 @@ abstract class EntityUsageTrackBase extends PluginBase implements EntityUsageTra return $this->enabledTargetEntityTypes === NULL || in_array($entity_type_id, $this->enabledTargetEntityTypes, TRUE); } + /** + * Prepare target entity values to be in the correct format. + * + * @param string $entityTypeId + * The entity type ID. + * @param array $ids + * An array of entity IDs, can be revision or UUIDs as well. + * @param string $idField + * The ID field; 'uuid', 'revision' or 'id'. + * + * @return array + * An array of the corresponding entity IDs from the IDs passed in, + * each prefixed with the string "$entityTypeId|". Non-loadable entities + * will be filtered out. + */ + protected function checkAndPrepareEntityIds(string $entityTypeId, array $ids, string $idField) { + if (empty($ids) || !$this->isEntityTypeTracked($entityTypeId)) { + return []; + } + $storage = $this->entityTypeManager->getStorage($entityTypeId); + $ids = $storage->getQuery() + ->accessCheck(FALSE) + ->condition($storage->getEntityType()->getKey($idField), $ids, 'IN') + ->execute(); + + return array_map(fn ($id) => $entityTypeId . '|' . $id, $ids); + } + } diff --git a/src/EntityUsageTrackInterface.php b/src/EntityUsageTrackInterface.php index 35eeb61777209b2fc2d627d68fc9bc2f08f37991..b3b4c56cc195c67669361ddfc915c7ceb2a50475 100644 --- a/src/EntityUsageTrackInterface.php +++ b/src/EntityUsageTrackInterface.php @@ -15,6 +15,27 @@ use Drupal\Core\Field\FieldItemInterface; * - Entities related through an entity_reference field are tracked using the * "entity_reference" method. * - Entities embedded into other entities are tracked using the "embed" method. + * + * Note that plugins extending this interface have to be performant. + * Constructing the entity_usage table for large sites can take a long time and + * involve millions of calls to ::getTargetEntities(). Best practice is to: + * - Use entity queries over entity loading to check existence. For example, use + * the \Drupal\entity_usage\EntityUsageTrackBase::checkAndPrepareEntityIds() + * helper method to do this. + * - If the field you are tracking supports multiple entities then check the + * existence for all entities are the same time. For example, use + * the \Drupal\entity_usage\EntityUsageTrackBase::checkAndPrepareEntityIds() + * helper method to do this. + * - Before doing any entity queries or entity loading check that the entity + * type is being tracked. The helper method + * \Drupal\entity_usage\EntityUsageTrackBase::isEntityTypeTracked() can do + * this. + * - If the plugin can be coded to process multiple cardinality fields + * efficiently, implement + * \Drupal\entity_usage\EntityUsageTrackMultipleLoadInterface to process all + * field values together. See + * \Drupal\entity_usage\Plugin\EntityUsage\Track\EntityReference as an + * example. */ interface EntityUsageTrackInterface extends PluginInspectionInterface { diff --git a/src/Plugin/EntityUsage/Track/DynamicEntityReference.php b/src/Plugin/EntityUsage/Track/DynamicEntityReference.php index 7f79730ebc564389e40a6283dc624797437e9621..6c8854791450370f5703273bbf919997e0cc96ff 100644 --- a/src/Plugin/EntityUsage/Track/DynamicEntityReference.php +++ b/src/Plugin/EntityUsage/Track/DynamicEntityReference.php @@ -3,7 +3,9 @@ namespace Drupal\entity_usage\Plugin\EntityUsage\Track; use Drupal\Core\Field\FieldItemInterface; +use Drupal\Core\Field\FieldItemListInterface; use Drupal\entity_usage\EntityUsageTrackBase; +use Drupal\entity_usage\EntityUsageTrackMultipleLoadInterface; /** * Tracks usage of entities related in dynamic_entity_reference fields. @@ -15,31 +17,60 @@ use Drupal\entity_usage\EntityUsageTrackBase; * field_types = {"dynamic_entity_reference"}, * ) */ -class DynamicEntityReference extends EntityUsageTrackBase { +class DynamicEntityReference extends EntityUsageTrackBase implements EntityUsageTrackMultipleLoadInterface { /** * {@inheritdoc} */ public function getTargetEntities(FieldItemInterface $item) { - /** @var \Drupal\dynamic_entity_reference\Plugin\Field\FieldType\DynamicEntityReferenceItem $item */ - $target_id = $item->get('target_id')->getValue(); - $target_type_id = $item->get('target_type')->getValue(); - if (empty($target_id) || empty($target_type_id)) { - return []; + /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item */ + return $this->doGetTargetEntities($item->getParent(), $item); + } + + /** + * {@inheritdoc} + */ + public function getTargetEntitiesFromField(FieldItemListInterface $field): array { + return $this->doGetTargetEntities($field); + } + + /** + * Retrieve the target entity(ies) from a field. + * + * @param \Drupal\Core\Field\FieldItemListInterface $field + * The field to get the target entity(ies) from. + * @param \Drupal\Core\Field\FieldItemInterface|null $field_item + * (optional) The field item to get the target entity(ies) from. + * + * @return string[] + * An indexed array of strings where each target entity type and ID are + * concatenated with a "|" character. Will return an empty array if no + * target entity could be retrieved from the received field item value. + */ + private function doGetTargetEntities(FieldItemListInterface $field, ?FieldItemInterface $field_item = NULL): array { + $entity_ids = []; + if ($field_item instanceof FieldItemInterface) { + $iterable = [$field_item]; } - // Check if target entity type is enabled, all entity types are enabled by - // default. - if (!$this->isEntityTypeTracked($target_type_id)) { - return []; + else { + $iterable = &$field; + } + + foreach ($iterable as $item) { + /** @var \Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem $item */ + $item_value = $item->get('target_id')->getValue(); + $target_type_id = $item->get('target_type')->getValue(); + + if (!empty($item_value)) { + $entity_ids[$target_type_id][] = $item_value; + } + } + + $return = []; + foreach ($entity_ids as $target_type_id => $entity_id_values) { + $return = array_merge($return, $this->checkAndPrepareEntityIds($target_type_id, $entity_id_values, 'id')); } - $target_type = $this->entityTypeManager->getDefinition($target_type_id); - // Only return a valid result if the target entity exists. - $query = $this->entityTypeManager->getStorage($target_type_id) - ->getQuery() - ->accessCheck(FALSE) - ->condition($target_type->getKey('id'), $target_id) - ->count(); - return $query->execute() > 0 ? [$target_type_id . '|' . $target_id] : []; + return $return; } } diff --git a/src/Plugin/EntityUsage/Track/EntityReference.php b/src/Plugin/EntityUsage/Track/EntityReference.php index 1075f60bcb3c703efb2652d23a51c8043049a537..6b47a3c853299d56359e667e1c6c3f1f5ed2fcfc 100644 --- a/src/Plugin/EntityUsage/Track/EntityReference.php +++ b/src/Plugin/EntityUsage/Track/EntityReference.php @@ -79,17 +79,7 @@ class EntityReference extends EntityUsageTrackBase implements EntityUsageTrackMu } } - if (empty($entity_ids)) { - return []; - } - - $target_type = $this->entityTypeManager->getDefinition($target_type_id); - // Only return a valid result if the target entity exists. - $query = $this->entityTypeManager->getStorage($target_type_id) - ->getQuery() - ->accessCheck(FALSE) - ->condition($target_type->getKey('id'), $entity_ids, 'IN'); - return array_values(array_map(fn ($id) => $target_type_id . '|' . $id, $query->execute())); + return $this->checkAndPrepareEntityIds($target_type_id, $entity_ids, 'id'); } } diff --git a/src/Plugin/EntityUsage/Track/LayoutBuilder.php b/src/Plugin/EntityUsage/Track/LayoutBuilder.php index 50ab2598577a71244768b7770ac415c497bf70c4..03e1045510a2492874aba917323eb94a098a63fb 100644 --- a/src/Plugin/EntityUsage/Track/LayoutBuilder.php +++ b/src/Plugin/EntityUsage/Track/LayoutBuilder.php @@ -147,10 +147,10 @@ class LayoutBuilder extends EntityUsageTrackBase { $target_entities = []; if (count($blockContentRevisionIds) > 0) { - $target_entities = $this->prepareEntityIds('block_content', $blockContentRevisionIds, 'revision'); + $target_entities = $this->checkAndPrepareEntityIds('block_content', $blockContentRevisionIds, 'revision'); } if (count($blockContentUuids) > 0) { - $target_entities = array_merge($target_entities, $this->prepareEntityIds('block_content', $blockContentUuids, 'uuid')); + $target_entities = array_merge($target_entities, $this->checkAndPrepareEntityIds('block_content', $blockContentUuids, 'uuid')); } if (count($ebbContentIds) > 0) { $target_entities = array_merge($target_entities, $this->prepareEntityBrowserBlockIds($ebbContentIds)); @@ -162,36 +162,6 @@ class LayoutBuilder extends EntityUsageTrackBase { } - /** - * Prepare target entity values to be in the correct format. - * - * @param string $entityTypeId - * The entity type ID. - * @param array $ids - * An array of entity IDs, can be revision or UUIDs as well. - * @param string $idField - * The ID field; UUID or revision or id. - * - * @return array - * An array of the corresponding entity IDs from the IDs passed in, - * each prefixed with the string "$entityTypeId|". Non-loadable entities - * will be filtered out. - */ - private function prepareEntityIds(string $entityTypeId, array $ids, string $idField) { - if (!$this->isEntityTypeTracked($entityTypeId)) { - return []; - } - $storage = $this->entityTypeManager->getStorage($entityTypeId); - - /** @var \Drupal\block_content\BlockContentInterface[] $blockContent */ - $ids = $storage->getQuery() - ->accessCheck(FALSE) - ->condition($storage->getEntityType()->getKey($idField), $ids, 'IN') - ->execute(); - - return array_map(fn ($id) => $entityTypeId . '|' . $id, $ids); - } - /** * Prepare Entity Browser Block IDs to be in the correct format. * @@ -219,7 +189,7 @@ class LayoutBuilder extends EntityUsageTrackBase { // Return items in the expected format, separating type and id with a "|". $return = array_merge( $return, - $this->prepareEntityIds($entity_type_id, $entity_ids, 'id') + $this->checkAndPrepareEntityIds($entity_type_id, $entity_ids, 'id') ); } @@ -255,7 +225,7 @@ class LayoutBuilder extends EntityUsageTrackBase { foreach ($ids as $entity_type_id => $entity_uuids) { $return = array_merge( $return, - $this->prepareEntityIds($entity_type_id, $entity_uuids, 'uuid') + $this->checkAndPrepareEntityIds($entity_type_id, $entity_uuids, 'uuid') ); }