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..df240a3a 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,20 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface { $breadcrumb = new Breadcrumb(); $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '')); - $chain = []; - $this->walkMembership($node, $chain); - - if (!$this->config->get('includeSelf')) { - array_pop($chain); + $chain = array_reverse($this->utils->findAncestors($node, [$this->config->get('referenceField')], 1)); + 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 index cc4c207c..dd2abc54 100644 --- a/src/Plugin/Condition/NodeHasAncestor.php +++ b/src/Plugin/Condition/NodeHasAncestor.php @@ -3,8 +3,6 @@ namespace Drupal\islandora\Plugin\Condition; use Drupal\Core\Condition\ConditionPluginBase; -use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; @@ -31,6 +29,13 @@ class NodeHasAncestor extends ConditionPluginBase implements ContainerFactoryPlu */ protected EntityTypeManagerInterface $entityTypeManager; + /** + * Islandora utils. + * + * @var \Drupal\islandora\IslandoraUtils + */ + protected IslandoraUtils $utils; + /** * Constructor for the ancestor condition. * @@ -45,10 +50,13 @@ class NodeHasAncestor extends ConditionPluginBase implements ContainerFactoryPlu * The plugin implementation definition. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The Drupal entity type manager. + * @param \Drupal\islandora\IslandoraUtils $islandora_utils + * Islandora utils service. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, IslandoraUtils $islandora_utils) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->entityTypeManager = $entity_type_manager; + $this->utils = $islandora_utils; } /** @@ -60,6 +68,7 @@ class NodeHasAncestor extends ConditionPluginBase implements ContainerFactoryPlu $plugin_id, $plugin_definition, $container->get('entity_type.manager'), + $container->get('islandora.utils') ); } @@ -141,48 +150,10 @@ class NodeHasAncestor extends ConditionPluginBase implements ContainerFactoryPlu return FALSE; } - $ancestors = []; - $this->findAncestors($node, $ancestors); + $ancestors = $this->utils->findAncestors($node); return !empty(array_intersect($this->configuration['ancestor_nids'], $ancestors)); } - /** - * Recursively finds ancestors of an entity. - * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * The entity being checked. - * @param array $ancestors - * The array of ancestors, passed by reference. - */ - protected function findAncestors(ContentEntityInterface $entity, array &$ancestors): void { - $parents = $this->getParents($entity); - foreach ($parents as $parent) { - if (!isset($ancestors[$parent->id()])) { - $ancestors[$parent->id()] = $parent->id(); - $this->findAncestors($parent, $ancestors); - } - } - } - - /** - * Helper that gets the immediate parents of a node. - * - * @param \Drupal\Core\Entity\ContentEntityInterface $entity - * The entity being checked. - * - * @return array - * An array of entity objects keyed by field item deltas. - */ - protected function getParents(ContentEntityInterface $entity): array { - if ($entity->hasField($this->configuration['parent_reference_field'])) { - $field = $entity->get($this->configuration['parent_reference_field']); - if (!$field->isEmpty()) { - return $field->referencedEntities(); - } - } - return []; - } - /** * {@inheritdoc} */