cms_content_sync-3.0.x-dev/src/Entity/EntityStatus.php
src/Entity/EntityStatus.php
<?php
namespace Drupal\cms_content_sync\Entity;
use Drupal\cms_content_sync\PushIntent;
use Drupal\cms_content_sync\SyncIntent;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\cms_content_sync\Entity\Pool;
use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Plugin\Type\EntityHandlerPluginManager;
use Drupal\cms_content_sync\PullIntent;
use Drupal\cms_content_sync\SyncCoreInterface\SyncCoreFactory;
use Drupal\Core\Entity\TranslatableInterface;
/**
* Defines the "Content Sync - Entity Status" entity type.
*
* @ingroup cms_content_sync_entity_status
*
* @ContentEntityType(
* id = "cms_content_sync_entity_status",
* label = @Translation("Content Sync - Entity Status"),
* base_table = "cms_content_sync_entity_status",
* entity_keys = {
* "id" = "id",
* "flow" = "flow",
* "pool" = "pool",
* "entity_uuid" = "entity_uuid",
* "entity_type" = "entity_type",
* "entity_type_version" = "entity_type_version",
* "flags" = "flags",
* },
* handlers = {
* "views_data" = "Drupal\views\EntityViewsData",
* "storage_schema" = "Drupal\cms_content_sync\EntityStatusStorageSchema",
* },
* )
*/
class EntityStatus extends ContentEntityBase implements EntityStatusInterface {
use EntityChangedTrait;
public const FLAG_UNUSED_CLONED = 0x00000001;
public const FLAG_DELETED = 0x00000002;
public const FLAG_USER_ENABLED_PUSH = 0x00000004;
public const FLAG_EDIT_OVERRIDE = 0x00000008;
public const FLAG_IS_SOURCE_ENTITY = 0x00000010;
public const FLAG_PUSH_ENABLED = 0x00000020;
public const FLAG_DEPENDENCY_PUSH_ENABLED = 0x00000040;
public const FLAG_LAST_PUSH_RESET = 0x00000080;
public const FLAG_LAST_PULL_RESET = 0x00000100;
public const FLAG_PUSH_FAILED = 0x00000200;
public const FLAG_PULL_FAILED = 0x00000400;
public const FLAG_PUSH_FAILED_SOFT = 0x00000800;
public const FLAG_PULL_FAILED_SOFT = 0x00001000;
public const FLAG_PUSHED_EMBEDDED = 0x00002000;
public const FLAG_PULLED_EMBEDDED = 0x00004000;
public const DATA_PULL_FAILURE = 'import_failure';
public const DATA_PUSH_FAILURE = 'export_failure';
public const DATA_ENTITY_PUSH_HASH = 'entity_push_hash';
public const DATA_PARENT_ENTITY = 'parent_entity';
public const DATA_TRANSLATION_SOURCE_URLS = 'translation_urls';
public const DATA_TRANSLATIONS = 'translations';
public const DATA_TRANSLATIONS_LAST_PULLED_VERSION_ID = 'import_version_id';
public const DATA_TRANSLATIONS_LAST_PULL = 'last_import';
public const DATA_TRANSLATIONS_LAST_PUSH = 'last_export';
public const DATA_TRANSLATIONS_LAST_PUSH_TRIGGER = 'last_export_trigger';
public const DATA_TRANSLATIONS_DELETED_AT = 'deleted_at';
public const DATA_TRANSLATIONS_MISSING_REFERENCES = 'missing_references';
public const FLOW_NO_FLOW = 'ERROR_STATUS_ENTITY_FLOW';
/**
* {@inheritdoc}
*/
public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
// Set Entity ID or UUID by default one or the other is not set.
if (!isset($values['entity_type'])) {
throw new \Exception(t('The type of the entity is required.'));
}
if (!isset($values['flow'])) {
throw new \Exception(t('The flow is required.'));
}
if (!isset($values['pool'])) {
throw new \Exception(t('The pool is required.'));
}
/**
* @var \Drupal\Core\Entity\EntityInterface $entity
*/
$entity = \Drupal::service('entity.repository')->loadEntityByUuid($values['entity_type'], $values['entity_uuid']);
if (!isset($values['entity_type_version'])) {
$values['entity_type_version'] = Flow::getEntityTypeVersion($entity->getEntityType()->id(), $entity->bundle());
return;
}
}
/**
* Get infos for pool.
*
* @param string $entity_type
* The entity types the infos should be pulled for.
* @param string $entity_uuid
* The entity uuid the infos should be pulled for.
* @param \Drupal\cms_content_sync\Entity\Pool $pool
* The pool the infos should be pulled for.
*
* @throws \Exception
*
* @return EntityStatus[]
* The status of the entity.
*/
public static function getInfoForPool($entity_type, $entity_uuid, Pool $pool) {
if (!$entity_type) {
throw new \Exception('$entity_type is required.');
}
if (!$entity_uuid) {
throw new \Exception('$entity_uuid is required.');
}
/**
* @var EntityStatus[] $entities
*/
return \Drupal::entityTypeManager()
->getStorage('cms_content_sync_entity_status')
->loadByProperties([
'entity_type' => $entity_type,
'entity_uuid' => $entity_uuid,
'pool' => $pool->id,
]);
}
/**
* Get a list of all entity status entities for the given entity.
*
* @param string $entity_type
* The entity type ID.
* @param string $entity_uuid
* The entity UUID.
* @param array $filter
* Additional filters. Usually "flow"=>... or "pool"=>...
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*
* @return EntityStatus[]
* The list of entity status entities.
*/
public static function getInfosForEntity($entity_type, $entity_uuid, ?array $filter = NULL) {
if (!$entity_type) {
throw new \Exception('$entity_type is required.');
}
if (!$entity_uuid) {
throw new \Exception('$entity_uuid is required.');
}
$base_filter = [
'entity_type' => $entity_type,
'entity_uuid' => $entity_uuid,
];
$filters_combined = $base_filter;
$filter_without_flow = isset($filter['flow']) && (empty($filter['flow']) || self::FLOW_NO_FLOW == $filter['flow']);
if ($filter_without_flow) {
$filters_combined = array_merge($filters_combined, [
'flow' => self::FLOW_NO_FLOW,
]);
}
elseif ($filter) {
$filters_combined = array_merge($filters_combined, $filter);
}
/**
* @var EntityStatus[] $entities
*/
$entities = \Drupal::entityTypeManager()
->getStorage('cms_content_sync_entity_status')
->loadByProperties($filters_combined);
$result = [];
// If a pull fails, we may create a status entity without a flow assigned.
// We ignore them for normal functionality, so they're filtered out.
if ($filter_without_flow) {
foreach ($entities as $i => $entity) {
if (!$entity->getFlow()) {
$result[] = $entity;
}
}
}
else {
foreach ($entities as $i => $entity) {
if ($entity->getFlow()) {
$result[] = $entity;
}
}
}
return $result;
}
/**
* Get a single entity status entity for the given entity.
*
* @param string $entity_type
* The entity type ID.
* @param string $entity_uuid
* The entity UUID.
* @param \Drupal\cms_content_sync\Entity\Flow|string $flow
* The flow the status entity belongs to.
* @param \Drupal\cms_content_sync\Entity\Pool|string $pool
* The pool the status entity belongs to.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Exception
*
* @return EntityStatus|mixed
* The status entity.
*/
public static function getInfoForEntity($entity_type, $entity_uuid, $flow, $pool = NULL) {
if (!$entity_type) {
throw new \Exception('$entity_type is required.');
}
if (!$entity_uuid) {
throw new \Exception('$entity_uuid is required.');
}
$filter = [
'entity_type' => $entity_type,
'entity_uuid' => $entity_uuid,
];
if ($pool) {
$filter['pool'] = is_string($pool) ? $pool : $pool->id;
}
if ($flow) {
$filter['flow'] = is_string($flow) ? $flow : $flow->id;
}
else {
$filter['flow'] = self::FLOW_NO_FLOW;
}
/**
* @var EntityStatus[] $entities
*/
$entities = \Drupal::entityTypeManager()
->getStorage('cms_content_sync_entity_status')
->loadByProperties($filter);
if (!$flow) {
foreach ($entities as $entity) {
if (!$entity->getFlow()) {
return $entity;
}
}
return NULL;
}
return reset($entities);
}
/**
* Reset status entities.
*/
public function resetStatus() {
$this->setLastPush(NULL);
$this->setLastPull(NULL);
$this->save();
// Above cache clearing doesn't work reliably. So we reset the whole entity cache.
\Drupal::service('cache.entity')->deleteAll();
}
/**
* Get the last push for an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check for.
*
* @throws \Exception
*
* @return null|int
* Returns the last push timestamp or null if not pushed.
*/
public static function getLastPushForEntity(EntityInterface $entity, $for_translation = FALSE) {
$entity_status = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
$latest = NULL;
$language = $for_translation && $entity instanceof TranslatableInterface ? $entity->language()->getId() : NULL;
foreach ($entity_status as $info) {
if ($info->getLastPush($language) && (!$latest || $info->getLastPush($language) > $latest)) {
$latest = $info->getLastPush($language);
}
}
return $latest;
}
/**
* Get last pull for an entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check for.
* @param string|null $language
*
* @throws \Exception
*
* @return null|int
* Returns the last pull timestamp or null if not pulled.
*/
public static function getLastPullForEntity(EntityInterface $entity, ?string $language = NULL) {
$entity_status = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
$latest = NULL;
foreach ($entity_status as $info) {
$current_last = $info->getLastPull($language);
if ($current_last && (!$latest || $current_last > $latest)) {
$latest = $current_last;
}
}
return $latest;
}
/**
* Get whether an entity was pushed or pulled before or none or both.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to check for.
* @param bool $for_translation
* Whether to only check for this specific translation.
*
* @throws \Exception
*
* @return null|string Either "push", "pull", "both" or NULL.
*/
public static function wasEntityPushedOrPulled(EntityInterface $entity, $for_translation = FALSE) {
$entity_status = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
$language = $for_translation && $entity instanceof TranslatableInterface ? $entity->language()->getId() : NULL;
$pushed = FALSE;
$pulled = FALSE;
foreach ($entity_status as $info) {
if ($info->getLastPush($language)) {
$pushed = TRUE;
}
if ($info->getLastPull($language)) {
$pulled = TRUE;
}
}
return $pushed ? ($pulled ? "both" : "push") : ($pulled ? "pull" : NULL);
}
/**
* Access the temporary push to pool info for a give field.
*
* @param mixed $entity_type
* The entity type of the entity to check for.
* @param mixed $uuid
* The uuid of the entity to check for.
* @param mixed $field_name
* The name of the field to check for.
* @param mixed $delta
* Delta value.
* @param mixed $tree_position
* The tree position.
* @param null|mixed $set_flow_id
* The flow id to be set.
* @param null|mixed $set_pool_ids
* The pool ids to be set.
* @param null|mixed $set_uuid
* The uuid to be set.
*/
public static function accessTemporaryPushToPoolInfoForField($entity_type, $uuid, $field_name, $delta, $tree_position = [], $set_flow_id = NULL, $set_pool_ids = NULL, $set_uuid = NULL) {
static $field_storage = [];
if ($set_flow_id && $set_pool_ids) {
$data = [
'flow_id' => $set_flow_id,
'pool_ids' => $set_pool_ids,
'uuid' => $set_uuid,
];
if (!isset($field_storage[$entity_type][$uuid])) {
$field_storage[$entity_type][$uuid] = [];
}
$setter = &$field_storage[$entity_type][$uuid];
foreach ($tree_position as $name) {
if (!isset($setter[$name])) {
$setter[$name] = [];
}
$setter = &$setter[$name];
}
if (!isset($setter[$field_name][$delta])) {
$setter[$field_name][$delta] = [];
}
$setter = &$setter[$field_name][$delta];
$setter = $data;
}
else {
if (!empty($field_storage[$entity_type][$uuid])) {
$value = $field_storage[$entity_type][$uuid];
foreach ($tree_position as $name) {
if (!isset($value[$name])) {
return NULL;
}
$value = $value[$name];
}
return $value[$field_name][$delta] ?? NULL;
}
}
return NULL;
}
/**
* Save the selected pool for a field.
*
* @param \Drupal\Core\Entity\EntityInterface $parent_entity
* The parent entity.
* @param string $parent_field_name
* The parent field name.
* @param int $parent_field_delta
* The parent field delta.
* @param \Drupal\Core\Entity\EntityInterface $reference
* The reference entity.
* @param array $tree_position
* The tree position.
*/
public static function saveSelectedPushToPoolForField(EntityInterface $parent_entity, $parent_field_name, $parent_field_delta, EntityInterface $reference, array $tree_position = []) {
$data = EntityStatus::accessTemporaryPushToPoolInfoForField($parent_entity->getEntityTypeId(), $parent_entity->uuid(), $parent_field_name, $parent_field_delta, $tree_position);
// On sites that don't push, this will be NULL.
if (empty($data['flow_id'])) {
return;
}
$values = $data['pool_ids'];
$processed = [];
if (is_array($values)) {
foreach ($values as $id => $selected) {
if ($selected && 'ignore' !== $id) {
$processed[] = $id;
}
}
}
else {
if ('ignore' !== $values) {
$processed[] = $values;
}
}
EntityStatus::saveSelectedPoolsToPushTo($reference, $data['flow_id'], $processed, $parent_entity, $parent_field_name);
}
/**
* Save the selected pools to push to.
*
* @param \Drupal\Core\Entity\EntityInterface $reference
* The reference entity.
* @param string $flow_id
* The flow id.
* @param string[] $pool_ids
* The pool ids to save.
* @param null|EntityInterface $parent_entity
* The parent entity.
* @param null|string $parent_field_name
* The parent field name.
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public static function saveSelectedPoolsToPushTo(EntityInterface $reference, $flow_id, array $pool_ids, ?EntityInterface $parent_entity = NULL, $parent_field_name = NULL) {
$entity_type = $reference->getEntityTypeId();
$bundle = $reference->bundle();
$uuid = $reference->uuid();
$flow = Flow::getAll()[$flow_id];
$pools = Pool::getAll();
$entity_type_pools = Pool::getSelectablePools($entity_type, $bundle, $parent_entity, $parent_field_name)[$flow_id]['pools'];
foreach ($entity_type_pools as $entity_type_pool_id => $config) {
$pool = $pools[$entity_type_pool_id];
$entity_status = EntityStatus::getInfoForEntity($entity_type, $uuid, $flow, $pool);
if (in_array($entity_type_pool_id, $pool_ids)) {
if (!$entity_status) {
$entity_status = EntityStatus::create([
'flow' => $flow->id,
'pool' => $pool->id,
'entity_type' => $entity_type,
'entity_uuid' => $uuid,
'entity_type_version' => Flow::getEntityTypeVersion($entity_type, $bundle),
'flags' => 0,
'source_url' => NULL,
]);
}
$entity_status->isPushEnabled(TRUE);
$entity_status->save();
continue;
}
if ($entity_status) {
$entity_status->isPushEnabled(FALSE);
$entity_status->save();
}
}
// Also check if the entity is going to be force pushed into another pool.
$force_push_pools = $flow->getController()->getPoolsToPushTo($reference, PushIntent::PUSH_ANY, SyncIntent::ACTION_CREATE);
if (count($entity_type_pools) && !count($pool_ids) && !count($force_push_pools)) {
\Drupal::messenger()->addWarning(\Drupal::translation()->translate("You didn't assign a pool to @entity_type %entity_label so it won't be pushed along with the content.", [
'@entity_type' => $entity_type,
'%entity_label' => $reference->label(),
]));
}
elseif (count($entity_type_pools) && !count($pool_ids) && count($force_push_pools)) {
$pools = '';
$numItems = count($force_push_pools);
$i = 0;
if (count($force_push_pools) > 1) {
foreach ($force_push_pools as $force_push_pool) {
if (++$i === $numItems) {
$pools .= $force_push_pool->label();
}
else {
$pools .= $force_push_pool->label() . ', ';
}
}
}
else {
foreach ($force_push_pools as $force_push_pool) {
$pools = $force_push_pool->label();
}
}
\Drupal::messenger()->addWarning(\Drupal::translation()->translate("You didn't assign a pool to @entity_type %entity_label, but it is going to be force pushed to the following pools based on the content sync configuration: %pools.", [
'%pools' => $pools,
'@entity_type' => $entity_type,
'%entity_label' => $reference->label(),
]));
}
}
/**
* Get the entity this entity status belongs to.
*
* @return \Drupal\Core\Entity\EntityInterface
* The entity this entity status belongs to.
*/
public function getEntity() {
return \Drupal::service('entity.repository')->loadEntityByUuid(
$this->getEntityTypeName(),
$this->getUuid()
);
}
/**
* Returns the information if the entity has been pushed before but the last push date was reset.
*
* @param bool $set
* Optional parameter to set the value for LastPushReset.
*
* @return bool
* True if the entity has been pushed before but the last push date was reset.
*/
public function wasLastPushReset($set = NULL) {
if (TRUE === $set) {
$this->set('flags', $this->get('flags')->value | self::FLAG_LAST_PUSH_RESET);
}
elseif (FALSE === $set) {
$this->set('flags', $this->get('flags')->value & ~self::FLAG_LAST_PUSH_RESET);
}
return (bool) ($this->get('flags')->value & self::FLAG_LAST_PUSH_RESET);
}
/**
* Was last pull reset.
*
* Returns the information if the entity has been pulled before but the
* last import date was reset.
*
* @param bool $set
* Optional parameter to set the value for LastPullReset.
*
* @return bool
* True if the entity has been pullked before but the last pull date was reset.
*/
public function wasLastPullReset($set = NULL) {
if (TRUE === $set) {
$this->set('flags', $this->get('flags')->value | self::FLAG_LAST_PULL_RESET);
}
elseif (FALSE === $set) {
$this->set('flags', $this->get('flags')->value & ~self::FLAG_LAST_PULL_RESET);
}
return (bool) ($this->get('flags')->value & self::FLAG_LAST_PULL_RESET);
}
/**
* Returns the information if the last push of the entity failed.
*
* @param bool $set
* Optional parameter to set the value for PushFailed.
* @param bool $soft
* A soft fail- this was intended according to configuration. But the user might want to know why to debug different
* expectations.
* @param null|array $details
* If $set is TRUE, you can provide additional details on why the push failed. Can be gotten via
* ->whyDidPushFail()
*
* @return bool
* True if the last push of the entity failed.
*/
public function didPushFail($set = NULL, $soft = FALSE, $details = NULL) {
$flag = $soft ? self::FLAG_PUSH_FAILED_SOFT : self::FLAG_PUSH_FAILED;
if (TRUE === $set) {
$this->set('flags', $this->get('flags')->value | $flag);
if (!empty($details)) {
$this->setData(self::DATA_PUSH_FAILURE, $details);
}
}
elseif (FALSE === $set) {
$this->set('flags', $this->get('flags')->value & ~$flag);
$this->setData(self::DATA_PUSH_FAILURE, NULL);
}
return (bool) ($this->get('flags')->value & $flag);
}
/**
* Get the details provided to ->didPushFail( TRUE, ... ) before.
*
* @return null|array
* The information why the push failed or null if it didnt.
*/
public function whyDidPushingFail() {
return $this->getData(self::DATA_PUSH_FAILURE);
}
/**
* Returns the information if the last pull of the entity failed.
*
* @param bool $set
* Optional parameter to set the value for PullFailed.
* @param bool $soft
* A soft fail- this was intended according to configuration. But the user might want to know why to debug different
* expectations.
* @param null|array $details
* If $set is TRUE, you can provide additional details on why the pull failed. Can be gotten via
* ->whyDidPullFail()
*
* @return bool
* True if the pull failed.
*/
public function didPullFail($set = NULL, $soft = FALSE, $details = NULL) {
$flag = $soft ? self::FLAG_PULL_FAILED_SOFT : self::FLAG_PULL_FAILED;
if (TRUE === $set) {
$this->set('flags', $this->get('flags')->value | $flag);
if (!empty($details)) {
$this->setData(self::DATA_PULL_FAILURE, $details);
}
}
elseif (FALSE === $set) {
$this->set('flags', $this->get('flags')->value & ~$flag);
$this->setData(self::DATA_PULL_FAILURE, NULL);
}
return (bool) ($this->get('flags')->value & $flag);
}
/**
* Get the details provided to ->didPullFail( TRUE, ... ) before.
*
* @return null|array
* Returns the infos why the pull failed or null if it didnt.
*/
public function whyDidPullingFail() {
return $this->getData(self::DATA_PULL_FAILURE);
}
/**
* Is push enabled.
*
* Returns the information if the entity has been chosen by the user to
* be pushed with this flow and pool.
*
* @param bool $set
* Optional parameter to set the value for PushEnabled.
* @param bool $setDependency
* Optional parameter to set the value for DependencyPushEnabled.
*
* @return bool
* True if push is enabled.
*/
public function isPushEnabled($set = NULL, $setDependency = NULL) {
if (TRUE === $set) {
$this->set('flags', $this->get('flags')->value | self::FLAG_PUSH_ENABLED);
}
elseif (FALSE === $set) {
$this->set('flags', $this->get('flags')->value & ~self::FLAG_PUSH_ENABLED);
}
if (TRUE === $setDependency) {
$this->set('flags', $this->get('flags')->value | self::FLAG_DEPENDENCY_PUSH_ENABLED);
}
elseif (FALSE === $setDependency) {
$this->set('flags', $this->get('flags')->value & ~self::FLAG_DEPENDENCY_PUSH_ENABLED);
}
return (bool) ($this->get('flags')->value & (self::FLAG_PUSH_ENABLED | self::FLAG_DEPENDENCY_PUSH_ENABLED));
}
/**
* Check if manual pushing is enabled for the pool.
*
* Returns the information if the entity has been chosen by the user to
* be pushed with this flow and pool.
*
* @return bool
* True if manual push is enabled.
*/
public function isManualPushEnabled() {
return (bool) ($this->get('flags')->value & (self::FLAG_PUSH_ENABLED));
}
/**
* Check whether the entity was pushed manually (entity was pushed AND Flow is
* configured to push this manually).
*
* @param string|null $language
*
* @return bool
* True if the entity was manually pushed.
*/
public function wasPushedManually(?string $language = NULL) {
return !!$this->getLastPush($language) && $this->getFlow() && $this->getEntity() && $this->getFlow()->getController()->canPushEntity($this->getEntity(), PushIntent::PUSH_MANUALLY, SyncIntent::ACTION_CREATE, $this->getPool());
}
/**
* Check a entity is puhsed as an dependency.
*
* Returns the information if the entity has been pushed with this flow and
* pool as a dependency.
*
* @return bool
* True if pushed as dependency.
*/
public function isPushedAsDependency() {
return (bool) ($this->get('flags')->value & (self::FLAG_DEPENDENCY_PUSH_ENABLED));
}
/**
* Returns the information if the user override the entity locally.
*
* @param bool $set
* Optional parameter to set the value for EditOverride.
* @param bool $individual
*
* @return bool
* True if the entity is overridden locally.
*/
public function isOverriddenLocally($set = NULL, $individual = FALSE) {
$status = EntityStatus::getInfosForEntity($this->getEntityTypeName(), $this->getUuid());
if (TRUE === $set) {
if ($individual) {
$this->set('flags', $this->get('flags')->value | self::FLAG_EDIT_OVERRIDE);
}
else {
foreach ($status as $info) {
$info->isOverriddenLocally(TRUE, TRUE);
}
}
return TRUE;
}
if (FALSE === $set) {
if ($individual) {
$this->set('flags', $this->get('flags')->value & ~self::FLAG_EDIT_OVERRIDE);
}
else {
foreach ($status as $info) {
$info->isOverriddenLocally(FALSE, TRUE);
}
}
return FALSE;
}
if ($individual) {
return (bool) ($this->get('flags')->value & self::FLAG_EDIT_OVERRIDE);
}
foreach ($status as $info) {
if ($info->isOverriddenLocally(NULL, TRUE)) {
return TRUE;
}
}
return FALSE;
}
/**
* The information if the entity has originally been created on this site.
*
* @param bool $set
* Optional parameter to set the value for IsSourceEntity.
* @param mixed $individual
*
* @return bool
* True if the entity has been created on this site.
*/
public function isSourceEntity($set = NULL, $individual = FALSE) {
$status = EntityStatus::getInfosForEntity($this->getEntityTypeName(), $this->getUuid());
if (TRUE === $set) {
if ($individual) {
$this->set('flags', $this->get('flags')->value | self::FLAG_IS_SOURCE_ENTITY);
}
else {
foreach ($status as $info) {
$info->isSourceEntity(TRUE, TRUE);
}
$this->isSourceEntity(TRUE, TRUE);
}
return TRUE;
}
if (FALSE === $set) {
if ($individual) {
$this->set('flags', $this->get('flags')->value & ~self::FLAG_IS_SOURCE_ENTITY);
}
else {
foreach ($status as $info) {
$info->isSourceEntity(FALSE, TRUE);
}
$this->isSourceEntity(FALSE, TRUE);
}
return FALSE;
}
if ($individual) {
return (bool) ($this->get('flags')->value & self::FLAG_IS_SOURCE_ENTITY);
}
foreach ($status as $info) {
if ($info->isSourceEntity(NULL, TRUE)) {
return TRUE;
}
}
return $this->isSourceEntity(NULL, TRUE);
}
/**
* Returns the information if the user allowed the push.
*
* @param bool $set
* Optional parameter to set the value for UserEnabledPush.
*
* @return bool
* True if the user allowed the push.
*/
public function didUserEnablePush($set = NULL) {
if (TRUE === $set) {
$this->set('flags', $this->get('flags')->value | self::FLAG_USER_ENABLED_PUSH);
}
elseif (FALSE === $set) {
$this->set('flags', $this->get('flags')->value & ~self::FLAG_USER_ENABLED_PUSH);
}
return (bool) ($this->get('flags')->value & self::FLAG_USER_ENABLED_PUSH);
}
/**
* Returns the information if the entity is deleted.
*
* @param bool $set
* Optional parameter to set the value for Deleted.
*
* @return bool
* True if the entity is deleted.
*/
public function isDeleted($set = NULL) {
if (TRUE === $set) {
$this->set('flags', $this->get('flags')->value | self::FLAG_DELETED);
}
elseif (FALSE === $set) {
$this->set('flags', $this->get('flags')->value & ~self::FLAG_DELETED);
}
return (bool) ($this->get('flags')->value & self::FLAG_DELETED);
}
/**
* Returns whether the entity was pushed embedded into another parent entity.
*
* This is always done for field collections but can also be enabled for other
* entities like paragraphs or media entities. This can save a lot of requests
* when entities aren't all syndicated individually.
*
* @param bool $set
* Optional parameter to set the value for the flag.
*
* @return bool
* True if the entity was pushed embedded into another parent entity.
*/
public function wasPushedEmbedded($set = NULL) {
if (TRUE === $set) {
$this->set('flags', $this->get('flags')->value | self::FLAG_PUSHED_EMBEDDED);
}
elseif (FALSE === $set) {
$this->set('flags', $this->get('flags')->value & ~self::FLAG_PUSHED_EMBEDDED);
}
return (bool) ($this->get('flags')->value & self::FLAG_PUSHED_EMBEDDED);
}
/**
* Returns whether the entity was pulled embedded in another parent entity.
*
* This is always done for field collections but can also be enabled for other
* entities like paragraphs or media entities. This can save a lot of requests
* when entities aren't all syndicated individually.
*
* @param bool $set
* Optional parameter to set the value for the flag.
*
* @return bool
* True if the entity was pulled embedded in another parent entity.
*/
public function wasPulledEmbedded($set = NULL) {
if (TRUE === $set) {
$this->set('flags', $this->get('flags')->value | self::FLAG_PULLED_EMBEDDED);
}
elseif (FALSE === $set) {
$this->set('flags', $this->get('flags')->value & ~self::FLAG_PULLED_EMBEDDED);
}
return (bool) ($this->get('flags')->value & self::FLAG_PULLED_EMBEDDED);
}
/**
* Set parent entity.
*
* If an entity is pushed or pulled embedded into another entity, we store
* that parent entity here. This is required so that at a later point we can
* still force pull and force push the embedded entity although it doesn't
* exist individually.
* This is also required to reset e.g. embedded paragraphs after the
* "Overwrite content locally" checkbox is unchecked.
*
* @param string $type
* Type of the parent entity.
* @param string $uuid
* UUID of the parent entity.
*/
public function setParentEntity($type, $uuid) {
$this->setData(self::DATA_PARENT_ENTITY, [
'type' => $type,
'uuid' => $uuid,
]);
}
/**
* See above.
*
* @return null|\Drupal\Core\Entity\EntityInterface
* Returns the parent entity or null if none exists.
*/
public function getParentEntity() {
$parent = $this->getData(self::DATA_PARENT_ENTITY);
if ($parent) {
$matches = \Drupal::entityTypeManager()
->getStorage($parent['type'])
->loadByProperties([
'uuid' => $parent['uuid'],
]);
if (!count($matches)) {
return NULL;
}
return reset($matches);
}
return NULL;
}
/**
* Returns the timestamp for the last pull.
*
* @return int
* The last pull timestamp.
*/
public function getLastPull(?string $language = NULL, $if_can_be_pulled = FALSE) {
if ($if_can_be_pulled) {
$flow = $this->getFlow();
if (!$flow) {
return NULL;
}
$entity = $this->getEntity();
if (!$entity) {
return NULL;
}
if (!$flow->getController()->canPullEntity($entity->getEntityTypeId(), $entity->bundle(), PullIntent::PULL_FORCED)) {
return NULL;
}
}
if ($language) {
$last_pull = $this->getTranslationData($language, EntityStatus::DATA_TRANSLATIONS_LAST_PULL);
if ($last_pull) {
return $last_pull;
}
// Data is available per translation, just not this one. So it explicitly wasn't pulled before, meaning we return NULL.
$any_translation_pull = $this->getTranslationData('*', EntityStatus::DATA_TRANSLATIONS_LAST_PULL);
if ($any_translation_pull) {
return NULL;
}
}
return $this->get('last_import')->value;
}
/**
* Set the last pull timestamp.
*
* @param int $timestamp
* The last pull timestamp to set.
*/
public function setLastPull($timestamp, ?string $language = NULL) {
if ($language) {
$this->setTranslationData($language, EntityStatus::DATA_TRANSLATIONS_LAST_PULL, $timestamp);
}
// All translations were pushed, so we need to update all translation indices.
else {
$any_translation_pull = $this->getTranslationData('*', EntityStatus::DATA_TRANSLATIONS_LAST_PULL);
// Unless none exists (for non-translatable entities and entities pulled before using v3.0.7)
if ($any_translation_pull) {
foreach ($this->getData(self::DATA_TRANSLATIONS) as $langcode => $values) {
if (isset($values[EntityStatus::DATA_TRANSLATIONS_LAST_PULL])) {
$this->setTranslationData($langcode, EntityStatus::DATA_TRANSLATIONS_LAST_PULL, $timestamp);
}
}
}
}
if ($this->getLastPull() == $timestamp) {
return;
}
$this->set('last_import', $timestamp);
// As this pull was successful, we can now reset the flags for status entity resets and failed pulls.
if (!empty($timestamp)) {
$this->wasLastPullReset(FALSE);
$this->didPullFail(FALSE);
// Delete status entities without Flow assigned- they're no longer needed.
$error_entities = EntityStatus::getInfosForEntity($this->getEntityTypeName(), $this->getUuid(), ['flow' => self::FLOW_NO_FLOW], TRUE);
foreach ($error_entities as $entity) {
$entity->delete();
}
}
// Otherwise this entity has been reset.
else {
$this->wasLastPullReset(TRUE);
}
}
/**
* Returns the UUID of the entity this information belongs to.
*
* @return string
* The entity UUID.
*/
public function getUuid() {
return $this->get('entity_uuid')->value;
}
/**
* Returns the entity type name of the entity this information belongs to.
*
* @return string
* The entity type name.
*/
public function getEntityTypeName() {
return $this->get('entity_type')->value;
}
/**
* Returns the timestamp for the last push.
*
* @return int
* The last push timestamp.
*/
public function getLastPush(?string $language = NULL) {
if ($language) {
$last_push = $this->getTranslationData($language, EntityStatus::DATA_TRANSLATIONS_LAST_PUSH);
if ($last_push) {
return $last_push;
}
// Data is available per translation, just not this one. So it explicitly wasn't pushed before, meaning we return NULL.
$any_translation_push = $this->getTranslationData('*', EntityStatus::DATA_TRANSLATIONS_LAST_PUSH);
if ($any_translation_push) {
return NULL;
}
}
return $this->get('last_export')->value;
}
/**
* Returns the timestamp of the last push trigger, if any.
*
* @param string|null $language
*
* @return int|null
*/
public function getLastPushTrigger(?string $language = NULL) {
if ($language) {
return $this->getTranslationData($language, self::DATA_TRANSLATIONS_LAST_PUSH_TRIGGER) ?? $this->getLastPush($language);
}
return $this->getData(self::DATA_TRANSLATIONS_LAST_PUSH_TRIGGER) ?? $this->getLastPush();
}
/**
* Set the last pull timestamp.
*
* @param int $timestamp
* The push timestamp to set.
*/
public function setLastPush($timestamp, ?string $language = NULL) {
if ($language) {
$this->setTranslationData($language, EntityStatus::DATA_TRANSLATIONS_LAST_PUSH, $timestamp);
}
// All translations were pushed, so we need to update all translation indices.
else {
$any_translation_push = $this->getTranslationData('*', EntityStatus::DATA_TRANSLATIONS_LAST_PUSH);
// Unless none exists (for non-translatable entities and entities pushed before using v3.0.7)
if ($any_translation_push) {
foreach ($this->getData(self::DATA_TRANSLATIONS) as $langcode => $values) {
if (isset($values[EntityStatus::DATA_TRANSLATIONS_LAST_PUSH])) {
$this->setTranslationData($langcode, EntityStatus::DATA_TRANSLATIONS_LAST_PUSH, $timestamp);
}
}
}
}
if ($this->getLastPush() == $timestamp) {
return;
}
$this->set('last_export', $timestamp);
// As this push was successful, we can now reset the flags for status entity resets and failed exports.
if (!empty($timestamp)) {
$this->wasLastPushReset(FALSE);
$this->didPushFail(FALSE);
}
// Otherwise this entity has been reset.
else {
$this->wasLastPushReset(TRUE);
}
}
/**
* Sets the timestamp of the last push trigger, if any.
*
* @param int|null $timestamp
* @param string|null $language
*
* @return int|null
*/
public function setLastPushTrigger(?int $timestamp, ?string $language = NULL) {
if ($language) {
$this->setTranslationData($language, self::DATA_TRANSLATIONS_LAST_PUSH_TRIGGER, $timestamp);
}
else {
$this->setData(self::DATA_TRANSLATIONS_LAST_PUSH_TRIGGER, $timestamp);
}
}
/**
* Sets the timestamp of when the translation was deleted.
*
* @param int $timestamp
* @param string $language
*
* @return null
*/
public function setTranslationDeletedAt(int $timestamp, string $language) {
$this->setTranslationData($language, self::DATA_TRANSLATIONS_DELETED_AT, $timestamp);
}
/**
* Gets the timestamp of when the translation was deleted.
*
* @param string $language
*
* @return int|null
*/
public function getTranslationDeletedAt(string $language) {
return $this->getTranslationData($language, self::DATA_TRANSLATIONS_DELETED_AT);
}
/**
* Adds a missing reference to the list.
*
* @param string $language
* @param string $field
* @param mixed $reference
*
* @return null
*/
public function addMissingReference(string $language, string $field, mixed $reference) {
$data = $this->getTranslationData($language, self::DATA_TRANSLATIONS_MISSING_REFERENCES);
if (!$data) {
$data = [];
}
if (empty($data[$field])) {
$data[$field] = [];
}
$data[$field][] = $reference;
$this->setTranslationData($language, self::DATA_TRANSLATIONS_MISSING_REFERENCES, $data);
}
/**
* Resets the missing reference list.
*
* @param string $language
* @param string $field
*
* @return null
*/
public function resetMissingReferences(string $language, string $field) {
$data = $this->getTranslationData($language, self::DATA_TRANSLATIONS_MISSING_REFERENCES);
if (!$data) {
return;
}
unset($data[$field]);
$this->setTranslationData($language, self::DATA_TRANSLATIONS_MISSING_REFERENCES, $data);
}
/**
* Gets all missing references.
*
* @param string $language
*
* @return array|null
*/
public function getMissingReferences(string $language) {
return $this->getTranslationData($language, self::DATA_TRANSLATIONS_MISSING_REFERENCES);
}
/**
* Check whether the given translation requires a push for it's deletion.
*
* @param string|null $language
*
* @return bool|string[]
*/
public function translationDeletionRequiresPush(?string $language = NULL) {
if (!$language) {
$translations = $this->getData(self::DATA_TRANSLATIONS);
if (!$translations) {
return FALSE;
}
$languages = [];
foreach ($translations as $language => $data) {
if ($this->translationDeletionRequiresPush($language)) {
$languages[] = $language;
}
}
return count($languages) ? $languages : FALSE;
}
$deleted_at = $this->getTranslationData($language, self::DATA_TRANSLATIONS_DELETED_AT);
if (!$deleted_at) {
return FALSE;
}
$pushed_at = $this->getLastPush($language);
$push_triggered_at = $this->getLastPushTrigger($language);
return $pushed_at && $pushed_at < $deleted_at && (!$push_triggered_at || $push_triggered_at < $deleted_at);
}
/**
* Get the flow.
*
* @return \Drupal\cms_content_sync\Entity\Flow
* The flow.
*/
public function getFlow() {
if (empty($this->get('flow')->value)) {
return NULL;
}
$flows = Flow::getAll();
if (empty($flows[$this->get('flow')->value])) {
return NULL;
}
return $flows[$this->get('flow')->value];
}
/**
* Get the pool.
*
* @return \Drupal\cms_content_sync\Entity\Pool
* The pool.
*/
public function getPool() {
return Pool::getAll()[$this->get('pool')->value];
}
/**
* Returns the entity type version.
*
* @return string
* The entity type version.
*/
public function getEntityTypeVersion() {
return $this->get('entity_type_version')->value;
}
/**
* Set the last pull timestamp.
*
* @param string $version
* The entity type version to be set.
*/
public function setEntityTypeVersion($version) {
$this->set('entity_type_version', $version);
}
/**
* Returns the entities source url.
*
* @return string
* The entities source url.
*/
public function getSourceUrl() {
return $this->get('source_url')->value;
}
/**
* Provide the entity's source url.
*
* @param string $url
* The source url to set.
*/
public function setSourceUrl($url) {
$this->set('source_url', $url);
}
/**
* Get a previously saved key=>value pair.
*
* @param null|string|string[] $key
* The key to retrieve.
*
* @see self::setData()
*
* @return mixed
* Whatever you previously stored here or NULL if the key doesn't exist.
*/
public function getData($key = NULL) {
$data = $this->get('data')->getValue();
if (empty($data)) {
return NULL;
}
$storage = &$data[0];
if (empty($key)) {
return $data;
}
if (!is_array($key)) {
$key = [$key];
}
foreach ($key as $index) {
if (!isset($storage[$index])) {
return NULL;
}
$storage = &$storage[$index];
}
return $storage;
}
/**
* Helper for ->getData() for translation specific meta data.
*
* @param string $language
* @param string $key
*
* @return mixed
*/
public function getTranslationData(string $language, string $key) {
if ($language === '*') {
$data = $this->getData(EntityStatus::DATA_TRANSLATIONS);
if ($data) {
foreach ($data as $langcode => $value) {
if (!empty($value[$key])) {
return $value[$key];
}
}
}
return NULL;
}
return $this->getData([EntityStatus::DATA_TRANSLATIONS, $language, $key]);
}
/**
* Set a key=>value pair.
*
* @param string|string[] $key
* The key to set (for hierarchical usage, provide
* an array of indices.
* @param mixed $value
* The value to set. Must be a valid value for Drupal's
* "map" storage (so basic types that can be serialized).
*/
public function setData($key, $value) {
$data = $this->get('data')->getValue();
if (!empty($data)) {
$data = $data[0];
}
else {
$data = [];
}
$storage = &$data;
if (is_string($key) && NULL === $value) {
if (isset($data[$key])) {
unset($data[$key]);
}
}
else {
if (!is_array($key)) {
$key = [$key];
}
foreach ($key as $index) {
if (!isset($storage[$index])) {
$storage[$index] = [];
}
$storage = &$storage[$index];
}
$storage = $value;
}
$this->set('data', $data);
}
/**
* Helper for ->setData() for translation specific meta data.
*
* @param string $language
* @param string $key
* @param mixed $value
*
* @return void
*/
public function setTranslationData(string $language, string $key, mixed $value) {
$this->setData([EntityStatus::DATA_TRANSLATIONS, $language, $key], $value);
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
$fields = parent::baseFieldDefinitions($entity_type);
$fields['flow'] = BaseFieldDefinition::create('string')
->setLabel(t('Flow'))
->setDescription(t('The flow the status entity is based on.'));
$fields['pool'] = BaseFieldDefinition::create('string')
->setLabel(t('Pool'))
->setDescription(t('The pool the entity is connected to.'));
$fields['entity_uuid'] = BaseFieldDefinition::create('string')
->setLabel(t('Entity UUID'))
->setDescription(t('The UUID of the entity that is synchronized.'))
->setSetting('max_length', 128);
$fields['entity_type'] = BaseFieldDefinition::create('string')
->setLabel(t('Entity type'))
->setDescription(t('The entity type of the entity that is synchronized.'));
$fields['entity_type_version'] = BaseFieldDefinition::create('string')
->setLabel(t('Entity type version'))
->setDescription(t('The version of the entity type provided by Content Sync.'))
->setSetting('max_length', 32);
$fields['source_url'] = BaseFieldDefinition::create('string')
->setLabel(t('Source URL'))
->setDescription(t('The entities source URL.'))
->setRequired(FALSE)
->setSetting('max_length', 1000);
$fields['last_export'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Last pushed'))
->setDescription(t('The last time the entity got pushed.'))
->setRequired(FALSE);
$fields['last_import'] = BaseFieldDefinition::create('timestamp')
->setLabel(t('Last pulled'))
->setDescription(t('The last time the entity got pulled.'))
->setRequired(FALSE);
$fields['flags'] = BaseFieldDefinition::create('integer')
->setLabel(t('Flags'))
->setDescription(t('Stores boolean information about the pushed/pulled entity.'))
->setSetting('unsigned', TRUE)
->setDefaultValue(0);
$fields['data'] = BaseFieldDefinition::create('map')
->setLabel(t('Data'))
->setDescription(t('Stores further information about the pushed/pulled entity that can also be used by entity and field handlers.'))
->setRequired(FALSE);
return $fields;
}
/**
* Get the entity push hash.
*
* @return null|string
* The entity push hash.
*/
public function getEntityPushHash() {
return $this->getData(self::DATA_ENTITY_PUSH_HASH);
}
/**
* Set the entity push hash.
*
* @param string $hash
* The hash to be set.
*/
public function setEntityPushHash($hash) {
$this->setData(self::DATA_ENTITY_PUSH_HASH, $hash);
}
/**
* Get all known translation source URLs.
*
* @return null|array
* An array of known translation source URLs.
*/
public function getAllTranslationSourceUrls() {
return $this->getData(self::DATA_TRANSLATION_SOURCE_URLS);
}
/**
* Get the source URL for the entity in the given language.
*
* @param string $language
* the language code to get the source URL for.
* @param bool $return_default_if_null
* if TRUE, will return $this->getSourceUrl() if there's no more specific URL available for the given translation language.
*
* @return null|string
* The source URL for the entity in the given language.
*/
public function getTranslationSourceUrl(string $language, $return_default_if_null = TRUE) {
$url = $this->getData([self::DATA_TRANSLATION_SOURCE_URLS, $language]);
if ($url) {
return $url;
}
return $return_default_if_null ? $this->getSourceUrl() : NULL;
}
/**
* Set the source URL for the entity in the given language.
*
* @param string $language
* the language code to Set the source URL for.
* @param string $url
* the URL to set.
*/
public function setTranslationSourceUrl(string $language, string $url) {
$this->setData([self::DATA_TRANSLATION_SOURCE_URLS, $language], $url);
}
}
