gathercontent-8.x-5.0/gathercontent_upload/src/Export/Exporter.php
gathercontent_upload/src/Export/Exporter.php
<?php
namespace Drupal\gathercontent_upload\Export;
use GatherContent\DataTypes\Item;
use GatherContent\GatherContentClientInterface;
use Drupal;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Language\Language;
use Drupal\Core\Language\LanguageInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\gathercontent\Entity\MappingInterface;
use Drupal\gathercontent\MetatagQuery;
use Drupal\gathercontent_upload\Event\GatherUploadContentEvents;
use Drupal\gathercontent_upload\Event\PostNodeUploadEvent;
use Drupal\gathercontent_upload\Event\PreNodeUploadEvent;
use Exception;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Class for handling import/update logic from GatherContent to Drupal.
*/
class Exporter implements ContainerInjectionInterface {
/**
* Drupal GatherContent Client.
*
* @var \Drupal\gathercontent\DrupalGatherContentClient
*/
protected $client;
/**
* Meta tag Query.
*
* @var \Drupal\gathercontent\MetatagQuery
*/
protected $metatag;
/**
* Entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Content translation manager.
*
* @var \Drupal\content_translation\ContentTranslationManagerInterface
*/
protected $contentTranslation;
/**
* Filesystem service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* Collected reference revisions.
*
* @var array
*/
protected $collectedReferenceRevisions = [];
/**
* Collected file fields.
*
* @var array
*/
protected $collectedFileFields = [];
/**
* List of allowed repeatable Drupal field types.
*
* @var array
*/
const ALLOWED_MULTI_VALUE_TYPES = [
'text',
'text_long',
'text_with_summary',
];
/**
* Exporter constructor.
*
* @param \GatherContent\GatherContentClientInterface $client
* @param \Drupal\gathercontent\MetatagQuery $metatag
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $eventDispatcher
* @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
* @param \Drupal\Core\File\FileSystemInterface $fileSystem
*/
public function __construct(
GatherContentClientInterface $client,
MetatagQuery $metatag,
EntityTypeManagerInterface $entityTypeManager,
EventDispatcherInterface $eventDispatcher,
ModuleHandlerInterface $moduleHandler,
FileSystemInterface $fileSystem
) {
$this->client = $client;
$this->metatag = $metatag;
$this->entityTypeManager = $entityTypeManager;
$this->eventDispatcher = $eventDispatcher;
$this->moduleHandler = $moduleHandler;
$this->fileSystem = $fileSystem;
if ($this->moduleHandler->moduleExists('content_translation')) {
$this->contentTranslation = Drupal::service('content_translation.manager');
}
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('gathercontent.client'),
$container->get('gathercontent.metatag'),
$container->get('entity_type.manager'),
$container->get('event_dispatcher'),
$container->get('module_handler'),
$container->get('file_system')
);
}
/**
* Getter GatherContentClient.
*/
public function getClient() {
return $this->client;
}
/**
* Exports the changes made in Drupal contents.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* Entity entity object.
* @param \Drupal\gathercontent\Entity\MappingInterface $mapping
* Mapping object.
* @param int|null $gcId
* GatherContent ID.
* @param array $context
* Batch context.
*
* @return int|null|string
* Returns entity ID.
*
* @throws \Exception
*/
public function export(EntityInterface $entity, MappingInterface $mapping, $gcId = NULL, array &$context = []) {
$this->collectedReferenceRevisions = [];
$data = $this->processGroups($entity, $mapping);
$event = $this->eventDispatcher
->dispatch(new PreNodeUploadEvent($entity, $data), GatherUploadContentEvents::PRE_NODE_UPLOAD);
/** @var \Drupal\gathercontent_upload\Event\PreNodeUploadEvent $event */
$data = $event->getGathercontentValues();
if (!empty($gcId)) {
$item = $this->client->itemUpdatePost($gcId, $data['content'], $data['assets']);
$this->updateFileGcIds($item->assets);
}
else {
$data['name'] = $entity->label();
$data['template_id'] = $mapping->getGathercontentTemplateId();
$item = $this->client->itemPost($mapping->getGathercontentProjectId(), new Item($data));
$gcId = $item['data']->id;
$this->updateFileGcIds($item['meta']->assets);
}
$this->eventDispatcher
->dispatch(new PostNodeUploadEvent($entity, $data), GatherUploadContentEvents::POST_NODE_UPLOAD);
if (empty($context['results']['mappings'][$mapping->id()])) {
$context['results']['mappings'][$mapping->id()] = [
'mapping' => $mapping,
'gcIds' => [
$gcId => [],
],
];
}
$context['results']['mappings'][$mapping->id()]['gcIds'][$gcId][] = $entity;
foreach ($this->collectedReferenceRevisions as $reference) {
$context['results']['mappings'][$mapping->id()]['gcIds'][$gcId][] = $reference;
}
return $entity->id();
}
/**
* Manages the panes and changes the Item object values.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* Entity object.
* @param \Drupal\gathercontent\Entity\MappingInterface $mapping
* Mappig object.
*
* @return array
* Returns Content array.
*
* @throws \Exception
*/
public function processGroups(EntityInterface $entity, MappingInterface $mapping) {
$mappingData = unserialize($mapping->getData());
if (empty($mappingData)) {
throw new Exception("Mapping data is empty.");
}
$templateData = unserialize($mapping->getTemplate());
$data = [
'content' => [],
'assets' => [],
];
foreach ($templateData->related->structure->groups as $group) {
$isTranslatable = $this->moduleHandler->moduleExists('content_translation')
&& $this->contentTranslation->isEnabled($mapping->getMappedEntityType(), $mapping->getContentType())
&& isset($mappingData[$group->uuid]['language'])
&& ($mappingData[$group->uuid]['language'] != Language::LANGCODE_NOT_SPECIFIED);
if ($isTranslatable) {
$language = $mappingData[$group->uuid]['language'];
}
else {
$language = Language::LANGCODE_NOT_SPECIFIED;
}
$fields = $this->processFields($group, $entity, $mappingData, $isTranslatable, $language);
$data['content'] += $fields['content'];
$data['assets'] += $fields['assets'];
}
return $data;
}
/**
* Processes field data.
*
* @param object $group
* Group object.
* @param \Drupal\Core\Entity\EntityInterface $entity
* Entity.
* @param array $mappingData
* Mapping array.
* @param bool $isTranslatable
* Translatable.
* @param string $language
* Language.
*
* @return array
* Returns data.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function processFields(object $group, EntityInterface $entity, array $mappingData, bool $isTranslatable, string $language) {
$exportedFields = [];
$fields = [];
$assets = [];
foreach ($group->fields as $field) {
// Skip field if it is not mapped.
if (empty($mappingData[$group->uuid]['elements'][$field->uuid])) {
continue;
}
$localFieldId = $mappingData[$group->uuid]['elements'][$field->uuid];
if ((isset($mappingData[$group->uuid]['type'])
&& $mappingData[$group->uuid]['type'] === 'content')
|| !isset($mappingData[$group->uuid]['type'])
) {
$localIdArray = explode('||', $localFieldId);
/** @var \Drupal\field\Entity\FieldConfig $fieldInfo */
$fieldInfo = FieldConfig::load($localIdArray[0]);
$currentEntity = $entity;
$type = '';
$bundle = '';
$titleField = $currentEntity->getEntityTypeId() . '.' . $currentEntity->bundle() . '.title';
if ($localIdArray[0] === $titleField
|| $localIdArray[0] === 'title'
) {
$currentFieldName = 'title';
}
else {
$currentFieldName = $fieldInfo->getName();
$type = $fieldInfo->getType();
$bundle = $fieldInfo->getTargetBundle();
}
// Get the deepest field's value, we need this to collect
// the referenced entities values.
$this->processTargets($currentEntity, $currentFieldName, $type, $bundle, $exportedFields, $localIdArray, $isTranslatable, $language);
$this->collectedReferenceRevisions[] = $currentEntity;
$isRepeatable = FALSE;
if ($fieldInfo) {
$fieldType = $fieldInfo->getType();
// Field can be an entity reference.
if (!in_array($fieldType, self::ALLOWED_MULTI_VALUE_TYPES)) {
if (!empty($localIdArray[1])) {
$fieldInfo = FieldConfig::load($localIdArray[1]);
}
}
if ($fieldInfo) {
$fieldType = $fieldInfo->getType();
$isMultiple = $fieldInfo->getFieldStorageDefinition()->isMultiple();
$isGcFieldRepeatable = FALSE;
if (property_exists($field, 'metadata')) {
if (!empty($field->metadata) && property_exists($field->metadata, 'repeatable')) {
$isGcFieldRepeatable = $field->metadata->repeatable->isRepeatable;
}
}
if ($isMultiple
&& $isGcFieldRepeatable
&& in_array($fieldType, self::ALLOWED_MULTI_VALUE_TYPES)
) {
$isRepeatable = TRUE;
}
}
}
$value = $this->processSetFields($field, $currentEntity, $isTranslatable, $language, $currentFieldName, $bundle, $isRepeatable);
if (!empty($value)) {
$fields[$field->uuid] = $value;
}
$asset = $this->processSetAssets($field, $currentEntity, $isTranslatable, $language, $currentFieldName);
if (!empty($asset)) {
$assets[$field->uuid] = $asset;
}
}
elseif ($mappingData[$group->uuid]['type'] === 'metatag') {
if ($this->moduleHandler->moduleExists('metatag')
&& $this->metatag->checkMetatag($entity->getEntityTypeId(), $entity->bundle())
) {
$fields[$field->uuid] = $this->processMetaTagFields($entity, $localFieldId, $isTranslatable, $language);
}
}
}
return [
'content' => $fields,
'assets' => $assets,
];
}
/**
* Processes the target ids for a field.
*
* @param \Drupal\Core\Entity\EntityInterface $currentEntity
* Entity object.
* @param string $currentFieldName
* Current field name.
* @param string $type
* Current type name.
* @param string $bundle
* Current bundle name.
* @param array $exportedFields
* Array of exported fields, preventing duplications.
* @param array $localIdArray
* Array of mapped embedded field id array.
* @param bool $isTranslatable
* Translatable.
* @param string $language
* Language.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function processTargets(EntityInterface &$currentEntity, string &$currentFieldName, string &$type, string &$bundle, array &$exportedFields, array $localIdArray, bool $isTranslatable, string $language) {
$idCount = count($localIdArray);
// Loop through the references, going deeper and deeper.
for ($i = 0; $i < $idCount - 1; $i++) {
$localId = $localIdArray[$i];
$fieldInfo = FieldConfig::load($localId);
$currentFieldName = $fieldInfo->getName();
$type = $fieldInfo->getType();
$bundle = $fieldInfo->getTargetBundle();
if ($isTranslatable && $currentEntity->hasTranslation($language)) {
$targetFieldValue = $currentEntity->getTranslation($language)->get($currentFieldName)->getValue();
}
else {
$targetFieldValue = $currentEntity->get($currentFieldName)->getValue();
}
// Load the targeted entity and process the data.
if (!empty($targetFieldValue)) {
$fieldTargetInfo = FieldConfig::load($localIdArray[$i + 1]);
$entityStorage = $this->entityTypeManager
->getStorage($fieldTargetInfo->getTargetEntityTypeId());
$childFieldName = $fieldTargetInfo->getName();
$childType = $fieldInfo->getType();
$childBundle = $fieldInfo->getTargetBundle();
foreach ($targetFieldValue as $target) {
$exportKey = $target['target_id'] . '_' . $childFieldName;
// The field is already collected.
if (!empty($exportedFields[$exportKey])) {
continue;
}
$childEntity = $entityStorage->loadByProperties([
'id' => $target['target_id'],
'type' => $fieldTargetInfo->getTargetBundle(),
]);
if (!empty($childEntity[$target['target_id']])) {
$currentEntity = $childEntity[$target['target_id']];
$currentFieldName = $childFieldName;
$type = $childType;
$bundle = $childBundle;
if ($i == ($idCount - 2)) {
$exportedFields[$exportKey] = TRUE;
}
break;
}
}
}
}
}
/**
* Processes meta fields.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* Entity object.
* @param string $localFieldName
* Field name.
* @param bool $isTranslatable
* Translatable bool.
* @param string $language
* Language string.
*
* @return string
* Returns value.
*/
public function processMetaTagFields(EntityInterface $entity, string $localFieldName, bool $isTranslatable, string $language) {
$fieldName = $this->metatag->getFirstMetatagField($entity->getEntityTypeId(), $entity->bundle());
if ($isTranslatable && $entity->hasTranslation($language)) {
$currentValue = unserialize($entity->getTranslation($language)->{$fieldName}->value);
}
else {
$currentValue = unserialize($entity->{$fieldName}->value);
}
return $currentValue[$localFieldName] ?? '';
}
/**
* Set value of the field.
*
* @param object $field
* Field object.
* @param \Drupal\Core\Entity\EntityInterface $entity
* Entity object.
* @param bool $isTranslatable
* Translatable bool.
* @param string $language
* Language string.
* @param string $localFieldName
* Field Name.
* @param string $bundle
* Local field Info bundle string.
* @param bool $isRepeatable
* Repeatable bool.
*
* @return array|string
* Returns value.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function processSetFields(object $field, EntityInterface $entity, bool $isTranslatable, string $language, string $localFieldName, string $bundle, bool $isRepeatable) {
$value = NULL;
switch ($field->field_type) {
case 'attachment':
// Fetch file targets.
if ($isTranslatable && $entity->hasTranslation($language)) {
$targets = $entity->getTranslation($language)->{$localFieldName}->getValue();
}
else {
$targets = $entity->{$localFieldName}->getValue();
}
$value = [];
foreach ($targets as $target) {
$file = $this->entityTypeManager
->getStorage('file')
->load($target['target_id']);
if (empty($file) || $file->get('gc_file_id')->isEmpty()) {
continue;
}
$value[] = $file->get('gc_file_id')->first()->getValue()['value'];
}
break;
case 'choice_radio':
case 'choice_checkbox':
// Fetch local selected option.
if ($isTranslatable && $entity->hasTranslation($language)) {
$targets = $entity->getTranslation($language)->{$localFieldName}->getValue();
}
else {
$targets = $entity->{$localFieldName}->getValue();
}
$value = [];
foreach ($targets as $target) {
$conditionArray = [
'tid' => $target['target_id'],
];
if (
$isTranslatable &&
$this->moduleHandler->moduleExists('content_translation') &&
$this->contentTranslation->isEnabled('taxonomy_term', $bundle) &&
$language !== LanguageInterface::LANGCODE_NOT_SPECIFIED
) {
$conditionArray['langcode'] = $language;
}
$terms = $this->entityTypeManager
->getStorage('taxonomy_term')
->loadByProperties($conditionArray);
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = array_shift($terms);
if (!empty($term)) {
$optionIds = $term->gathercontent_option_ids->getValue();
$options = $field->metadata->choice_fields->options;
foreach ($optionIds as $optionId) {
if (!$this->validOptionId(
$options,
$optionId['value'])
) {
continue;
}
$value[] = [
'id' => $optionId['value'],
];
}
}
}
break;
case 'guidelines':
// We don't upload this because this field shouldn't be
// edited.
break;
default:
if ($localFieldName === 'title') {
if ($isTranslatable && $entity->hasTranslation($language)) {
$value = $entity->getTranslation($language)->getTitle();
}
else {
$value = $entity->getTitle();
}
}
else {
if ($isTranslatable && $entity->hasTranslation($language)) {
if ($isRepeatable) {
$value = $this->getRepeatableFieldValues($entity->getTranslation($language)->{$localFieldName});
}
else {
$value = $entity->getTranslation($language)->{$localFieldName}->value;
}
}
else {
if ($isRepeatable) {
$value = $this->getRepeatableFieldValues($entity->{$localFieldName});
}
else {
$value = $entity->{$localFieldName}->value;
}
}
}
break;
}
return $value;
}
/**
* Set assets.
*
* @param object $field
* Field object.
* @param \Drupal\Core\Entity\EntityInterface $entity
* Entity object.
* @param bool $isTranslatable
* Translatable bool.
* @param string $language
* Language string.
* @param string $localFieldName
* Field Name.
*
* @return array|string
* Returns value.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function processSetAssets(object $field, EntityInterface $entity, bool $isTranslatable, string $language, string $localFieldName) {
$value = NULL;
switch ($field->field_type) {
case 'attachment':
// Fetch file targets.
if ($isTranslatable && $entity->hasTranslation($language)) {
$targets = $entity->getTranslation($language)->{$localFieldName}->getValue();
}
else {
$targets = $entity->{$localFieldName}->getValue();
}
$value = [];
foreach ($targets as $target) {
/** @var \Drupal\file\FileInterface $file */
$file = $this->entityTypeManager
->getStorage('file')
->load($target['target_id']);
if (empty($file) || !$file->get('gc_file_id')->isEmpty()) {
continue;
}
$value[] = $this->fileSystem->realpath($file->getFileUri());
}
$this->collectedFileFields[$field->uuid] = $targets;
break;
}
return $value;
}
/**
* Updates the file managed table to include the new GC ID for a given file.
*
* @param array $returnedAssets
* The assets returned by GC.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function updateFileGcIds(array $returnedAssets) {
if (empty($this->collectedFileFields) || empty($returnedAssets)) {
return;
}
foreach ($this->collectedFileFields as $fieldUuid => $fileField) {
if (empty($returnedAssets[$fieldUuid])) {
continue;
}
foreach ($fileField as $delta => $target) {
/** @var \Drupal\file\FileInterface $file */
$file = $this->entityTypeManager
->getStorage('file')
->load($target['target_id']);
if (empty($file) || empty($returnedAssets[$fieldUuid][$delta])) {
continue;
}
$file->set('gc_file_id', $returnedAssets[$fieldUuid][$delta]);
$file->save();
}
}
}
/**
* Check if the given option ID is valid for the template.
*
* @param array $options
* Options array.
* @param string $optionId
* Option ID.
*
* @return bool
* Returns if the option ID is valid for a given template.
*/
protected function validOptionId(array $options, string $optionId) {
foreach ($options as $option) {
if ($option->optionId === $optionId) {
return TRUE;
}
}
return FALSE;
}
/**
* Moves field values into an array.
*
* @param \Drupal\Core\Field\FieldItemListInterface $fieldItemList
* The list of field values.
*
* @return array
* Field values in an array.
*/
protected function getRepeatableFieldValues(FieldItemListInterface $fieldItemList): array {
$fieldValues = $fieldItemList->getValue();
$values = [];
foreach ($fieldValues as $fieldValue) {
$values[] = $fieldValue['value'];
}
return $values;
}
}
