You are here

PullIntent.php in CMS Content Sync 8

Same filename and directory in other branches
  1. 2.1.x src/PullIntent.php
  2. 2.0.x src/PullIntent.php

File

src/PullIntent.php
View source
<?php

namespace Drupal\cms_content_sync;

use Drupal\cms_content_sync\Controller\ContentSyncSettings;
use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Entity\Pool;
use Drupal\cms_content_sync\Event\AfterEntityPull;
use Drupal\cms_content_sync\Plugin\cms_content_sync\entity_handler\DefaultTaxonomyHandler;
use Drupal\cms_content_sync\Plugin\Type\EntityHandlerPluginManager;
use Drupal\field_collection\Entity\FieldCollectionItem;
class PullIntent extends SyncIntent {

  /**
   * @var string PULL_DISABLED
   *             Disable pull completely for this entity type, unless forced.
   *             - used as a configuration option
   *             - not used as $action
   */
  public const PULL_DISABLED = 'disabled';

  /**
   * @var string PULL_AUTOMATICALLY
   *             Automatically pull all entities of this entity type.
   *             - used as a configuration option
   *             - used as $action
   */
  public const PULL_AUTOMATICALLY = 'automatically';

  /**
   * @var string PULL_MANUALLY
   *             Pull only some of these entities, chosen manually.
   *             - used as a configuration option
   *             - used as $action
   */
  public const PULL_MANUALLY = 'manually';

  /**
   * @var string PULL_AS_DEPENDENCY
   *             Pull only some of these entities, pulled if other pulled entities
   *             use it.
   *             - used as a configuration option
   *             - used as $action
   */
  public const PULL_AS_DEPENDENCY = 'dependency';

  /**
   * @var string PULL_FORCED
   *             Force the entity to be pulled (as long as a handler is also selected).
   *             Can be used programmatically for custom workflows.
   *             - not used as a configuration option
   *             - used as $action
   */
  public const PULL_FORCED = 'forced';

  /**
   * @var string PULL_UPDATE_IGNORE
   *             Ignore all incoming updates
   */
  public const PULL_UPDATE_IGNORE = 'ignore';

  /**
   * @var string PULL_UPDATE_FORCE
   *             Overwrite any local changes on all updates
   */
  public const PULL_UPDATE_FORCE = 'force';

  /**
   * @var string PULL_UPDATE_FORCE_AND_FORBID_EDITING
   *             Pull all changes and forbid local editors to change the content
   */
  public const PULL_UPDATE_FORCE_AND_FORBID_EDITING = 'force_and_forbid_editing';

  /**
   * @var string PULL_UPDATE_FORCE_UNLESS_OVERRIDDEN
   *             Pull all changes and forbid local editors to change the content unless
   *             they check the "override" checkbox. As long as that is checked, we
   *             ignore any incoming updates in favor of the local changes.
   */
  public const PULL_UPDATE_FORCE_UNLESS_OVERRIDDEN = 'allow_override';

  /**
   * @var string PULL_UPDATE_UNPUBLISHED
   *             Pull all changes and as an unpublished revision. Only available for nodes
   *             that are revisionable.
   */
  public const PULL_UPDATE_UNPUBLISHED = 'pull_update_unpublished';

  /**
   * @var string PULL_FAILED_DIFFERENT_VERSION
   *             The remote entity type version is different to the local entity type version
   */
  public const PULL_FAILED_DIFFERENT_VERSION = 'import_failed_different_version';

  /**
   * @var string PULL_FAILED_INTERNAL_ERROR
   *             An internal Content Sync error occurred when trying to pull the entity
   */
  public const PULL_FAILED_CONTENT_SYNC_ERROR = 'import_failed_content_sync_error';

  /**
   * @var string PULL_FAILED_INTERNAL_ERROR
   *             An unexpected error occurred when trying to pull the entity
   */
  public const PULL_FAILED_INTERNAL_ERROR = 'import_failed_internal_error';

  /**
   * @var string PULL_FAILED_UNKNOWN_POOL
   *             Soft: The provided Pool doesn't exist
   */
  public const PULL_FAILED_UNKNOWN_POOL = 'import_failed_unknown_pool';

  /**
   * @var string PULL_FAILED_NO_FLOW
   *             Soft: No Flow is configured to pull this entity
   */
  public const PULL_FAILED_NO_FLOW = 'import_failed_no_flow';

  /**
   * @var string PULL_FAILED_HANDLER_DENIED
   *             Soft: The pull failed because the handler returned FALSE when executing the pull
   */
  public const PULL_FAILED_HANDLER_DENIED = 'import_failed_handler_denied';
  protected $mergeChanges;

  /**
   * @var array
   */
  protected $overriddenProperties = [];

  /**
   * @var array
   */
  protected $overriddenTranslatedProperties = [];

  /**
   * @var \EdgeBox\SyncCore\Interfaces\Syndication\IPullOperation
   */
  protected $operation;

  /**
   * SyncIntent constructor.
   *
   * @param \Drupal\cms_content_sync\Entity\Flow                    $flow
   *                                                                             {@see SyncIntent::$sync}
   * @param \Drupal\cms_content_sync\Entity\Pool                    $pool
   *                                                                             {@see SyncIntent::$pool}
   * @param string                                                  $reason
   *                                                                             {@see Flow::PUSH_*} or {@see Flow::PULL_*}
   * @param string                                                  $action
   *                                                                             {@see ::ACTION_*}
   * @param string                                                  $entity_type
   *                                                                             {@see SyncIntent::$entityType}
   * @param string                                                  $bundle
   *                                                                             {@see SyncIntent::$bundle}
   * @param \EdgeBox\SyncCore\Interfaces\Syndication\IPullOperation $operation
   *                                                                             The data provided from Sync Core for pulls.
   *                                                                             Format is the same as in ::getData()
   * @param null                                                    $parent_type
   * @param null                                                    $parent_uuid
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function __construct(Flow $flow, Pool $pool, $reason, $action, $entity_type, $bundle, $operation, $parent_type = null, $parent_uuid = null) {
    $this->operation = $operation;
    if (EntityHandlerPluginManager::isEntityTypeConfiguration($entity_type)) {
      $entity_id = $this->operation
        ->getId();
    }
    else {
      $entity_id = null;
    }
    parent::__construct($flow, $pool, $reason, $action, $entity_type, $bundle, $this->operation
      ->getUuid(), $entity_id, $this->operation
      ->getSourceUrl());
    if ($this->operation
      ->getSourceUrl()) {
      $this->entity_status
        ->set('source_url', $this->operation
        ->getSourceUrl());
    }
    if ($parent_type && $parent_uuid) {
      $this->entity_status
        ->wasPulledEmbedded(true);
      $this->entity_status
        ->setParentEntity($parent_type, $parent_uuid);
    }
    else {
      $this->entity_status
        ->wasPulledEmbedded(false);
    }
    $this->mergeChanges = PullIntent::PULL_UPDATE_FORCE_UNLESS_OVERRIDDEN == $this->flow
      ->getEntityTypeConfig($this->entityType, $this->bundle)['import_updates'] && $this->entity_status
      ->isOverriddenLocally();
  }

  /**
   * @return \EdgeBox\SyncCore\Interfaces\Syndication\IPullOperation
   */
  public function getOperation() {
    return $this->operation;
  }

  /**
   * Mark the given dependency as missing so it's automatically resolved whenever it gets pulled.
   *
   * @param array       $definition
   * @param null|string $field
   * @param null|array  $data
   */
  public function saveUnresolvedDependency($definition, $field = null, $data = null) {
    $reference = $this->operation
      ->loadReference($definition);

    // User references are ignored.
    if (!$reference
      ->getType() || !$reference
      ->getId() && !$reference
      ->getUuid()) {
      return;
    }
    MissingDependencyManager::saveUnresolvedDependency($reference
      ->getType(), $reference
      ->getId() ? $reference
      ->getId() : $reference
      ->getUuid(), $this
      ->getEntity(), $this
      ->getReason(), $field, $data);
  }

  /**
   * @return bool
   */
  public function shouldMergeChanges() {
    return $this->mergeChanges;
  }

  /**
   * Pull the provided entity.
   *
   * @throws Exception\SyncException
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   *
   * @return bool
   */
  public function execute() {
    $pull = $this->pool
      ->getNewestTimestamp($this->entityType, $this->uuid, true);
    if (!$pull) {
      if (SyncIntent::ACTION_UPDATE == $this->action) {
        $this->action = SyncIntent::ACTION_CREATE;
      }
    }
    elseif (SyncIntent::ACTION_CREATE == $this->action) {
      $this->action = SyncIntent::ACTION_UPDATE;
    }
    $pull = time();
    $config = $this->flow
      ->getEntityTypeConfig($this->entityType, $this->bundle);
    $handler = $this->flow
      ->getEntityTypeHandler($config);
    self::entityHasBeenPulledFromRemoteSite($this->entityType, $this->uuid, true);
    $result = $handler
      ->pull($this);
    \Drupal::logger('cms_content_sync')
      ->info('@not PULL @action @entity_type:@bundle @uuid @reason: @message<br>Flow: @flow_id | Pool: @pool_id', [
      '@reason' => $this->reason,
      '@action' => $this->action,
      '@entity_type' => $this->entityType,
      '@bundle' => $this->bundle,
      '@uuid' => $this->uuid,
      '@not' => $result ? '' : 'NO',
      '@message' => $result ? t('The entity has been pulled.') : t('The entity handler denied to pull this entity.'),
      '@flow_id' => $this
        ->getFlow()
        ->id(),
      '@pool_id' => $this
        ->getPool()
        ->id(),
    ]);

    // Don't save entity_status entity if entity wasn't pulled anyway.
    if (!$result) {
      return false;
    }

    // Need to save after setting timestamp to prevent exception.
    $this->entity_status
      ->setLastPull($pull);
    $this->pool
      ->setTimestamp($this->entityType, $this->uuid, $pull, true);
    $this->entity_status
      ->isDeleted(SyncIntent::ACTION_DELETE == $this->action);
    $this->entity_status
      ->save();
    if (SyncIntent::ACTION_DELETE == $this->action) {
      $this->pool
        ->markDeleted($this->entityType, $this->uuid);
    }
    $entity = $this
      ->getEntity();

    // Dispatch EntityExport event to give other modules the possibility to react on it.
    // Ignore deleted entities.
    if ($entity) {
      \Drupal::service('event_dispatcher')
        ->dispatch(AfterEntityPull::EVENT_NAME, new AfterEntityPull($entity, $this));
    }

    // Handle Extended Entity Import logging.
    $settings = ContentSyncSettings::getInstance();
    if ($settings
      ->getExtendedEntityImportLogging()) {
      $url = null;
      if ($entity && $entity
        ->hasLinkTemplate('canonical') && !$entity instanceof FieldCollectionItem) {
        $url = $entity
          ->toUrl('canonical', [
          'absolute' => true,
        ])
          ->toString(true)
          ->getGeneratedUrl();
      }
      $serializer = \Drupal::service('serializer');
      $data = $serializer
        ->serialize($this
        ->getOperation()
        ->getResponseBody($url), 'json', [
        'plugin_id' => 'entity',
      ]);
      \Drupal::logger('cms_content_sync_entity_import_log')
        ->debug('%entity_type - %uuid <br>Data: <br><pre><code>%data</code></pre>', [
        '%entity_type' => $entity
          ->getEntityTypeId(),
        '%uuid' => $entity
          ->uuid(),
        '%data' => $data,
      ]);
    }
    $this
      ->resolveMissingDependencies();
    return true;
  }

  /**
   * Check if the provided entity has just been pulled by Sync Core in this
   * very request. In this case it doesn't make sense to perform a remote
   * request telling Sync Core it has been created/updated/deleted
   * (it will know as a result of this current request).
   *
   * @param string $entity_type
   *                            The entity type
   * @param string $entity_uuid
   *                            The entity UUID
   * @param bool   $set
   *                            If TRUE, this entity will be set to have been pulled at this request
   *
   * @return bool
   */
  public static function entityHasBeenPulledFromRemoteSite($entity_type = null, $entity_uuid = null, $set = false) {
    static $entities = [];
    if (!$entity_type) {
      return !empty($entities);
    }
    if ($set) {
      return $entities[$entity_type][$entity_uuid] = true;
    }
    return !empty($entities[$entity_type][$entity_uuid]);
  }

  /**
   * Restore an entity that was added via
   * {@see SyncIntent::embedEntityDefinition} or
   * {@see SyncIntent::embedEntity}.
   *
   * @param array $definition
   *                          The definition you saved in a field and gotten
   *                          back when calling one of the mentioned functions above
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   *
   * @return \Drupal\Core\Entity\EntityInterface the restored entity
   */
  public function loadEmbeddedEntity($definition) {
    $reference = $this->operation
      ->loadReference($definition);
    $expected_version = Flow::getEntityTypeVersion($reference
      ->getType(), $reference
      ->getBundle());
    if ($expected_version != $reference
      ->getVersion()) {
      \Drupal::logger('cms_content_sync')
        ->warning('Outdated reference to @entity_type:@bundle: Remote version @remote_version doesn\'t match local version @local_version<br>Flow: @flow_id | Pool: @pool_id', [
        '@entity_type' => $reference
          ->getType(),
        '@bundle' => $reference
          ->getBundle(),
        '@remote_version' => $reference
          ->getVersion(),
        '@local_version' => $expected_version,
        '@flow_id' => $this
          ->getFlow()
          ->id(),
        '@pool_id' => $this
          ->getPool()
          ->id(),
      ]);
    }
    if ($reference
      ->isEmbedded()) {
      $embedded_entity = $reference
        ->getEmbeddedEntity();
      if (empty($embedded_entity)) {
        return null;
      }
      $pool_id = $reference
        ->getPoolId();
      $pool = Pool::getAll()[$pool_id];
      if (empty($pool)) {
        return null;
      }
      $entity_type_name = $reference
        ->getType();
      $entity_bundle = $reference
        ->getBundle();
      $flow = Flow::getFlowForApiAndEntityType($pool, $entity_type_name, $entity_bundle, PullIntent::PULL_AS_DEPENDENCY, SyncIntent::ACTION_CREATE);
      if (!$flow) {
        return null;
      }
      $intent = new PullIntent($flow, $pool, PullIntent::PULL_AS_DEPENDENCY, SyncIntent::ACTION_CREATE, $entity_type_name, $entity_bundle, $embedded_entity, $this->entityType, $this->uuid);
      $status = $intent
        ->execute();
      if (!$status) {
        return null;
      }
      return $intent
        ->getEntity();
    }
    if (empty($reference
      ->getId())) {
      $entity = \Drupal::service('entity.repository')
        ->loadEntityByUuid($reference
        ->getType(), $reference
        ->getUuid());
    }
    else {
      $entity = \Drupal::entityTypeManager()
        ->getStorage($reference
        ->getType())
        ->load($reference
        ->getId());
    }

    // Taxonomy terms can be mapped by their name.
    if (!$entity && !empty($reference
      ->getName())) {
      $config = $this->flow
        ->getEntityTypeConfig($reference
        ->getType(), $reference
        ->getBundle());
      if (!empty($config) && !empty($config['handler_settings'][DefaultTaxonomyHandler::MAP_BY_LABEL_SETTING])) {
        $entity_type = \Drupal::entityTypeManager()
          ->getDefinition($reference
          ->getType());
        $label_property = $entity_type
          ->getKey('label');
        $existing = \Drupal::entityTypeManager()
          ->getStorage($reference
          ->getType())
          ->loadByProperties([
          $label_property => $reference
            ->getName(),
        ]);
        $entity = reset($existing);
      }
    }
    return $entity;
  }

  /**
   * Get all embedded entity data besides the predefined keys.
   * Images for example have "alt" and "title" in addition to the file reference.
   *
   * @param $definition
   *
   * @return array
   */
  public function getEmbeddedEntityData($definition) {
    $reference = $this->operation
      ->loadReference($definition);
    return $reference
      ->getDetails();
  }

  /**
   * Get all field values at once for the currently active language.
   *
   * @return array all field values for the active language
   */
  public function getOverriddenProperties() {
    if ($this->activeLanguage) {
      if (!isset($this->overriddenTranslatedProperties[$this->activeLanguage])) {
        return null;
      }
      return $this->overriddenTranslatedProperties[$this->activeLanguage];
    }
    return $this->overriddenProperties;
  }

  /**
   * Provide the value of a field you stored when pushing by using.
   *
   * @see SyncIntent::setField()
   *
   * @param string $name
   *                     The name of the field to restore
   *
   * @return mixed the value you stored for this field
   */
  public function getProperty($name) {
    $overridden = $this
      ->getOverriddenProperties();
    return isset($overridden[$name]) ? $overridden[$name] : $this->operation
      ->getProperty($name, $this->activeLanguage);
  }

  /**
   * Overwrite the value for the given field to preprocess the values or access
   * them at a later stage.
   *
   * @param string $name
   *                      The name of the field in question
   * @param mixed  $value
   *                      The value to store
   */
  public function overwriteProperty($name, $value) {
    if ($this->activeLanguage) {
      $this->overriddenTranslatedProperties[$this->activeLanguage][$name] = $value;
      return;
    }
    $this->overriddenProperties[$name] = $value;
  }

  /**
   * Get all languages for field translations that are currently used.
   */
  public function getTranslationLanguages() {
    return $this->operation
      ->getUsedTranslationLanguages();
  }

  /**
   * Resolve all references to the entity that has just been pulled if they're missing at other content.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  protected function resolveMissingDependencies() {

    // Ignore deleted entities.
    if ($this
      ->getEntity()) {
      MissingDependencyManager::resolveDependencies($this
        ->getEntity());
    }
  }

}

Classes

Namesort descending Description
PullIntent