diff --git a/ocfl_export.services.yml b/ocfl_export.services.yml index c7ed67b..e3d3361 100644 --- a/ocfl_export.services.yml +++ b/ocfl_export.services.yml @@ -13,6 +13,7 @@ services: arguments: - '@entity_type.manager' - '@datetime.time' + - '@config.factory' ocfl_export.media_serializer: class: Drupal\ocfl_export\MediaSerializerService diff --git a/src/MediaSerializerService.php b/src/MediaSerializerService.php index e02ebf1..2c5ca06 100644 --- a/src/MediaSerializerService.php +++ b/src/MediaSerializerService.php @@ -38,6 +38,7 @@ class MediaSerializerService { $data = [ 'uuid' => $media->uuid(), + 'mid' => (int) $media->id(), 'type' => $media->bundle(), 'name' => $media->getName(), 'created' => $this->formatTimestamp((int) $media->getCreatedTime()), @@ -363,6 +364,7 @@ class MediaSerializerService { protected function serializeMediaReference(MediaInterface $media): array { $data = [ 'uuid' => $media->uuid(), + 'mid' => (int) $media->id(), 'bundle' => $media->bundle(), 'name' => $media->getName(), 'status' => $media->isPublished(), @@ -419,6 +421,7 @@ class MediaSerializerService { protected function serializeNodeReference(NodeInterface $node): array { return [ 'uuid' => $node->uuid(), + 'nid' => (int) $node->id(), 'type' => $node->bundle(), 'title' => $node->getTitle(), 'status' => $node->isPublished(), diff --git a/src/NodeSerializerService.php b/src/NodeSerializerService.php index e6c0189..77fa289 100644 --- a/src/NodeSerializerService.php +++ b/src/NodeSerializerService.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\ocfl_export; use Drupal\Component\Datetime\TimeInterface; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\media\MediaInterface; @@ -18,9 +19,15 @@ use Drupal\user\UserInterface; */ class NodeSerializerService { + /** + * Maximum number of ancestors followed when building hierarchy paths. + */ + protected const MAX_HIERARCHY_DEPTH = 50; + public function __construct( protected EntityTypeManagerInterface $entityTypeManager, protected TimeInterface $time, + protected ConfigFactoryInterface $configFactory, ) {} /** @@ -37,6 +44,7 @@ class NodeSerializerService { $data = [ 'uuid' => $node->uuid(), + 'nid' => (int) $node->id(), 'type' => $node->bundle(), 'title' => $node->getTitle(), 'created' => $this->formatTimestamp((int) $node->getCreatedTime()), @@ -47,6 +55,7 @@ class NodeSerializerService { 'name' => $author->getAccountName(), 'display_name' => $author->getDisplayName(), ], + 'relationships' => $this->serializeRelationships($node), 'fields' => $this->serializeFields($node), '_metadata' => [ 'ocfl_version' => '1.1', @@ -58,6 +67,155 @@ class NodeSerializerService { return $data; } + /** + * Serialize Islandora-style membership (isMemberOf) and model information. + * + * @param \Drupal\node\NodeInterface $node + * The node. + * + * @return array + * An array with 'model', 'member_of' (direct parents) and 'hierarchy' + * (every ancestor path, each ordered from the root down to the direct + * parent). Keys are omitted when the corresponding field is not present. + */ + protected function serializeRelationships(NodeInterface $node): array { + $config = $this->configFactory->get('ocfl_export.settings'); + $modelField = $config->get('model_field'); + $memberOfField = $config->get('member_of_field'); + $relationships = []; + + if ($modelField && $node->hasField($modelField)) { + $relationships['model'] = $this->getModel($node); + } + + if ($memberOfField && $node->hasField($memberOfField)) { + $relationships['member_of'] = array_map( + fn (NodeInterface $parent) => $this->summarizeHierarchyNode($parent), + $this->getMemberOfParents($node), + ); + $relationships['hierarchy'] = $this->buildAncestorPaths($node); + } + + return $relationships; + } + + /** + * Get the parent nodes a node is a member of. + * + * @param \Drupal\node\NodeInterface $node + * The node. + * + * @return \Drupal\node\NodeInterface[] + * The parent nodes. + */ + protected function getMemberOfParents(NodeInterface $node): array { + $field = $this->configFactory->get('ocfl_export.settings')->get('member_of_field'); + if (!$field || !$node->hasField($field)) { + return []; + } + + return array_values(array_filter( + $node->get($field)->referencedEntities(), + fn ($entity) => $entity instanceof NodeInterface, + )); + } + + /** + * Build every ancestor path for a node via the member-of field. + * + * @param \Drupal\node\NodeInterface $node + * The node. + * @param int[] $visited + * Node IDs already on the current path, to guard against cycles. + * + * @return array + * A list of paths. Each path is a list of node summaries ordered from the + * root collection down to the node's direct parent. + */ + protected function buildAncestorPaths(NodeInterface $node, array $visited = []): array { + $visited[] = (int) $node->id(); + if (count($visited) > static::MAX_HIERARCHY_DEPTH) { + return []; + } + + $paths = []; + foreach ($this->getMemberOfParents($node) as $parent) { + if (in_array((int) $parent->id(), $visited, TRUE)) { + continue; + } + + $summary = $this->summarizeHierarchyNode($parent); + $parentPaths = $this->buildAncestorPaths($parent, $visited); + if (!$parentPaths) { + $paths[] = [$summary]; + continue; + } + foreach ($parentPaths as $path) { + $path[] = $summary; + $paths[] = $path; + } + } + + return $paths; + } + + /** + * Summarize a node for membership and hierarchy listings. + * + * @param \Drupal\node\NodeInterface $node + * The node. + * + * @return array + * The node summary. + */ + protected function summarizeHierarchyNode(NodeInterface $node): array { + $summary = [ + 'nid' => (int) $node->id(), + 'uuid' => $node->uuid(), + 'title' => $node->getTitle(), + 'type' => $node->bundle(), + ]; + + $model = $this->getModel($node); + if ($model) { + $summary['model'] = $model['name']; + } + + return $summary; + } + + /** + * Get the Islandora model term of a node. + * + * @param \Drupal\node\NodeInterface $node + * The node. + * + * @return array|null + * The model name, term UUID and external URI (if any), or NULL. + */ + protected function getModel(NodeInterface $node): ?array { + $field = $this->configFactory->get('ocfl_export.settings')->get('model_field'); + if (!$field || !$node->hasField($field)) { + return NULL; + } + + $term = $node->get($field)->entity; + if (!$term instanceof TermInterface) { + return NULL; + } + + $model = [ + 'tid' => (int) $term->id(), + 'uuid' => $term->uuid(), + 'name' => $term->getName(), + ]; + if ($term->hasField('field_external_uri') && !$term->get('field_external_uri')->isEmpty()) { + $model['uri'] = $term->get('field_external_uri')->uri; + } + + return $model; + } + /** * Serialize all fields of a node. * @@ -301,6 +459,7 @@ class NodeSerializerService { protected function serializeMedia(MediaInterface $media): array { $data = [ 'uuid' => $media->uuid(), + 'mid' => (int) $media->id(), 'name' => $media->getName(), 'bundle' => $media->bundle(), ]; @@ -355,6 +514,7 @@ class NodeSerializerService { protected function serializeNodeReference(NodeInterface $node): array { return [ 'uuid' => $node->uuid(), + 'nid' => (int) $node->id(), 'type' => $node->bundle(), 'title' => $node->getTitle(), 'status' => $node->isPublished(),