cms_content_sync-3.0.x-dev/src/Entity/Flow.php
src/Entity/Flow.php
<?php
namespace Drupal\cms_content_sync\Entity;
use Drupal\cms_content_sync\Controller\FlowControllerSimple;
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\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
/**
* 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 v3
*/
public const TYPE_BOTH = 'both';
public const VARIANT_SIMPLE = 'simple';
public const VARIANT_PER_BUNDLE = 'per-bundle';
public const CACHE_TAG_ANY_FLOW = 'cms_content_sync:flow';
public const CACHE_ITEM_NAME_FLOWS = 'cms_content_sync/flows';
/**
* 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.
*
* @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;
/**
* @var Flow[]
* All content synchronization configs. Use {@see Flow::getAll}
* to request them.
*/
public static $all;
/**
* @var \Drupal\cms_content_sync\IFlowController
*/
protected $handler;
/**
*
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
Flow::clearFlowCache();
return parent::postSave($storage, $update);
}
/**
* Clear the flow cache.
*/
public static function clearFlowCache() {
Cache::invalidateTags([self::CACHE_TAG_ANY_FLOW]);
}
/**
* 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);
}
}
/**
* Provide the controller to act upon the stored configuration.
*
* @return \Drupal\cms_content_sync\IFlowController
*/
public function getController() {
if (!$this->handler) {
if (Flow::VARIANT_SIMPLE === $this->variant) {
$this->handler = new FlowControllerSimple($this);
}
else {
throw new \Exception("Unknown Flow variant '" . $this->variant . "'. Unable to instantiate Controller.");
}
}
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_manual = TRUE, $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 && $include_manual) {
$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_strings = [];
foreach ($field_definitions as $field_name => $field_definition) {
if ($field_definition->isComputed()) {
continue;
}
$field_strings[] = 'v2::' . join('::', [
$field_name,
$field_definition->getType(),
$field_definition->isRequired() ? 'required' : 'optional',
$field_definition->isTranslatable() ? 'translatable' : 'non-translatable',
]);
}
sort($field_strings);
$version = join("\n", $field_strings);
}
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;
}
/**
* Apply the overrides from the global $config entity for this Flow, if any
* are given.
*/
public static function applyOverrides(string $id, Flow &$configuration) {
global $config;
$config_name = 'cms_content_sync.flow.' . $id;
if (!isset($config[$config_name]) || empty($config[$config_name])) {
return;
}
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;
}
// Ensure backwards compatibility.
if ('sync_entities' === $key) {
foreach ($configuration->per_bundle_settings as $entity_type_name => $bundles) {
foreach ($bundles as $bundle_name => $bundle_config) {
if (isset($new_value[$entity_type_name . '-' . $bundle_name])) {
$configuration->per_bundle_settings[$entity_type_name][$bundle_name]['settings'] = array_merge_recursive($bundle_config['settings'], $new_value[$entity_type_name . '-' . $bundle_name]);
}
if (!empty($bundle_config['properties'])) {
foreach ($bundle_config['properties'] as $field_name => $property_config) {
if (isset($new_value[$entity_type_name . '-' . $bundle_name . '-' . $field_name])) {
$configuration->per_bundle_settings[$entity_type_name][$bundle_name]['properties'][$field_name] = array_merge_recursive($property_config, $new_value[$entity_type_name . '-' . $bundle_name . '-' . $field_name]);
}
}
}
}
}
continue;
}
$configuration->set($key, $new_value);
}
// Per-bundle config: calculate 'version' property if missing.
$configuration->getController()->getEntityTypeConfig();
}
/**
* 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.
* @param mixed $rebuild
*
* @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();
ksort($configurations);
foreach ($configurations as $id => &$configuration) {
self::applyOverrides($id, $configuration);
}
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 $entity_type_name
* @param $bundle
* @param null|IFlowController $existing
* @param null $field
*
* @return array
*/
public static function getDefaultFieldConfigForEntityType($entity_type_name, $bundle, $existing = NULL, $field = NULL) {
if ($field) {
$field_default_values = [
'export' => NULL,
'import' => NULL,
];
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_name);
// @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($entity_type_name, $bundle)[$field];
$field_handlers = $field_handler_service->getHandlerOptions($entity_type_name, $bundle, $field, $field_definition, TRUE);
if (empty($field_handlers)) {
throw new \Exception('Unsupported field type ' . $field_definition->getType() . ' for field ' . $entity_type_name . '.' . $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' => $entity_type_name,
'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($entity_type_name, FALSE);
$field_definition = $type ? \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_name, $bundle) : FALSE;
$result = [];
if ($field_definition) {
foreach ($field_definition as $key => $field) {
$field_config = $existing ? $existing->getPropertyConfig($entity_type_name, $bundle, $key) : NULL;
$result[$key] = $field_config ? $field_config : self::getDefaultFieldConfigForEntityType($entity_type_name, $bundle, NULL, $key);
}
}
return $result;
}
}
