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