Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ancestor helper and condition. #865

Merged
merged 4 commits into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 }
59 changes: 23 additions & 36 deletions modules/islandora_breadcrumbs/src/IslandoraBreadcrumbBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

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.
Expand All @@ -31,17 +31,27 @@ class IslandoraBreadcrumbBuilder implements BreadcrumbBuilderInterface {
*/
protected $nodeStorage;

/**
* Islandora utils.
*
* @var \Drupal\islandora\IslandoraUtils
*/
protected $utils;

/**
* Constructs a breadcrumb builder.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
* 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;
}

/**
Expand All @@ -68,49 +78,26 @@ public function build(RouteMatchInterface $route_match) {
$breadcrumb = new Breadcrumb();
$breadcrumb->addLink(Link::createFromRoute($this->t('Home'), '<front>'));

$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);
}
}

}
79 changes: 79 additions & 0 deletions src/IslandoraUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -672,4 +672,83 @@ public function canCreateIslandoraEntity($entity_type, $bundle_type) {
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;
}

}
169 changes: 169 additions & 0 deletions src/Plugin/Condition/NodeHasAncestor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

namespace Drupal\islandora\Plugin\Condition;

use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\islandora\IslandoraUtils;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
* Condition to see if a node has an ancestor.
*
* @Condition(
* id = "node_has_ancestor",
* label = @Translation("Node has ancestor"),
* context_definitions = {
* "node" = @ContextDefinition("entity:node", required = TRUE , label = @Translation("node"))
* }
* )
*/
class NodeHasAncestor extends ConditionPluginBase implements ContainerFactoryPluginInterface {

/**
* Drupal's entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected EntityTypeManagerInterface $entityTypeManager;

/**
* Islandora utils.
*
* @var \Drupal\islandora\IslandoraUtils
*/
protected IslandoraUtils $utils;

/**
* Constructor for the ancestor condition.
*
* @param array $configuration
* The plugin configuration, i.e. an array with configuration values keyed
* by configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* 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, IslandoraUtils $islandora_utils) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->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']]);
}
}

}