From a7e4c1659e2a7cb1539143158a2e2b1c05b8a23c Mon Sep 17 00:00:00 2001 From: Jordan Dukart Date: Tue, 5 Apr 2022 14:15:17 -0300 Subject: [PATCH] Add ancestor helper and condition. (#865) * Add ancestor condition. * Move the ancestors to the utils helper, re-work the builder. * Use the config option as opposed to hard coding. * Handle a looping scenario (a > b > c > a). --- .../islandora_breadcrumbs.services.yml | 2 +- .../src/IslandoraBreadcrumbBuilder.php | 59 +++--- src/IslandoraUtils.php | 79 ++++++++ src/Plugin/Condition/NodeHasAncestor.php | 169 ++++++++++++++++++ 4 files changed, 272 insertions(+), 37 deletions(-) create mode 100644 src/Plugin/Condition/NodeHasAncestor.php diff --git a/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml index 58e3c959..71c723f0 100644 --- a/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml +++ b/modules/islandora_breadcrumbs/islandora_breadcrumbs.services.yml @@ -1,6 +1,6 @@ services: islandora_breadcrumbs.breadcrumb: class: Drupal\islandora_breadcrumbs\IslandoraBreadcrumbBuilder - arguments: ['@entity_type.manager', '@config.factory'] + arguments: ['@entity_type.manager', '@config.factory', '@islandora.utils'] tags: - { name: breadcrumb_builder, priority: 100 } diff --git a/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php b/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php index 93ed7097..620f2289 100644 --- a/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php +++ b/modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php @@ -4,12 +4,12 @@ namespace Drupal\islandora_breadcrumbs; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Breadcrumb\Breadcrumb; use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface; use Drupal\Core\Link; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\islandora\IslandoraUtils; /** * Provides breadcrumbs for nodes using a configured entity reference field. @@ -31,6 +31,13 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { */ protected $nodeStorage; + /** + * Islandora utils. + * + * @var \Drupal\islandora\IslandoraUtils + */ + protected $utils; + /** * Constructs a breadcrumb builder. * @@ -38,10 +45,13 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { * Storage to load nodes. * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory * The configuration factory. + * @param \Drupal\islandora\IslandoraUtils $utils + * Islandora utils service. */ - public function __construct(EntityTypeManagerInterface $entity_manager, ConfigFactoryInterface $config_factory) { + public function __construct(EntityTypeManagerInterface $entity_manager, ConfigFactoryInterface $config_factory, IslandoraUtils $utils) { $this->nodeStorage = $entity_manager->getStorage('node'); $this->config = $config_factory->get('islandora_breadcrumbs.breadcrumbs'); + $this->utils = $utils; } /** @@ -68,49 +78,26 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { $breadcrumb = new Breadcrumb(); $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '')); - $chain = []; - $this->walkMembership($node, $chain); + $chain = array_reverse($this->utils->findAncestors($node, [$this->config->get('referenceField')], $this->config->get('maxDepth'))); - if (!$this->config->get('includeSelf')) { - array_pop($chain); + // XXX: Handle a looping breadcrumb scenario by filtering the present + // node out and then optionally re-adding it after if set to do so. + $chain = array_filter($chain, function ($link) use ($nid) { + return $link !== $nid; + }); + if ($this->config->get('includeSelf')) { + array_push($chain, $node); } $breadcrumb->addCacheableDependency($node); // Add membership chain to the breadcrumb. foreach ($chain as $chainlink) { - $breadcrumb->addCacheableDependency($chainlink); - $breadcrumb->addLink($chainlink->toLink()); + $node = $this->nodeStorage->load($chainlink); + $breadcrumb->addCacheableDependency($node); + $breadcrumb->addLink($node->toLink()); } $breadcrumb->addCacheContexts(['route']); return $breadcrumb; } - /** - * Follows chain of field_member_of links. - * - * We pass crumbs by reference to enable checking for looped chains. - */ - protected function walkMembership(EntityInterface $entity, &$crumbs) { - // Avoid infinate loops, return if we've seen this before. - foreach ($crumbs as $crumb) { - if ($crumb->uuid == $entity->uuid) { - return; - } - } - - // Add this item onto the pile. - array_unshift($crumbs, $entity); - - if ($this->config->get('maxDepth') > 0 && count($crumbs) >= $this->config->get('maxDepth')) { - return; - } - - // Find the next in the chain, if there are any. - if ($entity->hasField($this->config->get('referenceField')) && - !$entity->get($this->config->get('referenceField'))->isEmpty() && - $entity->get($this->config->get('referenceField'))->entity instanceof EntityInterface) { - $this->walkMembership($entity->get($this->config->get('referenceField'))->entity, $crumbs); - } - } - } diff --git a/src/IslandoraUtils.php b/src/IslandoraUtils.php index a4dda6c1..e5071a05 100644 --- a/src/IslandoraUtils.php +++ b/src/IslandoraUtils.php @@ -672,4 +672,83 @@ class IslandoraUtils { return FALSE; } + /** + * Recursively finds ancestors of an entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being checked. + * @param array $fields + * An optional array where the values are the field names to be used for + * retrieval. + * @param int|bool $max_height + * How many levels of checking should be done when retrieving ancestors. + * + * @return array + * An array where the keys and values are the node IDs of the ancestors. + */ + public function findAncestors(ContentEntityInterface $entity, array $fields = [self::MEMBER_OF_FIELD], $max_height = FALSE): array { + // XXX: If a negative integer is passed assume it's false. + if ($max_height < 0) { + $max_height = FALSE; + } + $context = [ + 'max_height' => $max_height, + 'ancestors' => [], + ]; + $this->findAncestorsByEntityReference($entity, $context, $fields); + return $context['ancestors']; + } + + /** + * Helper that builds up the ancestors. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being checked. + * @param array $context + * An array containing: + * -ancestors: The ancestors that have been found. + * -max_height: How far up the chain to go. + * @param array $fields + * An optional array where the values are the field names to be used for + * retrieval. + * @param int $current_height + * The current height of the recursion. + */ + protected function findAncestorsByEntityReference(ContentEntityInterface $entity, array &$context, array $fields = [self::MEMBER_OF_FIELD], int $current_height = 0): void { + $parents = $this->getParentsByEntityReference($entity, $fields); + foreach ($parents as $parent) { + if (isset($context['ancestors'][$parent->id()])) { + continue; + } + $context['ancestors'][$parent->id()] = $parent->id(); + if ($context['max_height'] === FALSE || $current_height < $context['max_height']) { + $this->findAncestorsByEntityReference($parent, $context, $fields, $current_height + 1); + } + } + } + + /** + * Helper that gets the immediate parents of a node. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity being checked. + * @param array $fields + * An array where the values are the field names to be used. + * + * @return array + * An array of entity objects keyed by field item deltas. + */ + protected function getParentsByEntityReference(ContentEntityInterface $entity, array $fields): array { + $parents = []; + foreach ($fields as $field) { + if ($entity->hasField($field)) { + $reference_field = $entity->get($field); + if (!$reference_field->isEmpty()) { + $parents = array_merge($parents, $reference_field->referencedEntities()); + } + } + } + return $parents; + } + } diff --git a/src/Plugin/Condition/NodeHasAncestor.php b/src/Plugin/Condition/NodeHasAncestor.php new file mode 100644 index 00000000..dd2abc54 --- /dev/null +++ b/src/Plugin/Condition/NodeHasAncestor.php @@ -0,0 +1,169 @@ +entityTypeManager = $entity_type_manager; + $this->utils = $islandora_utils; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('islandora.utils') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration(): array { + return parent::defaultConfiguration() + [ + 'ancestor_nids' => FALSE, + 'parent_reference_field' => IslandoraUtils::MEMBER_OF_FIELD, + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $default_nids = FALSE; + if ($this->configuration['ancestor_nids']) { + $default_nids = array_map(function ($nid) { + return $this->entityTypeManager->getStorage('node')->load($nid); + }, $this->configuration['ancestor_nids']); + } + $form['ancestor_nids'] = [ + '#type' => 'entity_autocomplete', + '#title' => $this->t('Parent node(s)'), + '#default_value' => $default_nids, + '#required' => TRUE, + '#description' => $this->t("Can be a collection node, compound object or paged content. Accepts multiple values separated by a comma."), + '#target_type' => 'node', + '#tags' => TRUE, + ]; + + $options = []; + $reference_fields = $this->entityTypeManager->getStorage('field_storage_config')->loadByProperties([ + 'type' => 'entity_reference', + 'settings' => [ + 'target_type' => 'node', + ], + ]); + foreach ($reference_fields as $field) { + $options[$field->get('field_name')] = $field->get('field_name'); + } + $form['parent_reference_field'] = [ + '#type' => 'select', + '#title' => $this->t('Direct parent reference'), + '#options' => $options, + '#default_value' => $this->configuration['parent_reference_field'], + '#required' => TRUE, + '#description' => $this->t('Field that contains the reference to its parent node.'), + ]; + + return parent::buildConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + // Entity autocomplete store things with target IDs, for convenience just + // store the plain nid. + $this->configuration['ancestor_nids'] = array_map(function ($nid) { + return $nid['target_id']; + }, $form_state->getValue('ancestor_nids')); + $this->configuration['parent_reference_field'] = $form_state->getValue('parent_reference_field'); + parent::submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function evaluate() { + if (empty($this->configuration['ancestor_nids']) && !$this->isNegated()) { + return TRUE; + } + + $node = $this->getContextValue('node'); + if (!$node) { + return FALSE; + } + + $ancestors = $this->utils->findAncestors($node); + return !empty(array_intersect($this->configuration['ancestor_nids'], $ancestors)); + } + + /** + * {@inheritdoc} + */ + public function summary() { + if (!empty($this->configuration['negate'])) { + return $this->t('The node does not have node @nid as one of its ancestors.', ['@nid' => $this->configuration['ancestor_nids']]); + } + else { + return $this->t('The node has node @nid as one of its ancestors.', ['@nid' => $this->configuration['ancestor_nids']]); + } + } + +}