create OCFL node.json files for each node and associated media/files.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

504 lines
14 KiB

<?php
declare(strict_types=1);
namespace Drupal\ocfl_export;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\file\FileInterface;
use Drupal\media\MediaInterface;
use Drupal\node\NodeInterface;
use Drupal\paragraphs\ParagraphInterface;
use Drupal\taxonomy\TermInterface;
use Drupal\user\UserInterface;
/**
* Service for serializing media entities to JSON format.
*/
class MediaSerializerService {
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager,
protected TimeInterface $time,
) {}
/**
* Serialize a media entity to an array suitable for JSON encoding.
*
* @param \Drupal\media\MediaInterface $media
* The media entity to serialize.
*
* @return array
* The serialized media data.
*/
public function serialize(MediaInterface $media): array {
$owner = $media->getOwner();
$data = [
'uuid' => $media->uuid(),
'mid' => (int) $media->id(),
'type' => $media->bundle(),
'name' => $media->getName(),
'created' => $this->formatTimestamp((int) $media->getCreatedTime()),
'changed' => $this->formatTimestamp((int) $media->getChangedTime()),
'status' => $media->isPublished(),
'owner' => [
'uid' => (int) $owner->id(),
'name' => $owner->getAccountName(),
'display_name' => $owner->getDisplayName(),
],
'source' => $this->serializeMediaSource($media),
'fields' => $this->serializeFields($media),
'_metadata' => [
'ocfl_version' => '1.1',
'export_timestamp' => $this->formatTimestamp($this->time->getRequestTime()),
'drupal_version' => \Drupal::VERSION,
'entity_type' => 'media',
],
];
return $data;
}
/**
* Serialize the media source (primary file/data).
*
* @param \Drupal\media\MediaInterface $media
* The media entity.
*
* @return array
* The serialized source data.
*/
protected function serializeMediaSource(MediaInterface $media): array {
$source = $media->getSource();
$sourceConfig = $source->getConfiguration();
$sourceField = $sourceConfig['source_field'] ?? NULL;
$data = [
'plugin_id' => $source->getPluginId(),
];
if ($sourceField && $media->hasField($sourceField)) {
$sourceFieldItems = $media->get($sourceField);
if (!$sourceFieldItems->isEmpty()) {
$sourceItem = $sourceFieldItems->first();
$file = $sourceItem->entity ?? NULL;
if ($file instanceof FileInterface) {
$data['file'] = [
'uuid' => $file->uuid(),
'filename' => $file->getFilename(),
'uri' => $file->getFileUri(),
'filemime' => $file->getMimeType(),
'filesize' => (int) $file->getSize(),
'url' => \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri()),
];
// Include alt and title if available (for images).
if (isset($sourceItem->alt)) {
$data['alt'] = $sourceItem->alt;
}
if (isset($sourceItem->title)) {
$data['title'] = $sourceItem->title;
}
if (isset($sourceItem->width)) {
$data['width'] = (int) $sourceItem->width;
}
if (isset($sourceItem->height)) {
$data['height'] = (int) $sourceItem->height;
}
}
else {
// For non-file sources (e.g., oEmbed, remote video).
$data['value'] = $sourceItem->getValue();
}
}
}
return $data;
}
/**
* Serialize all fields of a media entity.
*
* @param \Drupal\media\MediaInterface $media
* The media entity to serialize fields from.
*
* @return array
* The serialized field data.
*/
protected function serializeFields(MediaInterface $media): array {
$fields = [];
$fieldDefinitions = $media->getFieldDefinitions();
$sourceField = $media->getSource()->getConfiguration()['source_field'] ?? NULL;
foreach ($fieldDefinitions as $fieldName => $definition) {
// Skip base fields and source field (already serialized).
if ($this->isBaseField($fieldName) || $fieldName === $sourceField) {
continue;
}
$fieldItems = $media->get($fieldName);
if ($fieldItems->isEmpty()) {
continue;
}
$fields[$fieldName] = $this->serializeFieldItems($fieldItems);
}
return $fields;
}
/**
* Check if a field is a base field that should be skipped.
*
* @param string $fieldName
* The field name.
*
* @return bool
* TRUE if this is a base field to skip.
*/
protected function isBaseField(string $fieldName): bool {
$baseFields = [
'mid',
'vid',
'uuid',
'langcode',
'bundle',
'revision_created',
'revision_user',
'revision_log_message',
'status',
'uid',
'name',
'thumbnail',
'created',
'changed',
'default_langcode',
'revision_default',
'revision_translation_affected',
];
return in_array($fieldName, $baseFields, TRUE);
}
/**
* Serialize field items.
*
* @param \Drupal\Core\Field\FieldItemListInterface $fieldItems
* The field items to serialize.
*
* @return mixed
* The serialized field data.
*/
protected function serializeFieldItems(FieldItemListInterface $fieldItems): mixed {
$fieldType = $fieldItems->getFieldDefinition()->getType();
$values = [];
foreach ($fieldItems as $delta => $item) {
$value = $this->serializeFieldItem($item, $fieldType);
if ($value !== NULL) {
$values[] = $value;
}
}
// Return single value for single-value fields.
if (count($values) === 1 && $fieldItems->getFieldDefinition()->getFieldStorageDefinition()->getCardinality() === 1) {
return $values[0];
}
return $values;
}
/**
* Serialize a single field item.
*
* @param mixed $item
* The field item.
* @param string $fieldType
* The field type.
*
* @return mixed
* The serialized value.
*/
protected function serializeFieldItem(mixed $item, string $fieldType): mixed {
return match ($fieldType) {
'entity_reference' => $this->serializeEntityReference($item),
'entity_reference_revisions' => $this->serializeEntityReferenceRevisions($item),
'text_with_summary' => [
'value' => $item->value,
'format' => $item->format,
'summary' => $item->summary ?? '',
],
'text_long', 'text' => [
'value' => $item->value,
'format' => $item->format ?? NULL,
],
'image' => [
'target_id' => (int) $item->target_id,
'alt' => $item->alt ?? '',
'title' => $item->title ?? '',
'width' => $item->width ? (int) $item->width : NULL,
'height' => $item->height ? (int) $item->height : NULL,
'entity' => $this->serializeFileEntity($item->entity),
],
'file' => [
'target_id' => (int) $item->target_id,
'description' => $item->description ?? '',
'display' => $item->display ?? TRUE,
'entity' => $this->serializeFileEntity($item->entity),
],
'link' => [
'uri' => $item->uri,
'title' => $item->title ?? '',
'options' => $item->options ?? [],
],
'datetime' => $item->value,
'daterange' => [
'value' => $item->value,
'end_value' => $item->end_value,
],
'boolean' => (bool) $item->value,
'integer', 'list_integer' => (int) $item->value,
'decimal', 'float', 'list_float' => (float) $item->value,
'email' => $item->value,
'telephone' => $item->value,
default => $item->getValue(),
};
}
/**
* Serialize an entity reference field item.
*
* @param mixed $item
* The field item.
*
* @return array|null
* The serialized reference or NULL.
*/
protected function serializeEntityReference(mixed $item): ?array {
$entity = $item->entity;
if (!$entity) {
return NULL;
}
$data = [
'target_id' => (int) $item->target_id,
];
// Dereference based on entity type.
$data['entity'] = match ($entity->getEntityTypeId()) {
'taxonomy_term' => $this->serializeTaxonomyTerm($entity),
'media' => $this->serializeMediaReference($entity),
'user' => $this->serializeUser($entity),
'node' => $this->serializeNodeReference($entity),
'file' => $this->serializeFileEntity($entity),
default => [
'uuid' => $entity->uuid(),
'type' => $entity->bundle(),
'label' => $entity->label(),
],
};
return $data;
}
/**
* Serialize an entity reference revisions field item (paragraphs).
*
* @param mixed $item
* The field item.
*
* @return array|null
* The serialized reference or NULL.
*/
protected function serializeEntityReferenceRevisions(mixed $item): ?array {
$entity = $item->entity;
if (!$entity) {
return NULL;
}
if ($entity instanceof ParagraphInterface) {
return $this->serializeParagraph($entity);
}
return [
'target_id' => (int) $item->target_id,
'target_revision_id' => (int) $item->target_revision_id,
'entity' => [
'uuid' => $entity->uuid(),
'type' => $entity->bundle(),
],
];
}
/**
* Serialize a taxonomy term.
*
* @param \Drupal\taxonomy\TermInterface $term
* The taxonomy term.
*
* @return array
* The serialized term data.
*/
protected function serializeTaxonomyTerm(TermInterface $term): array {
return [
'uuid' => $term->uuid(),
'name' => $term->getName(),
'description' => $term->getDescription(),
'vocabulary' => $term->bundle(),
];
}
/**
* Serialize a media reference (1 level only - no recursion).
*
* @param \Drupal\media\MediaInterface $media
* The referenced media entity.
*
* @return array
* The serialized media reference data.
*/
protected function serializeMediaReference(MediaInterface $media): array {
$data = [
'uuid' => $media->uuid(),
'mid' => (int) $media->id(),
'bundle' => $media->bundle(),
'name' => $media->getName(),
'status' => $media->isPublished(),
'created' => $this->formatTimestamp((int) $media->getCreatedTime()),
'changed' => $this->formatTimestamp((int) $media->getChangedTime()),
];
// Get the source field value.
$sourceField = $media->getSource()->getConfiguration()['source_field'] ?? NULL;
if ($sourceField && $media->hasField($sourceField)) {
$sourceValue = $media->get($sourceField)->first();
if ($sourceValue) {
$file = $sourceValue->entity ?? NULL;
if ($file instanceof FileInterface) {
$data['uri'] = $file->getFileUri();
$data['url'] = \Drupal::service('file_url_generator')->generateAbsoluteString($file->getFileUri());
}
if (isset($sourceValue->alt)) {
$data['alt'] = $sourceValue->alt;
}
}
}
return $data;
}
/**
* Serialize a user entity.
*
* @param \Drupal\user\UserInterface $user
* The user entity.
*
* @return array
* The serialized user data.
*/
protected function serializeUser(UserInterface $user): array {
return [
'uuid' => $user->uuid(),
'uid' => (int) $user->id(),
'name' => $user->getAccountName(),
'display_name' => $user->getDisplayName(),
];
}
/**
* Serialize a node reference (1 level only).
*
* @param \Drupal\node\NodeInterface $node
* The referenced node.
*
* @return array
* The serialized node reference data.
*/
protected function serializeNodeReference(NodeInterface $node): array {
return [
'uuid' => $node->uuid(),
'nid' => (int) $node->id(),
'type' => $node->bundle(),
'title' => $node->getTitle(),
'status' => $node->isPublished(),
'created' => $this->formatTimestamp((int) $node->getCreatedTime()),
'changed' => $this->formatTimestamp((int) $node->getChangedTime()),
];
}
/**
* Serialize a paragraph entity.
*
* @param \Drupal\paragraphs\ParagraphInterface $paragraph
* The paragraph entity.
*
* @return array
* The serialized paragraph data.
*/
protected function serializeParagraph(ParagraphInterface $paragraph): array {
$data = [
'uuid' => $paragraph->uuid(),
'type' => $paragraph->bundle(),
'fields' => [],
];
$fieldDefinitions = $paragraph->getFieldDefinitions();
foreach ($fieldDefinitions as $fieldName => $definition) {
// Skip base paragraph fields.
if (in_array($fieldName, ['id', 'uuid', 'revision_id', 'langcode', 'type', 'status', 'created', 'parent_id', 'parent_type', 'parent_field_name', 'behavior_settings', 'default_langcode', 'revision_default', 'revision_translation_affected'], TRUE)) {
continue;
}
$fieldItems = $paragraph->get($fieldName);
if ($fieldItems->isEmpty()) {
continue;
}
$data['fields'][$fieldName] = $this->serializeFieldItems($fieldItems);
}
return $data;
}
/**
* Serialize a file entity.
*
* @param mixed $file
* The file entity.
*
* @return array|null
* The serialized file data or NULL.
*/
protected function serializeFileEntity(mixed $file): ?array {
if (!$file) {
return NULL;
}
return [
'uuid' => $file->uuid(),
'filename' => $file->getFilename(),
'uri' => $file->getFileUri(),
'filemime' => $file->getMimeType(),
'filesize' => (int) $file->getSize(),
];
}
/**
* Format a timestamp as ISO 8601.
*
* @param int $timestamp
* The Unix timestamp.
*
* @return string
* The formatted date string.
*/
protected function formatTimestamp(int $timestamp): string {
return date('c', $timestamp);
}
}