From dbf04ac7c8e5825d31e3fc34488884da7e45a89e Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Wed, 16 Apr 2025 16:35:31 +0200 Subject: [PATCH 01/14] Add tracker plugin to register usage in paragraphs on the host --- src/EntityUpdateManager.php | 7 +- src/EntityUpdateManagerInterface.php | 8 ++ src/EntityUsageTrackBase.php | 2 +- src/Plugin/EntityUsage/Track/Paragraph.php | 116 +++++++++++++++++++++ 4 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/Plugin/EntityUsage/Track/Paragraph.php diff --git a/src/EntityUpdateManager.php b/src/EntityUpdateManager.php index 71e1768..f8717ca 100644 --- a/src/EntityUpdateManager.php +++ b/src/EntityUpdateManager.php @@ -215,12 +215,9 @@ class EntityUpdateManager implements EntityUpdateManagerInterface { } /** - * Gets the enabled tracking plugins, all plugins are enabled by default. - * - * @return array<string, \Drupal\entity_usage\EntityUsageTrackInterface> - * The enabled plugin instances keyed by plugin ID. + * {@inheritdoc} */ - protected function getEnabledPlugins(): array { + public function getEnabledPlugins(): array { $all_plugin_ids = array_keys($this->trackManager->getDefinitions()); $enabled_plugins = $this->config->get('track_enabled_plugins'); $enabled_plugin_ids = is_array($enabled_plugins) ? $enabled_plugins : $all_plugin_ids; diff --git a/src/EntityUpdateManagerInterface.php b/src/EntityUpdateManagerInterface.php index 79e2d1d..414ea57 100644 --- a/src/EntityUpdateManagerInterface.php +++ b/src/EntityUpdateManagerInterface.php @@ -43,4 +43,12 @@ interface EntityUpdateManagerInterface { */ public function trackUpdateOnDeletion(EntityInterface $entity, $type = 'default'): void; + /** + * Gets the enabled tracking plugins, all plugins are enabled by default. + * + * @return array<string, \Drupal\entity_usage\EntityUsageTrackInterface> + * The enabled plugin instances keyed by plugin ID. + */ + public function getEnabledPlugins(): array; + } diff --git a/src/EntityUsageTrackBase.php b/src/EntityUsageTrackBase.php index eddcf77..0d0fdc6 100644 --- a/src/EntityUsageTrackBase.php +++ b/src/EntityUsageTrackBase.php @@ -566,7 +566,7 @@ abstract class EntityUsageTrackBase extends PluginBase implements EntityUsageTra * @param string $field_name * The field name that caused the exception. */ - private function logTrackingException(\Exception $e, EntityInterface $source_entity, string $field_name): void { + protected function logTrackingException(\Exception $e, EntityInterface $source_entity, string $field_name): void { Error::logException( $this->entityUsageLogger, $e, diff --git a/src/Plugin/EntityUsage/Track/Paragraph.php b/src/Plugin/EntityUsage/Track/Paragraph.php new file mode 100644 index 0000000..a2f331d --- /dev/null +++ b/src/Plugin/EntityUsage/Track/Paragraph.php @@ -0,0 +1,116 @@ +<?php + +namespace Drupal\entity_usage\Plugin\EntityUsage\Track; + +use Drupal\Core\Entity\TranslatableInterface; +use Drupal\Core\Field\FieldItemInterface; +use Drupal\Core\Field\FieldItemListInterface; +use Drupal\entity_usage\EntityUpdateManagerInterface; +use Drupal\entity_usage\EntityUsageTrackMultipleLoadInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Tracks usage of entities referenced in referenced paragraphs. + * + * @EntityUsageTrack( + * id = "paragraph", + * label = @Translation("Paragraph"), + * description = @Translation("Tracks relationships created within referenced paragraphs."), + * field_types = { + * "entity_reference_revisions", + * }, + * source_entity_class = "Drupal\Core\Entity\FieldableEntityInterface", + * ) + */ +class Paragraph extends EntityReference implements EntityUsageTrackMultipleLoadInterface { + + /** + * The entity update manager. + */ + protected EntityUpdateManagerInterface $entityUpdateManager; + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { + $tracker = parent::create($container, $configuration, $plugin_id, $plugin_definition); + $tracker->entityUpdateManager = $container->get('entity_usage.entity_update_manager'); + return $tracker; + } + + /** + * {@inheritdoc} + */ + public function getTargetEntities(FieldItemInterface $item): array { + return $this->doGetTargetEntities($item->getParent(), $item); + } + + /** + * {@inheritdoc} + */ + public function getTargetEntitiesFromField(FieldItemListInterface $field): array { + return $this->doGetTargetEntities($field); + } + + /** + * {@inheritdoc} + */ + private function doGetTargetEntities(FieldItemListInterface $field, ?FieldItemInterface $field_item = NULL): array { + $target_entities = []; + + $target_type_id = $field->getFieldDefinition()->getSetting('target_type'); + if ($target_type_id !== 'paragraph') { + return $target_entities; + } + + /** @var \Drupal\Core\Entity\EntityInterface $parent_entity */ + $parent_entity = $field->getParent()->getValue(); + $referenced_paragraphs = $field_item instanceof FieldItemInterface + ? array_filter([$field_item->entity]) + : $field->referencedEntities(); + foreach ($referenced_paragraphs as $referenced_paragraph) { + // @todo Is this logic correct? + if ($referenced_paragraph instanceof TranslatableInterface + && $referenced_paragraph->language()->getId() !== $parent_entity->language()->getId() + && $referenced_paragraph->hasTranslation($parent_entity->language()->getId()) + ) { + $referenced_paragraph = $referenced_paragraph->getTranslation($parent_entity->language()->getId()); + } + + foreach ($this->entityUpdateManager->getEnabledPlugins() as $plugin) { + $trackable_field_types = $plugin->getApplicableFieldTypes(); + $fields = array_keys($this->getReferencingFields($referenced_paragraph, $trackable_field_types)); + foreach ($fields as $field_name) { + if (!$referenced_paragraph->hasField($field_name) || $referenced_paragraph->{$field_name}->isEmpty()) { + continue; + } + + try { + if ($plugin instanceof EntityUsageTrackMultipleLoadInterface) { + $target_entities = array_merge( + $target_entities, + $plugin->getTargetEntitiesFromField($referenced_paragraph->{$field_name}), + ); + } + else { + /** @var \Drupal\Core\Field\FieldItemInterface $field_item */ + foreach ($referenced_paragraph->get($field_name) as $field_item) { + $target_entities = array_merge( + $target_entities, + $plugin->getTargetEntities($field_item), + ); + } + } + } + catch (\Exception $e) { + $this->logTrackingException($e, $referenced_paragraph, $field_name); + continue; + } + } + } + } + + return $target_entities; + } + +} -- GitLab From ed12bab6cbf5633620534010095a946bbc60e68d Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 17 Apr 2025 07:17:30 +0200 Subject: [PATCH 02/14] Rename tracker plugin to make it more clear what it does --- .../Track/{Paragraph.php => ParagraphInherited.php} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename src/Plugin/EntityUsage/Track/{Paragraph.php => ParagraphInherited.php} (91%) diff --git a/src/Plugin/EntityUsage/Track/Paragraph.php b/src/Plugin/EntityUsage/Track/ParagraphInherited.php similarity index 91% rename from src/Plugin/EntityUsage/Track/Paragraph.php rename to src/Plugin/EntityUsage/Track/ParagraphInherited.php index a2f331d..34afc26 100644 --- a/src/Plugin/EntityUsage/Track/Paragraph.php +++ b/src/Plugin/EntityUsage/Track/ParagraphInherited.php @@ -13,16 +13,16 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * Tracks usage of entities referenced in referenced paragraphs. * * @EntityUsageTrack( - * id = "paragraph", - * label = @Translation("Paragraph"), - * description = @Translation("Tracks relationships created within referenced paragraphs."), + * id = "paragraph_inherited", + * label = @Translation("Paragraph (inherited)"), + * description = @Translation("Tracks entity relationships found inside direct and nested paragraphs as if they belong to the root parent entity."), * field_types = { * "entity_reference_revisions", * }, * source_entity_class = "Drupal\Core\Entity\FieldableEntityInterface", * ) */ -class Paragraph extends EntityReference implements EntityUsageTrackMultipleLoadInterface { +class ParagraphInherited extends EntityReference implements EntityUsageTrackMultipleLoadInterface { /** * The entity update manager. -- GitLab From c97e2c71f37f2e34ab5a181389ae2cf1c39f7d23 Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 17 Apr 2025 15:57:11 +0200 Subject: [PATCH 03/14] Take direct paragraph updates into account --- entity_usage.module | 4 + entity_usage.services.yml | 10 + src/ParagraphInheritedUsageUpdater.php | 173 ++++++++++++++++++ .../EntityUsage/Track/ParagraphInherited.php | 13 +- 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 src/ParagraphInheritedUsageUpdater.php diff --git a/entity_usage.module b/entity_usage.module index 2597e34..c4bf296 100644 --- a/entity_usage.module +++ b/entity_usage.module @@ -57,6 +57,7 @@ function entity_usage_entity_insert(EntityInterface $entity): void { * Implements hook_entity_update(). */ function entity_usage_entity_update(EntityInterface $entity): void { + \Drupal::service('entity_usage.paragraph_inherited_usage_updater')->queueEntity($entity); $entity_usage_update_manager = \Drupal::service('entity_usage.entity_update_manager'); assert($entity_usage_update_manager instanceof EntityUpdateManagerInterface); $entity_usage_update_manager->trackUpdateOnEdition($entity); @@ -70,6 +71,7 @@ function entity_usage_entity_update(EntityInterface $entity): void { * Implements hook_entity_predelete(). */ function entity_usage_entity_predelete(EntityInterface $entity): void { + \Drupal::service('entity_usage.paragraph_inherited_usage_updater')->queueEntity($entity); $entity_usage_update_manager = \Drupal::service('entity_usage.entity_update_manager'); assert($entity_usage_update_manager instanceof EntityUpdateManagerInterface); $entity_usage_update_manager->trackUpdateOnDeletion($entity); @@ -79,6 +81,7 @@ function entity_usage_entity_predelete(EntityInterface $entity): void { * Implements hook_entity_translation_delete(). */ function entity_usage_entity_translation_delete(EntityInterface $translation): void { + \Drupal::service('entity_usage.paragraph_inherited_usage_updater')->queueEntity($translation); $entity_usage_update_manager = \Drupal::service('entity_usage.entity_update_manager'); assert($entity_usage_update_manager instanceof EntityUpdateManagerInterface); $entity_usage_update_manager->trackUpdateOnDeletion($translation, 'translation'); @@ -88,6 +91,7 @@ function entity_usage_entity_translation_delete(EntityInterface $translation): v * Implements hook_entity_revision_delete(). */ function entity_usage_entity_revision_delete(EntityInterface $entity): void { + \Drupal::service('entity_usage.paragraph_inherited_usage_updater')->queueEntity($entity); $entity_usage_update_manager = \Drupal::service('entity_usage.entity_update_manager'); assert($entity_usage_update_manager instanceof EntityUpdateManagerInterface); $entity_usage_update_manager->trackUpdateOnDeletion($entity, 'revision'); diff --git a/entity_usage.services.yml b/entity_usage.services.yml index 8103e64..0bf49c1 100644 --- a/entity_usage.services.yml +++ b/entity_usage.services.yml @@ -50,6 +50,7 @@ services: Drupal\entity_usage\PreSaveUrlRecorder: ~ Drupal\entity_usage\RecreateTrackingDataForFieldQueuer: ~ + entity_usage.recreate_tracking_data_for_field_queuer: '@Drupal\entity_usage\RecreateTrackingDataForFieldQueuer' # By default we enable Entity and Language subscribers to the # \Drupal\entity_usage\Events\Events::URL_TO_ENTITY event. If the file and/or @@ -57,3 +58,12 @@ services: # \Drupal\entity_usage\EntityUsageServiceProvider::register(). Drupal\entity_usage\UrlToEntityIntegrations\EntityRouting: ~ Drupal\entity_usage\UrlToEntityIntegrations\LanguageIntegration: ~ + + entity_usage.paragraph_inherited_usage_updater: + class: Drupal\entity_usage\ParagraphInheritedUsageUpdater + arguments: + - '@entity_type.manager' + - '@plugin.manager.entity_usage.track' + - '@entity_usage.recreate_tracking_data_for_field_queuer' + tags: + - { name: needs_destruction } diff --git a/src/ParagraphInheritedUsageUpdater.php b/src/ParagraphInheritedUsageUpdater.php new file mode 100644 index 0000000..7cca29a --- /dev/null +++ b/src/ParagraphInheritedUsageUpdater.php @@ -0,0 +1,173 @@ +<?php + +namespace Drupal\entity_usage; + +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\RevisionableStorageInterface; +use Drupal\paragraphs\ParagraphInterface; + +/** + * Updates usage info for outer host entities of paragraphs. + * + * This service complements the "paragraph_inherited" entity usage tracker + * plugin, which automatically updates usage when a host entity is modified. + * + * When a paragraph is updated directly — without triggering an update + * on its host entity — this service makes sure the host entity's usage data + * is updated accordingly. + * + * @internal + */ +class ParagraphInheritedUsageUpdater { + + /** + * ParagraphInheritedUsageUpdater constructor. + */ + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + protected EntityUsageTrackManager $entityUsageTrackManager, + protected RecreateTrackingDataForFieldQueuer $recreateTrackingDataForFieldQueuer, + ) {} + + /** + * A list of paragraph revision IDs for which the usage must be recalculated. + * + * @var int[] + */ + protected array $paragraphRevisionIds = []; + + /** + * Records a paragraph's revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to record. + */ + public function queueEntity(EntityInterface $entity): void { + if ($entity instanceof ParagraphInterface) { + $this->paragraphRevisionIds[$entity->getRevisionId()] = $entity->getRevisionId(); + } + } + + /** + * Removes a paragraph revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The paragraph revision to remove. + */ + public function removeEntityFromQueue(EntityInterface $entity): void { + if ($entity instanceof ParagraphInterface) { + unset($this->paragraphRevisionIds[$entity->getRevisionId()]); + } + } + + /** + * {@inheritdoc} + */ + public function destruct() { + if (empty($this->paragraphRevisionIds)) { + return; + } + $paragraph_revisions = $this + ->entityTypeManager + ->getStorage('paragraph') + ->loadMultipleRevisions($this->paragraphRevisionIds); + foreach ($paragraph_revisions as $paragraph_revision) { + $outer_host_revisions = $this->getParagraphOuterHostRevisions($paragraph_revision); + foreach ($outer_host_revisions as $outer_host_revision) { + $this + ->recreateTrackingDataForFieldQueuer + ->addRecord( + $outer_host_revision->getEntityTypeId(), + $outer_host_revision->id(), + $outer_host_revision->getRevisionId(), + 'paragraph_inherited', + $paragraph_revision->get('parent_field_name')->value, + ); + } + } + } + + /** + * Gets the outermost host entity of a paragraph. + * + * Walks up the hierarchy until the top-level host is found. + * + * @param \Drupal\paragraphs\ParagraphInterface $paragraph_revision + * The paragraph revision. + * + * @return \Drupal\Core\Entity\EntityInterface[]|null + * An array of entity revisions keyed by their revision ID, or an empty + * array if none found. + */ + public function getParagraphOuterHostRevisions(ParagraphInterface $paragraph_revision): array|null { + $outer_host_revisions = []; + + $parent_revisions = $this->getParagraphParentRevisions($paragraph_revision); + while (!empty($parent_revisions)) { + foreach ($parent_revisions as $key => $parent_revision) { + if (!$parent_revision instanceof ParagraphInterface) { + $outer_host_revisions[] = $parent_revision; + unset($parent_revisions[$key]); + } + } + + if (empty($parent_revisions)) { + break; + } + + $next_parent_revisions = []; + foreach ($parent_revisions as $parent_revision) { + $next_parent_revisions = array_merge( + $next_parent_revisions, + $this->getParagraphParentRevisions($parent_revision), + ); + } + + $parent_revisions = array_filter($next_parent_revisions); + } + + return $outer_host_revisions; + } + + /** + * Gets all revisions that reference the given paragraph revision. + * + * @param \Drupal\paragraphs\ParagraphInterface $paragraph_revision + * The paragraph revision. + * + * @return \Drupal\Core\Entity\EntityInterface[]|null + * An array of entity revisions keyed by their revision ID, or an empty + * array if none found. + */ + public function getParagraphParentRevisions(ParagraphInterface $paragraph_revision): array|null { + $parent_entity_type_id = $paragraph_revision->get('parent_type')->value; + $parent_field_name = $paragraph_revision->get('parent_field_name')->value; + if (empty($parent_entity_type_id) || empty($parent_field_name)) { + return NULL; + } + + $parent_entity_type = $this + ->entityTypeManager + ->getDefinition($parent_entity_type_id); + + $parent_entity_storage = $this + ->entityTypeManager + ->getStorage($parent_entity_type->id()); + + $parent_revision_ids = $parent_entity_storage + ->getQuery() + ->accessCheck(FALSE) + ->allRevisions() + ->condition("{$parent_field_name}.target_revision_id", $paragraph_revision->getRevisionId()) + ->execute(); + if (empty($parent_revision_ids)) { + return []; + } + + return $parent_entity_type->isRevisionable() && $parent_entity_storage instanceof RevisionableStorageInterface + ? $parent_entity_storage->loadMultipleRevisions(array_keys($parent_revision_ids)) + : $parent_entity_storage->loadMultiple($parent_revision_ids); + } + +} diff --git a/src/Plugin/EntityUsage/Track/ParagraphInherited.php b/src/Plugin/EntityUsage/Track/ParagraphInherited.php index 34afc26..a4b2acf 100644 --- a/src/Plugin/EntityUsage/Track/ParagraphInherited.php +++ b/src/Plugin/EntityUsage/Track/ParagraphInherited.php @@ -7,6 +7,7 @@ use Drupal\Core\Field\FieldItemInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\entity_usage\EntityUpdateManagerInterface; use Drupal\entity_usage\EntityUsageTrackMultipleLoadInterface; +use Drupal\entity_usage\ParagraphInheritedUsageUpdater; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -15,7 +16,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * @EntityUsageTrack( * id = "paragraph_inherited", * label = @Translation("Paragraph (inherited)"), - * description = @Translation("Tracks entity relationships found inside direct and nested paragraphs as if they belong to the root parent entity."), + * description = @Translation("Tracks entity relationships found inside direct and nested paragraphs as if they belong to the (outer) host entity."), * field_types = { * "entity_reference_revisions", * }, @@ -29,6 +30,11 @@ class ParagraphInherited extends EntityReference implements EntityUsageTrackMult */ protected EntityUpdateManagerInterface $entityUpdateManager; + /** + * The paragraph inherited usage updated. + */ + protected ParagraphInheritedUsageUpdater $paragraphInheritedUsageUpdater; + /** * {@inheritdoc} */ @@ -65,6 +71,7 @@ class ParagraphInherited extends EntityReference implements EntityUsageTrackMult /** @var \Drupal\Core\Entity\EntityInterface $parent_entity */ $parent_entity = $field->getParent()->getValue(); + /** @var \Drupal\paragraphs\ParagraphInterface[] $referenced_paragraphs */ $referenced_paragraphs = $field_item instanceof FieldItemInterface ? array_filter([$field_item->entity]) : $field->referencedEntities(); @@ -108,6 +115,10 @@ class ParagraphInherited extends EntityReference implements EntityUsageTrackMult } } } + + $this + ->paragraphInheritedUsageUpdater + ->removeEntityFromQueue($referenced_paragraph); } return $target_entities; -- GitLab From 459c0c82dc4f6f03488c3863d43c573e94926486 Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 17 Apr 2025 17:07:48 +0200 Subject: [PATCH 04/14] Actually initialize paragraphInheritedUsageUpdater --- src/Plugin/EntityUsage/Track/ParagraphInherited.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Plugin/EntityUsage/Track/ParagraphInherited.php b/src/Plugin/EntityUsage/Track/ParagraphInherited.php index a4b2acf..fe15a7c 100644 --- a/src/Plugin/EntityUsage/Track/ParagraphInherited.php +++ b/src/Plugin/EntityUsage/Track/ParagraphInherited.php @@ -41,6 +41,7 @@ class ParagraphInherited extends EntityReference implements EntityUsageTrackMult public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static { $tracker = parent::create($container, $configuration, $plugin_id, $plugin_definition); $tracker->entityUpdateManager = $container->get('entity_usage.entity_update_manager'); + $tracker->paragraphInheritedUsageUpdater = $container->get('entity_usage.paragraph_inherited_usage_updater'); return $tracker; } -- GitLab From cff58e0c2b5e969578880c5ae0279e6e2e823ded Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 17 Apr 2025 17:26:22 +0200 Subject: [PATCH 05/14] Add more documentation --- src/Plugin/EntityUsage/Track/ParagraphInherited.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Plugin/EntityUsage/Track/ParagraphInherited.php b/src/Plugin/EntityUsage/Track/ParagraphInherited.php index fe15a7c..2410971 100644 --- a/src/Plugin/EntityUsage/Track/ParagraphInherited.php +++ b/src/Plugin/EntityUsage/Track/ParagraphInherited.php @@ -117,6 +117,10 @@ class ParagraphInherited extends EntityReference implements EntityUsageTrackMult } } + // Since this paragraph has now been processed, we can safely remove it + // from the usage updater queue. This prevents the usage tracking from + // happening multiple times (for example, when a user saves a paragraph + // through the paragraphs field widget). $this ->paragraphInheritedUsageUpdater ->removeEntityFromQueue($referenced_paragraph); -- GitLab From 4b5de81b5ac9c5356270bf0b61660e42bc0738d2 Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 17 Apr 2025 18:39:52 +0200 Subject: [PATCH 06/14] Fix some return types --- src/ParagraphInheritedUsageUpdater.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ParagraphInheritedUsageUpdater.php b/src/ParagraphInheritedUsageUpdater.php index 7cca29a..a4cd5b4 100644 --- a/src/ParagraphInheritedUsageUpdater.php +++ b/src/ParagraphInheritedUsageUpdater.php @@ -96,11 +96,11 @@ class ParagraphInheritedUsageUpdater { * @param \Drupal\paragraphs\ParagraphInterface $paragraph_revision * The paragraph revision. * - * @return \Drupal\Core\Entity\EntityInterface[]|null + * @return \Drupal\Core\Entity\EntityInterface[] * An array of entity revisions keyed by their revision ID, or an empty * array if none found. */ - public function getParagraphOuterHostRevisions(ParagraphInterface $paragraph_revision): array|null { + public function getParagraphOuterHostRevisions(ParagraphInterface $paragraph_revision): array { $outer_host_revisions = []; $parent_revisions = $this->getParagraphParentRevisions($paragraph_revision); @@ -136,15 +136,15 @@ class ParagraphInheritedUsageUpdater { * @param \Drupal\paragraphs\ParagraphInterface $paragraph_revision * The paragraph revision. * - * @return \Drupal\Core\Entity\EntityInterface[]|null + * @return \Drupal\Core\Entity\EntityInterface[] * An array of entity revisions keyed by their revision ID, or an empty * array if none found. */ - public function getParagraphParentRevisions(ParagraphInterface $paragraph_revision): array|null { + public function getParagraphParentRevisions(ParagraphInterface $paragraph_revision): array { $parent_entity_type_id = $paragraph_revision->get('parent_type')->value; $parent_field_name = $paragraph_revision->get('parent_field_name')->value; if (empty($parent_entity_type_id) || empty($parent_field_name)) { - return NULL; + return []; } $parent_entity_type = $this -- GitLab From 35327b878e2204194f69ce3b3e5c52221063fd63 Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Fri, 18 Apr 2025 13:38:34 +0200 Subject: [PATCH 07/14] A few fixes + allow to derive hierarchy from field name stored in entity usage table --- src/EntityUsage.php | 39 +++++-- src/EntityUsageInterface.php | 15 ++- src/EntityUsageTrackBase.php | 101 +++++++++++++++--- src/EntityUsageTrackInterface.php | 11 +- src/ParagraphInheritedUsageUpdater.php | 63 ++++++----- .../EntityUsage/Track/ParagraphInherited.php | 30 ++++-- 6 files changed, 193 insertions(+), 66 deletions(-) diff --git a/src/EntityUsage.php b/src/EntityUsage.php index 1a8a371..43f825f 100644 --- a/src/EntityUsage.php +++ b/src/EntityUsage.php @@ -469,28 +469,55 @@ class EntityUsage implements EntityUsageBulkInterface { /** * {@inheritdoc} */ - public function listTargetEntitiesByFieldAndMethod(string|int $source_id, string $source_entity_type_id, string $source_langcode, string|int $source_vid, string $method, string $field_name): array { + public function listTargetEntitiesByFieldAndMethod(string|int $source_id, string $source_entity_type_id, string $source_langcode, string|int $source_vid, string $method, string $field_name, bool $return_as_arrays = FALSE): array { // Entities can have string IDs. We support that by using different columns // on each case. $source_id_column = $this->isInt($source_id) ? 'source_id' : 'source_id_string'; $query = $this->connection->select($this->tableName, 'e') - ->fields('e', [ + ->fields('e', array_filter([ 'target_id', 'target_id_string', 'target_type', - ]) + $return_as_arrays ? 'method' : NULL, + $return_as_arrays ? 'field_name' : NULL, + ])) ->condition($source_id_column, $source_id) ->condition('source_type', $source_entity_type_id) ->condition('source_vid', $source_vid ?: 0) - ->condition('field_name', $field_name) - ->condition('method', $method) ->condition('count', 0, '>') ->orderBy('target_id', 'DESC'); + $query->condition( + $query + ->orConditionGroup() + ->condition('method', $method) + ->condition('method', "{$method}|%", 'LIKE') + ->condition('method', "%|{$method}", 'LIKE') + ->condition('method', "%|{$method}|%", 'LIKE') + ); + + $query->condition( + $query + ->orConditionGroup() + ->condition('field_name', $field_name) + ->condition('field_name', "{$field_name}|%", 'LIKE') + ->condition('field_name', "%|{$field_name}", 'LIKE') + ->condition('field_name', "%|{$field_name}|%", 'LIKE') + ->condition('field_name', "%|%:{$field_name}", 'LIKE') + ->condition('field_name', "%|%:{$field_name}|%", 'LIKE') + ); + $entities = []; foreach ($query->execute() as $usage) { $target_id_value = !empty($usage->target_id) ? $usage->target_id : $usage->target_id_string; - $entities[] = $usage->target_type . '|' . $target_id_value; + $entities[] = !$return_as_arrays + ? $usage->target_type . '|' . $target_id_value + : [ + 'target_type' => $usage->target_type, + 'target_id' => $target_id_value, + 'method' => $usage->method, + 'field_name' => $usage->field_name, + ]; } return $entities; } diff --git a/src/EntityUsageInterface.php b/src/EntityUsageInterface.php index 4d93183..bc47c2f 100644 --- a/src/EntityUsageInterface.php +++ b/src/EntityUsageInterface.php @@ -242,14 +242,19 @@ interface EntityUsageInterface { * the plugin id. * @param string $field_name * The field name. + * @param bool $return_as_arrays + * If TRUE, the target entities are returned as arrays. * - * @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 entities found. + * @return array + * An indexed array where each value is: + * - if $return_as_arrays = FALSE: a string of the target entity type and ID + * concatenated with a "|" character. + * - if $return_as_arrays = TRUE: an array with the target_type, target_id, + * method and field_name keys. + * Will return an empty array if no target entities found. * * @see \Drupal\entity_usage\EntityUsageTrackInterface::getTargetEntities() */ - public function listTargetEntitiesByFieldAndMethod(string|int $source_id, string $source_entity_type_id, string $source_langcode, string|int $source_vid, string $method, string $field_name): array; + public function listTargetEntitiesByFieldAndMethod(string|int $source_id, string $source_entity_type_id, string $source_langcode, string|int $source_vid, string $method, string $field_name, bool $return_as_arrays = FALSE): array; } diff --git a/src/EntityUsageTrackBase.php b/src/EntityUsageTrackBase.php index 0d0fdc6..750db54 100644 --- a/src/EntityUsageTrackBase.php +++ b/src/EntityUsageTrackBase.php @@ -241,13 +241,22 @@ abstract class EntityUsageTrackBase extends PluginBase implements EntityUsageTra $this->logTrackingException($e, $source_entity, $field_name); continue; } - // If a field references the same target entity, we record only one - // usage. - $target_entities = array_unique($target_entities); + foreach ($target_entities as $target_entity) { - [$target_type, $target_id] = explode("|", $target_entity); + $normalized_target_entity = $this->normalizeTargetEntity($target_entity); $source_vid = ($source_entity instanceof RevisionableInterface && $source_entity->getRevisionId()) ? $source_entity->getRevisionId() : 0; - $this->usageService->registerUsage($target_id, $target_type, $source_entity->id(), $source_entity->getEntityTypeId(), $source_entity->language()->getId(), $source_vid, $this->pluginId, $field_name); + $this + ->usageService + ->registerUsage( + $normalized_target_entity['target_id'], + $normalized_target_entity['target_type'], + $source_entity->id(), + $source_entity->getEntityTypeId(), + $source_entity->language()->getId(), + $source_vid, + $normalized_target_entity['method'] ?? $this->pluginId, + $normalized_target_entity['field_name'] ?? $field_name, + ); } } } @@ -327,24 +336,64 @@ abstract class EntityUsageTrackBase extends PluginBase implements EntityUsageTra $this->logTrackingException($e, $source_entity, $field_name); } + foreach ($current_targets as $key => $current_target) { + unset($current_targets[$key]); + $current_target = $this->normalizeTargetEntity($current_target); + $new_key = implode('|', array_filter([ + $current_target['target_type'], + $current_target['target_id'], + $current_target['field_name'] ?? $field_name, + ])); + $current_targets[$new_key] = $current_target; + } + $source_entity_langcode = $source_entity->language()->getId(); $source_vid = ($source_entity instanceof RevisionableInterface && $source_entity->getRevisionId()) ? $source_entity->getRevisionId() : 0; - $original_targets = $this->usageService->listTargetEntitiesByFieldAndMethod($source_entity->id(), $source_entity->getEntityTypeId(), $source_entity_langcode, $source_vid, $this->pluginId, $field_name); - - // If a field references the same target entity, we record only one usage. - $original_targets = array_unique($original_targets); - $current_targets = array_unique($current_targets); + $original_targets = $this + ->usageService + ->listTargetEntitiesByFieldAndMethod($source_entity->id(), $source_entity->getEntityTypeId(), $source_entity_langcode, $source_vid, $this->pluginId, $field_name, TRUE); + foreach ($original_targets as $key => $original_target) { + unset($original_targets[$key]); + $original_target = $this->normalizeTargetEntity($original_target); + $new_key = implode('|', array_filter([ + $original_target['target_type'], + $original_target['target_id'], + $original_target['field_name'] ?? $field_name, + ])); + $original_targets[$new_key] = $original_target; + } - $added_ids = array_diff($current_targets, $original_targets); - $removed_ids = array_diff($original_targets, $current_targets); + $added_ids = array_diff_key($current_targets, $original_targets); + $removed_ids = array_diff_key($original_targets, $current_targets); foreach ($added_ids as $added_entity) { - [$target_type, $target_id] = explode('|', $added_entity); - $this->usageService->registerUsage($target_id, $target_type, $source_entity->id(), $source_entity->getEntityTypeId(), $source_entity_langcode, $source_vid, $this->pluginId, $field_name); + $this + ->usageService + ->registerUsage( + $added_entity['target_id'], + $added_entity['target_type'], + $source_entity->id(), + $source_entity->getEntityTypeId(), + $source_entity_langcode, + $source_vid, + $added_entity['method'] ?? $this->pluginId, + $added_entity['field_name'] ?? $field_name, + ); } foreach ($removed_ids as $removed_entity) { - [$target_type, $target_id] = explode('|', $removed_entity); - $this->usageService->registerUsage($target_id, $target_type, $source_entity->id(), $source_entity->getEntityTypeId(), $source_entity_langcode, $source_vid, $this->pluginId, $field_name, 0); + $this + ->usageService + ->registerUsage( + $removed_entity['target_id'], + $removed_entity['target_type'], + $source_entity->id(), + $source_entity->getEntityTypeId(), + $source_entity_langcode, + $source_vid, + $removed_entity['method'] ?? $this->pluginId, + $removed_entity['field_name'] ?? $field_name, + 0 + ); } } @@ -580,4 +629,24 @@ abstract class EntityUsageTrackBase extends PluginBase implements EntityUsageTra ); } + /** + * Normalized a target entity. + * + * @param string|array $target_entity + * The target array, which can either be a string or an array. + * + * @return array + * If the target entity was already an array, it is returned unchanged. If + * it was a string, the target entity is retured as an array with the + * target_type and target_id keys. + */ + protected function normalizeTargetEntity(string|array $target_entity): array { + return is_array($target_entity) + ? $target_entity + : array_combine( + ['target_type', 'target_id'], + explode('|', $target_entity) + ); + } + } diff --git a/src/EntityUsageTrackInterface.php b/src/EntityUsageTrackInterface.php index dac7dbb..eab342f 100644 --- a/src/EntityUsageTrackInterface.php +++ b/src/EntityUsageTrackInterface.php @@ -109,10 +109,19 @@ interface EntityUsageTrackInterface extends PluginInspectionInterface { * @param \Drupal\Core\Field\FieldItemInterface $item * The field item to get the target entity(ies) from. * - * @return string[] + * @return string[]|array[] * 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. + * + * @return array + * An indexed array where each value can be a string or an array: + * - if a string: the target entity type and ID concatenated with a "|" + * character. + * - if an array: an array with the target_type and target_id keys, and + * optionally the method and field_name keys. + * Will return an empty array if no target entities could be retrieved from + * the received field item value. */ public function getTargetEntities(FieldItemInterface $item): array; diff --git a/src/ParagraphInheritedUsageUpdater.php b/src/ParagraphInheritedUsageUpdater.php index a4cd5b4..ec79987 100644 --- a/src/ParagraphInheritedUsageUpdater.php +++ b/src/ParagraphInheritedUsageUpdater.php @@ -73,74 +73,75 @@ class ParagraphInheritedUsageUpdater { ->getStorage('paragraph') ->loadMultipleRevisions($this->paragraphRevisionIds); foreach ($paragraph_revisions as $paragraph_revision) { - $outer_host_revisions = $this->getParagraphOuterHostRevisions($paragraph_revision); + $outer_host_revisions = $this->getParagraphOuterHostRevisionsFieldItemLists($paragraph_revision); foreach ($outer_host_revisions as $outer_host_revision) { $this ->recreateTrackingDataForFieldQueuer ->addRecord( - $outer_host_revision->getEntityTypeId(), - $outer_host_revision->id(), - $outer_host_revision->getRevisionId(), + $outer_host_revision->getEntity()->getEntityTypeId(), + $outer_host_revision->getEntity()->id(), + $outer_host_revision->getEntity()->getRevisionId(), 'paragraph_inherited', - $paragraph_revision->get('parent_field_name')->value, + $outer_host_revision->getName(), ); } } } /** - * Gets the outermost host entity of a paragraph. + * Gets the outermost host entity's field item list for a paragraph. * - * Walks up the hierarchy until the top-level host is found. + * Walks up the hierarchy until the top-level host is found. The field item + * list that starts the chain is returned. * * @param \Drupal\paragraphs\ParagraphInterface $paragraph_revision * The paragraph revision. * - * @return \Drupal\Core\Entity\EntityInterface[] - * An array of entity revisions keyed by their revision ID, or an empty - * array if none found. + * @return \Drupal\Core\Field\FieldItemListInterface[] + * An array of field item lists or an empty array if none found. */ - public function getParagraphOuterHostRevisions(ParagraphInterface $paragraph_revision): array { + public function getParagraphOuterHostRevisionsFieldItemLists(ParagraphInterface $paragraph_revision): array { $outer_host_revisions = []; - $parent_revisions = $this->getParagraphParentRevisions($paragraph_revision); - while (!empty($parent_revisions)) { - foreach ($parent_revisions as $key => $parent_revision) { + $parent_revision_field_item_lists = $this->getParagraphParentRevisionsFieldItemLists($paragraph_revision); + while (!empty($parent_revision_field_item_lists)) { + foreach ($parent_revision_field_item_lists as $key => $parent_revision_field_item_list) { + $parent_revision = $parent_revision_field_item_list->getEntity(); if (!$parent_revision instanceof ParagraphInterface) { - $outer_host_revisions[] = $parent_revision; - unset($parent_revisions[$key]); + $outer_host_revisions[] = $parent_revision_field_item_list; + unset($parent_revision_field_item_lists[$key]); } } - if (empty($parent_revisions)) { + if (empty($parent_revision_field_item_lists)) { break; } - $next_parent_revisions = []; - foreach ($parent_revisions as $parent_revision) { - $next_parent_revisions = array_merge( - $next_parent_revisions, - $this->getParagraphParentRevisions($parent_revision), + $next_parent_revision_field_item_lists = []; + foreach ($parent_revision_field_item_lists as $parent_revision_field_item_list) { + $next_parent_revision_field_item_lists = array_merge( + $next_parent_revision_field_item_lists, + $this->getParagraphParentRevisionsFieldItemLists($parent_revision_field_item_list->getEntity()), ); } - $parent_revisions = array_filter($next_parent_revisions); + $parent_revision_field_item_lists = array_filter($next_parent_revision_field_item_lists); } return $outer_host_revisions; } /** - * Gets all revisions that reference the given paragraph revision. + * Gets all field item lists that reference the given paragraph revision. * * @param \Drupal\paragraphs\ParagraphInterface $paragraph_revision * The paragraph revision. * - * @return \Drupal\Core\Entity\EntityInterface[] - * An array of entity revisions keyed by their revision ID, or an empty - * array if none found. + * @return \Drupal\Core\Field\FieldItemListInterface[] + * An array of field item lists keyed by the paragraph revision's ID or an + * empty array if none found. */ - public function getParagraphParentRevisions(ParagraphInterface $paragraph_revision): array { + public function getParagraphParentRevisionsFieldItemLists(ParagraphInterface $paragraph_revision): array { $parent_entity_type_id = $paragraph_revision->get('parent_type')->value; $parent_field_name = $paragraph_revision->get('parent_field_name')->value; if (empty($parent_entity_type_id) || empty($parent_field_name)) { @@ -165,9 +166,13 @@ class ParagraphInheritedUsageUpdater { return []; } - return $parent_entity_type->isRevisionable() && $parent_entity_storage instanceof RevisionableStorageInterface + $parent_revisions = $parent_entity_type->isRevisionable() && $parent_entity_storage instanceof RevisionableStorageInterface ? $parent_entity_storage->loadMultipleRevisions(array_keys($parent_revision_ids)) : $parent_entity_storage->loadMultiple($parent_revision_ids); + + return array_map(function (EntityInterface $parent_revision) use ($parent_field_name) { + return $parent_revision->get($parent_field_name); + }, $parent_revisions); } } diff --git a/src/Plugin/EntityUsage/Track/ParagraphInherited.php b/src/Plugin/EntityUsage/Track/ParagraphInherited.php index 2410971..5eaa21e 100644 --- a/src/Plugin/EntityUsage/Track/ParagraphInherited.php +++ b/src/Plugin/EntityUsage/Track/ParagraphInherited.php @@ -6,8 +6,10 @@ use Drupal\Core\Entity\TranslatableInterface; use Drupal\Core\Field\FieldItemInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\entity_usage\EntityUpdateManagerInterface; +use Drupal\entity_usage\EntityUsageTrackBase; use Drupal\entity_usage\EntityUsageTrackMultipleLoadInterface; use Drupal\entity_usage\ParagraphInheritedUsageUpdater; +use Drupal\paragraphs\ParagraphInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -23,7 +25,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * source_entity_class = "Drupal\Core\Entity\FieldableEntityInterface", * ) */ -class ParagraphInherited extends EntityReference implements EntityUsageTrackMultipleLoadInterface { +class ParagraphInherited extends EntityUsageTrackBase implements EntityUsageTrackMultipleLoadInterface { /** * The entity update manager. @@ -93,20 +95,15 @@ class ParagraphInherited extends EntityReference implements EntityUsageTrackMult continue; } + $field_target_entities = []; try { if ($plugin instanceof EntityUsageTrackMultipleLoadInterface) { - $target_entities = array_merge( - $target_entities, - $plugin->getTargetEntitiesFromField($referenced_paragraph->{$field_name}), - ); + $field_target_entities = $plugin->getTargetEntitiesFromField($referenced_paragraph->{$field_name}); } else { /** @var \Drupal\Core\Field\FieldItemInterface $field_item */ foreach ($referenced_paragraph->get($field_name) as $field_item) { - $target_entities = array_merge( - $target_entities, - $plugin->getTargetEntities($field_item), - ); + $field_target_entities = $plugin->getTargetEntities($field_item); } } } @@ -114,6 +111,13 @@ class ParagraphInherited extends EntityReference implements EntityUsageTrackMult $this->logTrackingException($e, $referenced_paragraph, $field_name); continue; } + + foreach ($field_target_entities as $field_target_entity) { + $target_entities[] = $this->normalizeTargetEntity($field_target_entity) + [ + 'method' => "{$this->getPluginId()}|{$plugin->getPluginId()}", + 'field_name' => "{$referenced_paragraph->getRevisionId()}:{$field_name}", + ]; + } } } @@ -126,6 +130,14 @@ class ParagraphInherited extends EntityReference implements EntityUsageTrackMult ->removeEntityFromQueue($referenced_paragraph); } + foreach ($target_entities as &$target_entity) { + $field_name = $field->getName(); + if ($parent_entity instanceof ParagraphInterface) { + $field_name = "{$parent_entity->getRevisionId()}:{$field_name}"; + } + $target_entity['field_name'] = "$field_name|{$target_entity['field_name']}"; + } + return $target_entities; } -- GitLab From 11abb2d84ca1f887f1f6808d49737ab7db0e6fe7 Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Tue, 22 Apr 2025 14:58:05 +0200 Subject: [PATCH 08/14] Increase field name column length --- entity_usage.install | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/entity_usage.install b/entity_usage.install index f573370..c8f4ed2 100644 --- a/entity_usage.install +++ b/entity_usage.install @@ -77,7 +77,7 @@ function entity_usage_schema(): array { 'field_name' => [ 'description' => 'The field in the source entity containing the target entity.', 'type' => 'varchar_ascii', - 'length' => 128, + 'length' => 1024, 'not null' => TRUE, 'default' => '', ], @@ -116,3 +116,21 @@ function entity_usage_schema(): array { function entity_usage_update_last_removed(): int { return 8206; } + +/** + * Increase field_name length in entity_usage table. + */ +function entity_usage_update_8208() { + \Drupal::database()->schema()->changeField( + 'entity_usage', + 'field_name', + 'field_name', + [ + 'type' => 'varchar_ascii', + 'length' => 1024, + 'not null' => TRUE, + 'default' => '', + 'description' => 'The field in the source entity containing the target entity.', + ] + ); +} -- GitLab From 693e22777052b9d4c49378548adb8336875917eb Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 24 Apr 2025 11:24:30 +0200 Subject: [PATCH 09/14] Spelling, phpcs and phpstan fixes --- entity_usage.install | 2 +- src/EntityUsageInterface.php | 2 +- src/EntityUsageTrackBase.php | 2 +- src/EntityUsageTrackInterface.php | 7 +---- src/ParagraphInheritedUsageUpdater.php | 27 ++++++++++++------- .../EntityUsage/Track/ParagraphInherited.php | 18 ++++++++++--- 6 files changed, 37 insertions(+), 21 deletions(-) diff --git a/entity_usage.install b/entity_usage.install index c8f4ed2..4fafe05 100644 --- a/entity_usage.install +++ b/entity_usage.install @@ -120,7 +120,7 @@ function entity_usage_update_last_removed(): int { /** * Increase field_name length in entity_usage table. */ -function entity_usage_update_8208() { +function entity_usage_update_8208(): void { \Drupal::database()->schema()->changeField( 'entity_usage', 'field_name', diff --git a/src/EntityUsageInterface.php b/src/EntityUsageInterface.php index bc47c2f..9626b14 100644 --- a/src/EntityUsageInterface.php +++ b/src/EntityUsageInterface.php @@ -251,7 +251,7 @@ interface EntityUsageInterface { * concatenated with a "|" character. * - if $return_as_arrays = TRUE: an array with the target_type, target_id, * method and field_name keys. - * Will return an empty array if no target entities found. + * Will return an empty array if no target entities found. * * @see \Drupal\entity_usage\EntityUsageTrackInterface::getTargetEntities() */ diff --git a/src/EntityUsageTrackBase.php b/src/EntityUsageTrackBase.php index 750db54..546b875 100644 --- a/src/EntityUsageTrackBase.php +++ b/src/EntityUsageTrackBase.php @@ -637,7 +637,7 @@ abstract class EntityUsageTrackBase extends PluginBase implements EntityUsageTra * * @return array * If the target entity was already an array, it is returned unchanged. If - * it was a string, the target entity is retured as an array with the + * it was a string, the target entity is returned as an array with the * target_type and target_id keys. */ protected function normalizeTargetEntity(string|array $target_entity): array { diff --git a/src/EntityUsageTrackInterface.php b/src/EntityUsageTrackInterface.php index eab342f..e87ab25 100644 --- a/src/EntityUsageTrackInterface.php +++ b/src/EntityUsageTrackInterface.php @@ -109,12 +109,7 @@ interface EntityUsageTrackInterface extends PluginInspectionInterface { * @param \Drupal\Core\Field\FieldItemInterface $item * The field item to get the target entity(ies) from. * - * @return string[]|array[] - * 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. - * - * @return array + * @return string[]|array * An indexed array where each value can be a string or an array: * - if a string: the target entity type and ID concatenated with a "|" * character. diff --git a/src/ParagraphInheritedUsageUpdater.php b/src/ParagraphInheritedUsageUpdater.php index ec79987..2612729 100644 --- a/src/ParagraphInheritedUsageUpdater.php +++ b/src/ParagraphInheritedUsageUpdater.php @@ -4,6 +4,8 @@ namespace Drupal\entity_usage; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Entity\RevisionableStorageInterface; use Drupal\paragraphs\ParagraphInterface; @@ -64,23 +66,27 @@ class ParagraphInheritedUsageUpdater { /** * {@inheritdoc} */ - public function destruct() { + public function destruct(): void { if (empty($this->paragraphRevisionIds)) { return; } - $paragraph_revisions = $this + $paragraph_storage = $this ->entityTypeManager - ->getStorage('paragraph') - ->loadMultipleRevisions($this->paragraphRevisionIds); + ->getStorage('paragraph'); + assert($paragraph_storage instanceof RevisionableStorageInterface); + $paragraph_revisions = $paragraph_storage->loadMultipleRevisions($this->paragraphRevisionIds); foreach ($paragraph_revisions as $paragraph_revision) { $outer_host_revisions = $this->getParagraphOuterHostRevisionsFieldItemLists($paragraph_revision); foreach ($outer_host_revisions as $outer_host_revision) { + $outer_host_entity = $outer_host_revision->getEntity(); $this ->recreateTrackingDataForFieldQueuer ->addRecord( - $outer_host_revision->getEntity()->getEntityTypeId(), - $outer_host_revision->getEntity()->id(), - $outer_host_revision->getEntity()->getRevisionId(), + $outer_host_entity->getEntityTypeId(), + $outer_host_entity->id(), + $outer_host_entity instanceof RevisionableInterface && $outer_host_entity->getRevisionId() + ? $outer_host_entity->getRevisionId() + : 0, 'paragraph_inherited', $outer_host_revision->getName(), ); @@ -97,7 +103,7 @@ class ParagraphInheritedUsageUpdater { * @param \Drupal\paragraphs\ParagraphInterface $paragraph_revision * The paragraph revision. * - * @return \Drupal\Core\Field\FieldItemListInterface[] + * @return \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\FieldItemInterface>[] * An array of field item lists or an empty array if none found. */ public function getParagraphOuterHostRevisionsFieldItemLists(ParagraphInterface $paragraph_revision): array { @@ -119,9 +125,11 @@ class ParagraphInheritedUsageUpdater { $next_parent_revision_field_item_lists = []; foreach ($parent_revision_field_item_lists as $parent_revision_field_item_list) { + $parent_revision_field_item_list_entity = $parent_revision_field_item_list->getEntity(); + assert($parent_revision_field_item_list_entity instanceof ParagraphInterface); $next_parent_revision_field_item_lists = array_merge( $next_parent_revision_field_item_lists, - $this->getParagraphParentRevisionsFieldItemLists($parent_revision_field_item_list->getEntity()), + $this->getParagraphParentRevisionsFieldItemLists($parent_revision_field_item_list_entity), ); } @@ -171,6 +179,7 @@ class ParagraphInheritedUsageUpdater { : $parent_entity_storage->loadMultiple($parent_revision_ids); return array_map(function (EntityInterface $parent_revision) use ($parent_field_name) { + assert($parent_revision instanceof FieldableEntityInterface); return $parent_revision->get($parent_field_name); }, $parent_revisions); } diff --git a/src/Plugin/EntityUsage/Track/ParagraphInherited.php b/src/Plugin/EntityUsage/Track/ParagraphInherited.php index 5eaa21e..5c4ee46 100644 --- a/src/Plugin/EntityUsage/Track/ParagraphInherited.php +++ b/src/Plugin/EntityUsage/Track/ParagraphInherited.php @@ -51,18 +51,30 @@ class ParagraphInherited extends EntityUsageTrackBase implements EntityUsageTrac * {@inheritdoc} */ public function getTargetEntities(FieldItemInterface $item): array { - return $this->doGetTargetEntities($item->getParent(), $item); + $parent = $item->getParent(); + return $parent instanceof FieldItemListInterface + ? $this->doGetTargetEntities($parent, $item) + : []; } /** * {@inheritdoc} + * + * @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\FieldItemInterface> $field */ public function getTargetEntitiesFromField(FieldItemListInterface $field): array { return $this->doGetTargetEntities($field); } /** - * {@inheritdoc} + * Gets the target entities. + * + * @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\FieldItemInterface> $field + * The field. + * @param \Drupal\Core\Field\FieldItemInterface|null $field_item + * (optional) The field item. + * + * @return array<int, array<string, mixed>> */ private function doGetTargetEntities(FieldItemListInterface $field, ?FieldItemInterface $field_item = NULL): array { $target_entities = []; @@ -76,7 +88,7 @@ class ParagraphInherited extends EntityUsageTrackBase implements EntityUsageTrac $parent_entity = $field->getParent()->getValue(); /** @var \Drupal\paragraphs\ParagraphInterface[] $referenced_paragraphs */ $referenced_paragraphs = $field_item instanceof FieldItemInterface - ? array_filter([$field_item->entity]) + ? array_filter([$field_item->entity ?? NULL]) : $field->referencedEntities(); foreach ($referenced_paragraphs as $referenced_paragraph) { // @todo Is this logic correct? -- GitLab From fdc246d58e743bc5f239203782133afdfc0ff826 Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 24 Apr 2025 11:38:41 +0200 Subject: [PATCH 10/14] phpcs and phpstan fixes --- src/EntityUsageTrackInterface.php | 10 +++++----- src/EntityUsageTrackMultipleLoadInterface.php | 14 +++++++++----- src/ParagraphInheritedUsageUpdater.php | 3 ++- .../EntityUsage/Track/ParagraphInherited.php | 11 ++++++++--- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/EntityUsageTrackInterface.php b/src/EntityUsageTrackInterface.php index e87ab25..b2ace6c 100644 --- a/src/EntityUsageTrackInterface.php +++ b/src/EntityUsageTrackInterface.php @@ -109,12 +109,12 @@ interface EntityUsageTrackInterface extends PluginInspectionInterface { * @param \Drupal\Core\Field\FieldItemInterface $item * The field item to get the target entity(ies) from. * - * @return string[]|array + * @return array<int, string|array{target_type: string, target_id: string|int, field_name?: string, method?: string}> * An indexed array where each value can be a string or an array: - * - if a string: the target entity type and ID concatenated with a "|" - * character. - * - if an array: an array with the target_type and target_id keys, and - * optionally the method and field_name keys. + * - if a string: the target entity type and ID concatenated with a "|" + * character. + * - if an array: an array with the target_type and target_id keys, and + * optionally the method and field_name keys. * Will return an empty array if no target entities could be retrieved from * the received field item value. */ diff --git a/src/EntityUsageTrackMultipleLoadInterface.php b/src/EntityUsageTrackMultipleLoadInterface.php index 195261e..e9e0302 100644 --- a/src/EntityUsageTrackMultipleLoadInterface.php +++ b/src/EntityUsageTrackMultipleLoadInterface.php @@ -19,13 +19,17 @@ interface EntityUsageTrackMultipleLoadInterface extends EntityUsageTrackInterfac /** * Retrieve the target entity(ies) from a field. * - * @param \Drupal\Core\Field\FieldItemListInterface $field + * @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\FieldItemInterface> $field * The field 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. + * @return array<int, string|array{target_type: string, target_id: string|int, field_name?: string, method?: string}> + * An indexed array where each value can be a string or an array: + * - if a string: the target entity type and ID concatenated with a "|" + * character. + * - if an array: an array with the target_type and target_id keys, and + * optionally the method and field_name keys. + * Will return an empty array if no target entities could be retrieved from + * the received field item value. */ public function getTargetEntitiesFromField(FieldItemListInterface $field): array; diff --git a/src/ParagraphInheritedUsageUpdater.php b/src/ParagraphInheritedUsageUpdater.php index 2612729..c1ac2fa 100644 --- a/src/ParagraphInheritedUsageUpdater.php +++ b/src/ParagraphInheritedUsageUpdater.php @@ -76,6 +76,7 @@ class ParagraphInheritedUsageUpdater { assert($paragraph_storage instanceof RevisionableStorageInterface); $paragraph_revisions = $paragraph_storage->loadMultipleRevisions($this->paragraphRevisionIds); foreach ($paragraph_revisions as $paragraph_revision) { + assert($paragraph_revision instanceof ParagraphInterface); $outer_host_revisions = $this->getParagraphOuterHostRevisionsFieldItemLists($paragraph_revision); foreach ($outer_host_revisions as $outer_host_revision) { $outer_host_entity = $outer_host_revision->getEntity(); @@ -145,7 +146,7 @@ class ParagraphInheritedUsageUpdater { * @param \Drupal\paragraphs\ParagraphInterface $paragraph_revision * The paragraph revision. * - * @return \Drupal\Core\Field\FieldItemListInterface[] + * @return \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\FieldItemInterface>[] * An array of field item lists keyed by the paragraph revision's ID or an * empty array if none found. */ diff --git a/src/Plugin/EntityUsage/Track/ParagraphInherited.php b/src/Plugin/EntityUsage/Track/ParagraphInherited.php index 5c4ee46..5b51270 100644 --- a/src/Plugin/EntityUsage/Track/ParagraphInherited.php +++ b/src/Plugin/EntityUsage/Track/ParagraphInherited.php @@ -59,8 +59,6 @@ class ParagraphInherited extends EntityUsageTrackBase implements EntityUsageTrac /** * {@inheritdoc} - * - * @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\FieldItemInterface> $field */ public function getTargetEntitiesFromField(FieldItemListInterface $field): array { return $this->doGetTargetEntities($field); @@ -74,7 +72,14 @@ class ParagraphInherited extends EntityUsageTrackBase implements EntityUsageTrac * @param \Drupal\Core\Field\FieldItemInterface|null $field_item * (optional) The field item. * - * @return array<int, array<string, mixed>> + * @return array<int, string|array{target_type: string, target_id: string|int, field_name?: string, method?: string}> + * An indexed array where each value can be a string or an array: + * - if a string: the target entity type and ID concatenated with a "|" + * character. + * - if an array: an array with the target_type and target_id keys, and + * optionally the method and field_name keys. + * Will return an empty array if no target entities could be retrieved from + * the received field item value. */ private function doGetTargetEntities(FieldItemListInterface $field, ?FieldItemInterface $field_item = NULL): array { $target_entities = []; -- GitLab From 88d4f7be5504a8d8c1819d1dcca9f8ad19b0a2c2 Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 24 Apr 2025 11:47:17 +0200 Subject: [PATCH 11/14] phpcs and phpstan fixes --- src/EntityUsageTrackMultipleLoadInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EntityUsageTrackMultipleLoadInterface.php b/src/EntityUsageTrackMultipleLoadInterface.php index e9e0302..34c0a93 100644 --- a/src/EntityUsageTrackMultipleLoadInterface.php +++ b/src/EntityUsageTrackMultipleLoadInterface.php @@ -19,7 +19,7 @@ interface EntityUsageTrackMultipleLoadInterface extends EntityUsageTrackInterfac /** * Retrieve the target entity(ies) from a field. * - * @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\FieldItemInterface> $field + * @param \Drupal\Core\Field\FieldItemListInterface $field * The field to get the target entity(ies) from. * * @return array<int, string|array{target_type: string, target_id: string|int, field_name?: string, method?: string}> -- GitLab From 559f3c3c03dc283ae049107eb1b7bf7a9d068e63 Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 24 Apr 2025 11:54:08 +0200 Subject: [PATCH 12/14] phpcs fixes --- src/EntityUsageTrackMultipleLoadInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EntityUsageTrackMultipleLoadInterface.php b/src/EntityUsageTrackMultipleLoadInterface.php index 34c0a93..04c6cff 100644 --- a/src/EntityUsageTrackMultipleLoadInterface.php +++ b/src/EntityUsageTrackMultipleLoadInterface.php @@ -25,7 +25,7 @@ interface EntityUsageTrackMultipleLoadInterface extends EntityUsageTrackInterfac * @return array<int, string|array{target_type: string, target_id: string|int, field_name?: string, method?: string}> * An indexed array where each value can be a string or an array: * - if a string: the target entity type and ID concatenated with a "|" - * character. + * character. * - if an array: an array with the target_type and target_id keys, and * optionally the method and field_name keys. * Will return an empty array if no target entities could be retrieved from -- GitLab From ffe3a1be621d79b2dd6706e205ee8eb153ad3274 Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 24 Apr 2025 11:57:12 +0200 Subject: [PATCH 13/14] phpstan fixes --- src/EntityUsageTrackMultipleLoadInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EntityUsageTrackMultipleLoadInterface.php b/src/EntityUsageTrackMultipleLoadInterface.php index 04c6cff..b81fe4c 100644 --- a/src/EntityUsageTrackMultipleLoadInterface.php +++ b/src/EntityUsageTrackMultipleLoadInterface.php @@ -19,7 +19,7 @@ interface EntityUsageTrackMultipleLoadInterface extends EntityUsageTrackInterfac /** * Retrieve the target entity(ies) from a field. * - * @param \Drupal\Core\Field\FieldItemListInterface $field + * @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\FieldItemInterface> $field * The field to get the target entity(ies) from. * * @return array<int, string|array{target_type: string, target_id: string|int, field_name?: string, method?: string}> -- GitLab From 545eafc05720f55214ab593e288ec295ac377f19 Mon Sep 17 00:00:00 2001 From: Raf Philtjens <raf@rafatwork.be> Date: Thu, 24 Apr 2025 12:02:55 +0200 Subject: [PATCH 14/14] phpstan fixes --- src/EntityUsageTrackMultipleLoadInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/EntityUsageTrackMultipleLoadInterface.php b/src/EntityUsageTrackMultipleLoadInterface.php index b81fe4c..04c6cff 100644 --- a/src/EntityUsageTrackMultipleLoadInterface.php +++ b/src/EntityUsageTrackMultipleLoadInterface.php @@ -19,7 +19,7 @@ interface EntityUsageTrackMultipleLoadInterface extends EntityUsageTrackInterfac /** * Retrieve the target entity(ies) from a field. * - * @param \Drupal\Core\Field\FieldItemListInterface<\Drupal\Core\Field\FieldItemInterface> $field + * @param \Drupal\Core\Field\FieldItemListInterface $field * The field to get the target entity(ies) from. * * @return array<int, string|array{target_type: string, target_id: string|int, field_name?: string, method?: string}> -- GitLab