cms_content_sync-3.0.x-dev/src/SyncIntent.php
src/SyncIntent.php
<?php
namespace Drupal\cms_content_sync;
use Drupal\cms_content_sync\Entity\EntityStatus;
use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Exception\SyncException;
use Drupal\Core\Entity\EntityInterface;
/**
* Class SyncIntent.
*
* For every pull and push of every entity, an instance of this class is
* created and passed through the entity and field handlers. When pushing,
* you can set field values and embed entities. When pushing, you can
* receive these values back and resolve the entity references you saved.
*
* The same class is used for push and pull to allow adjusting values
* with hook integration.
*/
abstract class SyncIntent {
/**
* @var string ACTION_CREATE
* push/pull the creation of this entity
*/
public const ACTION_CREATE = 'create';
/**
* @var string ACTION_UPDATE
* push/pull the update of this entity
*/
public const ACTION_UPDATE = 'update';
/**
* @var string ACTION_DELETE
* push/pull the deletion of this entity
*/
public const ACTION_DELETE = 'delete';
/**
* @var string ACTION_DELETE_TRANSLATION
* Drupal doesn't update the ->getTranslationStatus($langcode) to
* TRANSLATION_REMOVED before calling hook_entity_translation_delete, so we
* need to use a custom action to circumvent deletions of translations of
* entities not being handled. This is only used for calling the
* ->pushEntity function. It will then be replaced by a simple
* ::ACTION_UPDATE.
*/
public const ACTION_DELETE_TRANSLATION = 'delete translation';
/**
* @var \Drupal\cms_content_sync\Entity\Flow
* @var \Drupal\cms_content_sync\Entity\Pool[]
* @var string entity type of the processed entity
* @var string bundle of the processed entity
* @var string UUID of the processed entity
* @var array the field values for the untranslated entity
* @var array The entities that should be processed along with this entity. Each entry is an array consisting of all SyncIntent::_*KEY entries.
* @var string the currently active language
* @var array the field values for the translation of the entity per language as key
*/
protected $flow;
protected $pools;
protected $reason;
protected $action;
protected $entity;
protected $entityType;
protected $bundle;
protected $uuid;
protected $id;
protected $activeLanguage;
protected $entityStatuses;
protected $individualTranslation;
/**
* @var \Drupal\cms_content_sync\Entity\EntityStatusProxyInterface
*/
protected $entity_status;
/**
* Ignore the given properties/fields completely.
*
* @var array
*/
protected $ignoreProperties = [];
/**
* @var float
*/
protected $start;
/**
* @var float
*/
protected $end = 0;
/**
* @var float
*/
protected $childTime = 0;
/**
* @var array
*/
protected $timers = [];
/**
* 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 string $uuid
* {@see SyncIntent::$uuid}.
* @param null $id
* @param string $source_url
* The source URL if pulled or NULL if pushed from this site.
* @param bool $individual_translation
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function __construct(Flow $flow, array $pools, $reason, $action, $entity_type, $bundle, $uuid, $id = NULL, $source_url = NULL, $individual_translation = FALSE) {
$this->start = microtime(TRUE);
$this->flow = $flow;
$this->pools = $pools;
$this->reason = $reason;
$this->action = $action;
$this->entityType = $entity_type;
$this->bundle = $bundle;
$this->uuid = $uuid;
$this->id = $id;
$this->entityStatuses = [];
$this->individualTranslation = $individual_translation;
foreach ($pools as $pool) {
$status = EntityStatus::getInfoForEntity($entity_type, $uuid, $flow, $pool);
if (!$status) {
$status = EntityStatus::create([
'flow' => $this->flow->id,
'pool' => $pool->id,
'entity_type' => $entity_type,
'entity_uuid' => $uuid,
'entity_type_version' => Flow::getEntityTypeVersion($entity_type, $bundle),
'flags' => 0,
'source_url' => $source_url,
]);
}
$this->entityStatuses[$pool->id] = $status;
}
if (1 === count($this->entityStatuses)) {
$this->entity_status = reset($this->entityStatuses);
}
else {
$this->entity_status = new EntityStatusProxy($this->entityStatuses);
}
}
/**
*
*/
public function startTimer($name) {
$this->timers[$name] = [
'start' => microtime(TRUE),
'end' => NULL,
'duration' => NULL,
];
}
/**
*
*/
public function stopTimer($name) {
$this->timers[$name]['end'] = microtime(TRUE);
$this->timers[$name]['duration'] = $this->timers[$name]['end'] - $this->timers[$name]['start'];
}
/**
* Whether or not only the current translation should be pushed or pulled.
* By default, all translations are sent and received in the same request
* to keep the number of requests low and performance up. But if we are
* dealing with a lot of translations or very nested entities then we want
* to instead use one request per translation.
*
* @return bool
*/
public function isIndividualTranslation() {
return $this->individualTranslation;
}
/**
* Ignore the provided property or field completely when pushing / pulling.
* This allows you to programmatically exclude specific properties e.g. if
* you want to customize path alias settings / creation per site.
*/
public function ignoreProperty(string $name) {
$this->ignoreProperties[] = $name;
}
/**
* Check whether the given property / field should be ignored. Used by the
* entity handlers.
*
* @return bool
*/
public function shouldIgnoreProperty(string $name) {
return in_array($name, $this->ignoreProperties);
}
/**
* Execute the intent.
*
* @return bool
*/
abstract public function execute();
/**
* @return string
*/
public function getReason() {
return $this->reason;
}
/**
* @return string
*/
public function getAction() {
return $this->action;
}
/**
* @return \Drupal\cms_content_sync\Entity\Flow
*/
public function getFlow() {
return $this->flow;
}
/**
* @return string[]
*/
public function getPoolIds() {
$ids = [];
foreach ($this->pools as $pool) {
$ids[] = $pool->id;
}
return $ids;
}
/**
* @return \Drupal\Core\Entity\EntityInterface
* The entity of the intent, if it already exists locally
*/
public function getEntity($fresh = FALSE) {
if ($fresh) {
$this->entity = NULL;
}
if (!$this->entity) {
if ($this->id) {
$entity = \Drupal::entityTypeManager()
->getStorage($this->entityType)
->load($this->id);
}
else {
$entity = \Drupal::service('entity.repository')
->loadEntityByUuid($this->entityType, $this->uuid);
// If the paragraph was initially created in the wrong language, it may
// not have any default language assigned.
// The query above implicitly adds a filter for the default language if
// not explicitly disabled, so we're trying again but without caring
// about that flag.
if (!$entity && $this->entityType === 'paragraph') {
$entities = \Drupal::entityTypeManager()
->getStorage($this->entityType)
->loadByProperties(['uuid' => $this->uuid, 'default_langcode' => NULL]);
if (count($entities)) {
$entity = reset($entities);
}
}
}
if ($entity) {
$this->setEntity($entity);
}
}
return $this->entity;
}
/**
* Returns the entity status per pool.
*/
public function getAllEntityStatuses() {
return $this->entityStatuses;
}
/**
* Returns the entity status proxy for all pools.
*
* @return \Drupal\cms_content_sync\Entity\EntityStatusProxyInterface
*/
public function getEntityStatus() {
return $this->entity_status;
}
/**
* Set the entity when pulling (may not be saved yet then).
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity you just created.
*
* @throws \Drupal\cms_content_sync\Exception\SyncException
*
* @return $this|EntityInterface|TranslatableInterface
*/
public function setEntity(EntityInterface $entity) {
if ($entity === $this->entity) {
return $this->entity;
}
if ($this->entity && $this->entity->id() !== $entity->id()) {
throw new SyncException(SyncException::CODE_INTERNAL_ERROR, NULL, 'Attempting to re-set existing entity.');
}
/**
* @var \Drupal\Core\Entity\EntityInterface $entity
* @var \Drupal\Core\Entity\TranslatableInterface $entity
*/
$this->entity = $entity;
if ($this->entity) {
if ($this->activeLanguage) {
$this->entity = $this->entity->getTranslation($this->activeLanguage);
}
}
return $this->entity;
}
/**
* Retrieve a value you stored before via ::setstatusData().
*
* @see EntityStatus::getData()
*
* @param string|string[] $key
* The key to retrieve.
*
* @return mixed whatever you previously stored here
*/
public function getStatusData($key) {
return $this->entity_status ? $this->entity_status->getData($key) : NULL;
}
/**
* Store a key=>value pair for later retrieval.
*
* @see EntityStatus::setData()
*
* @param string|string[] $key
* The key to store the data against. Especially
* field handlers should use nested keys like ['field','[name]','[key]'].
* @param mixed $value
* Whatever simple value you'd like to store.
*
* @return bool
*/
public function setStatusData($key, $value) {
if (!$this->entity_status) {
return FALSE;
}
$this->entity_status->setData($key, $value);
return TRUE;
}
/**
* Change the language used for provided field values. If you want to add a
* translation of an entity, the same SyncIntent is used. First, you
* add your fields using self::setField() for the untranslated version.
* After that you call self::changeTranslationLanguage() with the language
* identifier for the translation in question. Then you perform all the
* self::setField() updates for that language and eventually return to the
* untranslated entity by using self::changeTranslationLanguage() without
* arguments.
*
* @param string $language
* The identifier of the language to switch to or NULL to reset.
*/
public function changeTranslationLanguage($language = NULL) {
$this->activeLanguage = $language;
if ($this->entity) {
if ($language) {
$this->entity = $this->entity->getTranslation($language);
}
else {
$this->entity = $this->entity->getUntranslated();
}
}
}
/**
* Return the language that's currently used.
*
* @see SyncIntent::changeTranslationLanguage() for a detailed explanation.
*/
public function getActiveLanguage() {
return $this->activeLanguage;
}
/**
* @see SyncIntent::$entityType
*
* @return string
*/
public function getEntityType() {
return $this->entityType;
}
/**
* @see SyncIntent::$bundle
*/
public function getBundle() {
return $this->bundle;
}
/**
* @return null|string
*
* @see SyncIntent::$uuid
*/
public function getUuid() {
return $this->uuid;
}
/**
* @return null|string
*
* @see SyncIntent::$id
*/
public function getId() {
return $this->id;
}
/**
* @return string
*/
public function getSharedId() {
return $this->getId() ?? $this->getUuid();
}
/**
*
*/
protected function formatDuration() {
if (!$this->end) {
$this->end = microtime(TRUE);
}
$duration = $this->end - $this->start;
$duration_formatted = number_format($duration, 3) . 's';
if ($this->childTime) {
$duration_without_children = $duration - $this->childTime;
if ($duration_without_children < 0) {
$duration_without_children = 0;
}
$duration_without_children_formatted = number_format($duration_without_children, 3) . 's';
$duration_formatted .= '/' . $duration_without_children_formatted;
}
return $duration_formatted;
}
/**
*
*/
protected function formatTimers() {
$formatted = [];
foreach ($this->timers as $name => $properties) {
if (NULL === $properties['duration']) {
continue;
}
$formatted[] = $name . ': ' . number_format($properties['duration'], 3) . 's';
}
if (!count($formatted)) {
return '-';
}
return implode(', ', $formatted);
}
}
