You are here

Flow.php in CMS Content Sync 2.1.x

File

src/Entity/Flow.php
View source
<?php

namespace Drupal\cms_content_sync\Entity;

use Drupal\cms_content_sync\Controller\Migration;
use Drupal\cms_content_sync\Controller\FlowControllerPerBundle;
use Drupal\cms_content_sync\Controller\FlowControllerSimple;
use Drupal\cms_content_sync\IFlowController;
use Drupal\cms_content_sync\Plugin\Type\EntityHandlerPluginManager;
use Drupal\cms_content_sync\PullIntent;
use Drupal\cms_content_sync\PushIntent;
use Drupal\cms_content_sync\SyncIntent;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityInterface;

/**
 * Defines the "Content Sync - Flow" entity.
 *
 * @ConfigEntityType(
 *   id = "cms_content_sync_flow",
 *   label = @Translation("Content Sync - Flow"),
 *   handlers = {
 *     "list_builder" = "Drupal\cms_content_sync\Controller\FlowListBuilder",
 *     "form" = {
 *       "add" = "Drupal\cms_content_sync\Form\FlowForm",
 *       "edit" = "Drupal\cms_content_sync\Form\FlowForm",
 *       "delete" = "Drupal\cms_content_sync\Form\FlowDeleteForm",
 *       "copy_remote" = "Drupal\cms_content_sync\Form\CopyRemoteFlow",
 *     }
 *   },
 *   config_prefix = "flow",
 *   admin_permission = "administer cms content sync",
 *   entity_keys = {
 *     "id" = "id",
 *     "label" = "name",
 *   },
 *   config_export = {
 *     "id",
 *     "name",
 *     "variant",
 *     "type",
 *     "simple_settings",
 *     "per_bundle_settings",
 *     "sync_entities",
 *   },
 *   links = {
 *     "edit-form" = "/admin/config/services/cms_content_sync/flow/{cms_content_sync_flow}/edit",
 *     "delete-form" = "/admin/config/services/cms_content_sync/flow/{cms_content_sync_flow}/delete",
 *   }
 * )
 */
class Flow extends ConfigEntityBase implements FlowInterface {

  /**
   * @var string HANDLER_IGNORE
   *             Ignore this entity type / bundle / field completely
   */
  public const HANDLER_IGNORE = 'ignore';

  /**
   * @var string PREVIEW_DISABLED
   *             Hide these entities completely
   */
  public const PREVIEW_DISABLED = 'disabled';

  /**
   * @var string PREVIEW_TABLE
   *             Show these entities in a table view
   */
  public const PREVIEW_TABLE = 'table';

  /**
   * This Flow pushes entities.
   */
  public const TYPE_PUSH = 'push';

  /**
   * This Flow pulls entities.
   */
  public const TYPE_PULL = 'pull';

  /**
   * This Flow pushes and pulls entities.
   *
   * @deprecated will be removed in v2
   */
  public const TYPE_BOTH = 'both';
  public const V2_STATUS_NONE = '';
  public const V2_STATUS_EXPORTED = 'exported';
  public const V2_STATUS_ACTIVE = 'active';
  public const VARIANT_SIMPLE = 'simple';
  public const VARIANT_PER_BUNDLE = 'per-bundle';

  /**
   * The variant. Simple is the new default, per-bundle is the old default for
   * Flows created before v2.1.
   * Simple offers only a handful of options and applies the same settings
   * to all entity types whereas per-bundle mode allows for entity type based
   * management of all configuration; so more complex but also more precise.
   * Simple uses our new embed frontend, so has better UX whereas per-bundle
   * uses clunky Drupal forms.
   *
   * @var string
   */
  public $variant;

  /**
   * Either "push" or "pull".
   *
   * @var string
   */
  public $type;

  /**
   * The Flow ID.
   *
   * @var string
   */
  public $id;

  /**
   * The Flow name.
   *
   * @var string
   */
  public $name;

  /**
   * The settings for variant Simple.
   *
   * @var array
   */
  public $simple_settings;

  /**
   * The settings for variant per-bundle. Hierarchy is:
   * [type machine name e.g. 'node']
   *   [bundle machine name e.g. 'page']
   *     ['settings']
   *       [...]
   *     ['properties']
   *       [property/field name e.g. 'title']
   *         [...]
   *
   * @var array
   */
  public $per_bundle_settings;

  /**
   * Old way to store settings. This is automatically migrated to be stored in
   * the new `$per_bundle_settings` property instead by
   * `cms_content_sync_update_8021`.
   *
   * @var array
   *
   * @deprecated
   */
  public $sync_entities;

  /**
   * @var Flow[]
   *             All content synchronization configs. Use {@see Flow::getAll}
   *             to request them.
   */
  public static $all = null;

  /**
   * Ensure that pools are pulled before the flows.
   */
  public function calculateDependencies() {
    parent::calculateDependencies();
    foreach ($this
      ->getController()
      ->getUsedPools() as $pool) {
      $this
        ->addDependency('config', 'cms_content_sync.pool.' . $pool->id);
    }
  }

  /**
   * @var IFlowController
   */
  protected $handler;

  /**
   * Provide the controller to act upon the stored configuration.
   *
   * @return IFlowController
   */
  public function getController() {
    if (!$this->handler) {
      if ($this->variant === Flow::VARIANT_PER_BUNDLE) {
        $this->handler = new FlowControllerPerBundle($this);
      }
      else {
        $this->handler = new FlowControllerSimple($this);
      }
    }
    return $this->handler;
  }

  /**
   * Get all flows pushing this entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   * @param $action
   * @param bool $include_dependencies
   *
   * @throws \Exception
   *
   * @return array|Flow[]
   */
  public static function getFlowsForPushing($entity, $action, $include_dependencies = true) {
    if (SyncIntent::ACTION_DELETE === $action) {
      $last_push = EntityStatus::getLastPushForEntity($entity);
      if (empty($last_push)) {
        return [];
      }
    }
    $flows = PushIntent::getFlowsForEntity($entity, PushIntent::PUSH_AUTOMATICALLY, $action);
    if (!count($flows) && SyncIntent::ACTION_DELETE === $action) {
      $flows = PushIntent::getFlowsForEntity($entity, PushIntent::PUSH_MANUALLY, $action);
    }
    if ($include_dependencies && !count($flows)) {
      $flows = PushIntent::getFlowsForEntity($entity, PushIntent::PUSH_AS_DEPENDENCY, $action);
      if (count($flows)) {
        $infos = EntityStatus::getInfosForEntity($entity
          ->getEntityTypeId(), $entity
          ->uuid());
        $pushed = [];
        foreach ($infos as $info) {
          if (!in_array($info
            ->getFlow(), $flows)) {
            continue;
          }
          if (in_array($info
            ->getFlow(), $pushed)) {
            continue;
          }
          if (!$info
            ->getLastPush()) {
            continue;
          }
          $pushed[] = $info
            ->getFlow();
        }
        $flows = $pushed;
      }
    }
    return $flows;
  }

  /**
   * Get a unique version hash for the configuration of the provided entity type
   * and bundle.
   *
   * @param string $type_name
   *                            The entity type in question
   * @param string $bundle_name
   *                            The bundle in question
   *
   * @return string
   *                A 32 character MD5 hash of all important configuration for this entity
   *                type and bundle, representing it's current state and allowing potential
   *                conflicts from entity type updates to be handled smoothly
   */
  public static function getEntityTypeVersion($type_name, $bundle_name) {

    // @todo Include export_config keys in version definition for config entity types like webforms.
    if (EntityHandlerPluginManager::isEntityTypeFieldable($type_name)) {
      $entityFieldManager = \Drupal::service('entity_field.manager');
      $field_definitions = $entityFieldManager
        ->getFieldDefinitions($type_name, $bundle_name);
      $field_definitions_array = (array) $field_definitions;
      unset($field_definitions_array['field_cms_content_synced']);
      $field_names = array_keys($field_definitions_array);
      sort($field_names);
      $version = json_encode($field_names);
    }
    else {
      $version = '';
    }
    return md5($version);
  }

  /**
   * Check whether the local deletion of the given entity is allowed.
   *
   * @return bool
   */
  public static function isLocalDeletionAllowed(EntityInterface $entity) {
    if (!$entity
      ->uuid()) {
      return true;
    }
    $entity_status = EntityStatus::getInfosForEntity($entity
      ->getEntityTypeId(), $entity
      ->uuid());
    foreach ($entity_status as $info) {
      if (!$info
        ->getLastPull() || $info
        ->isSourceEntity()) {
        continue;
      }
      $flow = $info
        ->getFlow();
      if (!$flow) {
        continue;
      }
      $config = $flow
        ->getController()
        ->getEntityTypeConfig($entity
        ->getEntityTypeId(), $entity
        ->bundle(), true);
      if (PullIntent::PULL_DISABLED === $config['import']) {
        continue;
      }
      if (!boolval($config['import_deletion_settings']['allow_local_deletion_of_import'])) {
        return false;
      }
    }
    return true;
  }

  /**
   * Load all entities.
   *
   * Load all cms_content_sync_flow entities and add overrides from global $config.
   *
   * @param bool $skip_inactive
   *                            Do not return inactive flows by default
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   *
   * @return Flow[]
   */
  public static function getAll($skip_inactive = true, $rebuild = false) {
    if ($skip_inactive && null !== self::$all && !$rebuild) {
      return self::$all;
    }

    /**
     * @var Flow[] $configurations
     */
    $configurations = \Drupal::entityTypeManager()
      ->getStorage('cms_content_sync_flow')
      ->loadMultiple();
    foreach ($configurations as $id => &$configuration) {
      global $config;
      $config_name = 'cms_content_sync.flow.' . $id;
      if (!isset($config[$config_name]) || empty($config[$config_name])) {
        continue;
      }
      foreach ($config[$config_name] as $key => $new_value) {
        if (in_array($key, [
          'per_bundle_settings',
          'simple_settings',
        ])) {
          $configuration->{$key} = array_merge_recursive($configuration->{$key}, $new_value);
          continue;
        }
        $configuration
          ->set($key, $new_value);
      }

      // Calculate 'version' property if missing.
      $configuration
        ->getController()
        ->getEntityTypeConfig();
    }
    if ($skip_inactive) {
      $result = [];
      foreach ($configurations as $id => $flow) {
        if ($flow
          ->get('status')) {
          $result[$id] = $flow;
        }
      }
      $configurations = $result;
      self::$all = $configurations;
    }
    return $configurations;
  }
  public static function resetFlowCache() {
    self::$all = null;
  }

  /**
   * Get the first synchronization that allows the pull of the provided entity
   * type.
   *
   * @param Pool   $pool
   * @param string $entity_type_name
   * @param string $bundle_name
   * @param string $reason
   * @param string $action
   * @param bool   $strict
   *
   * @return null|Flow
   */
  public static function getFlowForPoolAndEntityType($pool, $entity_type_name, $bundle_name, $reason, $action = SyncIntent::ACTION_CREATE, $strict = false) {
    $flows = self::getAll();

    // If $reason is DEPENDENCY and there's a Flow pulling AUTOMATICALLY we take that. But only if there's no Flow
    // explicitly handling this entity AS_DEPENDENCY.
    $fallback = null;
    foreach ($flows as $flow) {
      if ($pool && !in_array($pool, $flow
        ->getController()
        ->getUsedPoolsForPulling($entity_type_name, $bundle_name))) {
        continue;
      }
      if (!$flow
        ->getController()
        ->canPullEntity($entity_type_name, $bundle_name, $reason, $action, true)) {
        if (!$strict && $flow
          ->getController()
          ->canPullEntity($entity_type_name, $bundle_name, $reason, $action, false)) {
          $fallback = $flow;
        }
        continue;
      }
      return $flow;
    }
    if (!empty($fallback)) {
      return $fallback;
    }
    return null;
  }

  /**
   * Unset the flow version warning.
   */
  public function resetVersionWarning() {
    $moduleHandler = \Drupal::service('module_handler');
    if ($moduleHandler
      ->moduleExists('cms_content_sync_developer')) {
      $developer_config = \Drupal::service('config.factory')
        ->getEditable('cms_content_sync.developer');
      $mismatching_versions = $developer_config
        ->get('version_mismatch');
      if (!empty($mismatching_versions)) {
        unset($mismatching_versions[$this
          ->id()]);
        $developer_config
          ->set('version_mismatch', $mismatching_versions)
          ->save();
      }
    }
  }

  /**
   * @param $type
   * @param $bundle
   * @param IFlowController|null $existing
   * @param null $field
   *
   * @return array
   */
  public static function getDefaultFieldConfigForEntityType($type, $bundle, $existing = null, $field = null) {
    if ($field) {
      $field_default_values = [
        'export' => null,
        'import' => null,
      ];
      $entity_type = \Drupal::entityTypeManager()
        ->getDefinition($type);

      // @todo Should be gotten from the Entity Type Handler instead.
      $forbidden_fields = [
        // These are not relevant or misleading when synchronized.
        'revision_default',
        'revision_translation_affected',
        'content_translation_outdated',
        // Field collections.
        'host_type',
        // Files.
        'uri',
        'filemime',
        'filesize',
        // Media.
        'thumbnail',
        // Taxonomy.
        'parent',
        // These are standard fields defined by the Flow
        // Entity type that entities may not override (otherwise
        // these fields will collide with CMS Content Sync functionality)
        $entity_type
          ->getKey('bundle'),
        $entity_type
          ->getKey('id'),
        $entity_type
          ->getKey('uuid'),
        $entity_type
          ->getKey('label'),
        $entity_type
          ->getKey('revision'),
      ];
      $pools = Pool::getAll();
      if (count($pools)) {
        $reserved = reset($pools)
          ->getClient()
          ->getReservedPropertyNames();
        $forbidden_fields = array_merge($forbidden_fields, $reserved);
      }
      if (false !== in_array($field, $forbidden_fields)) {
        $field_default_values['handler'] = 'ignore';
        $field_default_values['export'] = PushIntent::PUSH_DISABLED;
        $field_default_values['import'] = PullIntent::PULL_DISABLED;
        return $field_default_values;
      }
      $field_handler_service = \Drupal::service('plugin.manager.cms_content_sync_field_handler');
      $field_definition = \Drupal::service('entity_field.manager')
        ->getFieldDefinitions($type, $bundle)[$field];
      $field_handlers = $field_handler_service
        ->getHandlerOptions($type, $bundle, $field, $field_definition, true);
      if (empty($field_handlers)) {
        throw new \Exception('Unsupported field type ' . $field_definition
          ->getType() . ' for field ' . $type . '.' . $bundle . '.' . $field);
      }
      reset($field_handlers);
      $handler_id = empty($field_default_values['handler']) ? key($field_handlers) : $field_default_values['handler'];
      $field_default_values['handler'] = $handler_id;
      $field_default_values['export'] = PushIntent::PUSH_AUTOMATICALLY;
      $field_default_values['import'] = PullIntent::PULL_AUTOMATICALLY;
      $handler = $field_handler_service
        ->createInstance($handler_id, [
        'entity_type_name' => $type,
        'bundle_name' => $bundle,
        'field_name' => $field,
        'field_definition' => $field_definition,
        'settings' => $field_default_values,
        'sync' => null,
      ]);
      $advanced_settings = $handler
        ->getHandlerSettings($field_default_values);
      if (count($advanced_settings)) {
        foreach ($advanced_settings as $name => $element) {
          $field_default_values['handler_settings'][$name] = $element['#default_value'];
        }
      }
      return $field_default_values;
    }
    $entityTypeManager = \Drupal::service('entity_type.manager');
    $type = $entityTypeManager
      ->getDefinition($type, false);
    $field_definition = $type ? \Drupal::service('entity_field.manager')
      ->getFieldDefinitions($type, $bundle) : false;
    $result = [];
    if ($field_definition) {
      foreach ($field_definition as $key => $field) {
        $field_config = $existing ? $existing
          ->getPropertyConfig($type, $bundle, $key) : NULL;
        $result[$key] = $field_config ? $field_config : self::getDefaultFieldConfigForEntityType($type, $bundle, null, $key);
      }
    }
    return $result;
  }
  public function useV2() {
    return self::V2_STATUS_ACTIVE === $this
      ->getV2Status() || Migration::useV2();
  }
  public function v2Ready() {
    $status = $this
      ->getV2Status();
    return self::V2_STATUS_NONE != $status;
  }
  public function getV2Status() {
    return Migration::getFlowStatus($this);
  }

}

Classes

Namesort descending Description
Flow Defines the "Content Sync - Flow" entity.