content_workflow_bynder-1.0.0/content_workflow_bynder_upload/src/Export/Exporter.php
content_workflow_bynder_upload/src/Export/Exporter.php
<?php namespace Drupal\content_workflow_bynder_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\content_workflow_bynder\Entity\MappingInterface; use Drupal\content_workflow_bynder\MetatagQuery; use Drupal\content_workflow_bynder_upload\Event\ContentWorkflowBynderUploadContentEvents; use Drupal\content_workflow_bynder_upload\Event\PostNodeUploadEvent; use Drupal\content_workflow_bynder_upload\Event\PreNodeUploadEvent; use Exception; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Class for handling import/update logic from Content Workflow to Drupal. */ class Exporter implements ContainerInjectionInterface { /** * Drupal ContentWorkflowBynder Client. * * @var \Drupal\content_workflow_bynder\DrupalContentWorkflowBynderClient */ protected $client; /** * Meta tag Query. * * @var \Drupal\content_workflow_bynder\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\content_workflow_bynder\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('content_workflow_bynder.client'), $container->get('content_workflow_bynder.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\content_workflow_bynder\Entity\MappingInterface $mapping * Mapping object. * @param int|null $cwbId * ContentWorkflowBynder ID. * @param array $context * Batch context. * * @return int|null|string * Returns entity ID. * * @throws \Exception */ public function export(EntityInterface $entity, MappingInterface $mapping, $cwbId = NULL, array &$context = []) { $this->collectedReferenceRevisions = []; $data = $this->processGroups($entity, $mapping); $event = $this->eventDispatcher ->dispatch(new PreNodeUploadEvent($entity, $data), ContentWorkflowBynderUploadContentEvents::PRE_NODE_UPLOAD); /** @var \Drupal\content_workflow_bynder_upload\Event\PreNodeUploadEvent $event */ $data = $event->getGathercontentValues(); if (!empty($cwbId)) { $item = $this->client->itemUpdatePost($cwbId, $data['content'], $data['assets']); $this->updateFileCwbIds($item->assets); } else { $data['name'] = $entity->label(); $data['template_id'] = $mapping->getGathercontentTemplateId(); $item = $this->client->itemPost($mapping->getGathercontentProjectId(), new Item($data)); $cwbId = $item['data']->id; $this->updateFileCwbIds($item['meta']->assets); } $this->eventDispatcher ->dispatch(new PostNodeUploadEvent($entity, $data), ContentWorkflowBynderUploadContentEvents::POST_NODE_UPLOAD); if (empty($context['results']['mappings'][$mapping->id()])) { $context['results']['mappings'][$mapping->id()] = [ 'mapping' => $mapping, 'cwbIds' => [ $cwbId => [], ], ]; } $context['results']['mappings'][$mapping->id()]['cwbIds'][$cwbId][] = $entity; foreach ($this->collectedReferenceRevisions as $reference) { $context['results']['mappings'][$mapping->id()]['cwbIds'][$cwbId][] = $reference; } return $entity->id(); } /** * Manages the panes and changes the Item object values. * * @param \Drupal\Core\Entity\EntityInterface $entity * Entity object. * @param \Drupal\content_workflow_bynder\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(); $isCwbFieldRepeatable = FALSE; if (property_exists($field, 'metadata')) { if (!empty($field->metadata) && property_exists($field->metadata, 'repeatable')) { $isCwbFieldRepeatable = $field->metadata->repeatable->isRepeatable; } } if ($isMultiple && $isCwbFieldRepeatable && 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('cwb_file_id')->isEmpty()) { continue; } $value[] = $file->get('cwb_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->contentworkflowbynder_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('cwb_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 CWB ID for a given file. * * @param array $returnedAssets * The assets returned by CWB. * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException * @throws \Drupal\Core\Entity\EntityStorageException */ public function updateFileCwbIds(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('cwb_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; } }