You are here

ContentEntityConflictHandler.php in Conflict 8.2

File

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

namespace Drupal\conflict\Entity;

use Drupal\Component\Utility\NestedArray;
use Drupal\conflict\ConflictResolver\ConflictResolverManagerInterface;
use Drupal\conflict\ConflictTypes;
use Drupal\conflict\FieldComparatorManagerInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\ParameterBag;
class ContentEntityConflictHandler implements EntityConflictHandlerInterface, EntityHandlerInterface {
  use StringTranslationTrait;
  use MessengerTrait;
  use DependencySerializationTrait {
    __wakeup as traitWakeup;
    __sleep as traitSleep;
  }

  /**
   * The entity type.
   *
   * @var \Drupal\Core\Entity\ContentEntityTypeInterface
   */
  protected $entityType;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The module handler to invoke the alter hook.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The entity storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected $storage;

  /**
   * The key value store storing the original entity.
   *
   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
   */
  protected $keyValueOriginalEntity;

  /**
   * The field comparator manager.
   *
   * @var \Drupal\conflict\FieldComparatorManagerInterface
   */
  protected $fieldComparatorManager;

  /**
   * The conflict manager.
   *
   * @var \Drupal\conflict\ConflictResolver\ConflictResolverManagerInterface
   */
  protected $conflictManager;

  /**
   * EntityConflictResolutionHandlerDefault constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value_original_entity
   *   The key value factory for storing the conflict original entity.
   * @param \Drupal\conflict\FieldComparatorManagerInterface $field_comparator_manager
   *   The field comparator manager.
   * @param \Drupal\conflict\ConflictResolver\ConflictResolverManagerInterface $conflict_manager
   *   The conflict manager.
   */
  public function __construct(EntityTypeInterface $entity_type, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, KeyValueStoreInterface $key_value_original_entity, FieldComparatorManagerInterface $field_comparator_manager, ConflictResolverManagerInterface $conflict_manager) {
    $this->entityType = $entity_type;
    $this->entityTypeManager = $entity_type_manager;
    $this->storage = $entity_type_manager
      ->getStorage($entity_type
      ->id());
    $this->moduleHandler = $module_handler;
    $this->keyValueOriginalEntity = $key_value_original_entity;
    $this->fieldComparatorManager = $field_comparator_manager;
    $this->conflictManager = $conflict_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
    return new static($entity_type, $container
      ->get('entity_type.manager'), $container
      ->get('module_handler'), $container
      ->get('keyvalue.expirable')
      ->get('conflict_original_entity'), $container
      ->get('conflict.field_comparator.manager'), $container
      ->get('conflict_resolver.manager'));
  }

  /**
   * {@inheritdoc}
   */
  public function entityFormAlter(array &$form, FormStateInterface $form_state, EntityInterface $entity, $inline_entity_form = FALSE) {

    // Let the conflict module updates the other translations before any other
    // entity builder has run, otherwise we might overwrite changes that will be
    // made by the entity builders on other translations. An example for this is
    // \Drupal\content_translation\ContentTranslationHandler::entityFormEntityBuild().
    $form['#entity_builders'] = isset($form['#entity_builders']) ? $form['#entity_builders'] : [];
    array_unshift($form['#entity_builders'], [
      $this,
      'entityFormEntityBuilder',
    ]);
    if (!isset($form['conflict_entity_original_hash'])) {
      $input = $form_state
        ->getUserInput();
      $hash_path = $form['#parents'];
      $hash_path[] = 'conflict_entity_original_hash';
      $hash = NestedArray::getValue($input, $hash_path) ?: $entity->{EntityConflictHandlerInterface::CONFLICT_ENTITY_ORIGINAL_HASH};
      $form['conflict_entity_original_hash'] = [
        '#type' => 'hidden',
        '#default_value' => $hash,
      ];
    }

    // @todo This check is actually not really needed, as #validate is only
    // executed at form level, not at inline form level, where only
    // #element_validate should be executed.
    if (!$inline_entity_form) {

      // @todo we have to ensure that our validate method is running at the end
      // but if there is another module which is moving its form_alter hook to
      // the end then there might be some collision. Should we decorate the
      // form builder instead and add our validate method after all the hooks
      // have run?
      $form['#validate'][] = [
        $this,
        'entityMainFormValidateLast',
      ];
    }
  }

  /**
   * Entity builder method.
   *
   * @param string $entity_type_id
   *   The entity type ID.
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity.
   * @param $form
   *   The entity form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @see \Drupal\conflict\Entity\ContentEntityConflictHandler::entityFormAlter()
   */
  public function entityFormEntityBuilder($entity_type_id, EntityInterface $entity, &$form, FormStateInterface $form_state) {

    // Run only as part of the final form level submission.
    if (!$this
      ->isFormLevelSubmission($form_state)) {
      return;
    }
    if ($entity instanceof ContentEntityInterface && !$entity
      ->isNew()) {
      if ($entity
        ->isDefaultRevision()) {

        /** @var \Drupal\Core\Entity\ContentEntityInterface $entity_server */
        $id = $entity
          ->id();
        $entity_server = $this->storage
          ->loadUnchanged($id);
      }
      else {

        // TODO - how to deal with forward revisions?
        return;
      }
      $input = $form_state
        ->getUserInput();
      $hash_path = $form['#parents'];
      $hash_path[] = 'conflict_entity_original_hash';
      $hash = NestedArray::getValue($input, $hash_path);

      /** @var \Drupal\Core\Entity\ContentEntityInterface $entity_original */
      $entity_original = unserialize($this->keyValueOriginalEntity
        ->get($hash));

      // Set the original entity as it might have changed. The original entity
      // in our context is the one that was used to build the form initially
      // and not the unchanged entity.
      $entity->{static::CONFLICT_ENTITY_ORIGINAL} = $entity_original;

      // Currently we do not support concurrent editing in the following cases:
      //  - editing a translation that is removed on the newest version.
      //  - while creating a new translation.
      $edited_langcode = $entity
        ->language()
        ->getId();
      if (!$entity_server
        ->hasTranslation($edited_langcode)) {
        if ($entity_original
          ->hasTranslation($edited_langcode)) {

          // Currently being on a translation that has been removed in the
          // newest version.
          $form_state
            ->setError($form, t('You are editing a translation, that has been removed meanwhile. As a result, your changes cannot be saved.'));
          return;
        }
        else {

          // @todo A new translation is being added. Currently we do not have
          // any support for concurrent editing during translating content. If
          // the entity has been modified meanwhile then the
          // EntityChangedConstraintValidator will fail.
          return;
        }
      }
      else {
        if (!$entity_original
          ->hasTranslation($edited_langcode)) {
          $form_state
            ->setError($form, t('You are creating a translation, that has been created meanwhile. As a result, your changes cannot be saved.'));
          return;
        }
      }

      // Work directly with the entity translations.
      $entity_server = $entity_server
        ->getTranslation($edited_langcode);
      $entity_original = $entity_original
        ->getTranslation($edited_langcode);

      // Check if the entity requires a merge.
      $needs_merge = $this
        ->needsMerge($entity, $entity_original, $entity_server, FALSE);
      if ($needs_merge) {

        /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
        $form_display = $form_state
          ->getFormObject()
          ->getFormDisplay($form_state);

        // Append each field's widget to the field so that further processing
        // can access it.
        foreach (array_keys($form_display
          ->getComponents()) as $field_name) {
          if ($entity
            ->getFieldDefinition($field_name)) {
            $entity
              ->get($field_name)->conflictWidget = $form_display
              ->getRenderer($field_name);
          }
        }

        // Auto merge changes in other translations.
        $this
          ->autoMergeNonEditedTranslations($entity, $entity_server);

        // Auto merge changes in non-editable fields.
        $this
          ->autoMergeNonEditableFields($entity, $entity_server, $form_display);

        // Auto merge non-changed fields.
        $this
          ->autoMergeNotTouchedFields($entity, $entity_original, $entity_server, $form, $form_state);

        // Auto merge entity metadata.
        $this
          ->autoMergeEntityMetadata($entity, $entity_server, $form, $form_state);

        // Run the entities through the event system for conflict discovery
        // and resolution.
        $context = new ParameterBag([
          'form' => $form,
          'form_state' => $form_state,
          'form_display' => $form_display,
        ]);

        // Disable the merge remote only changes strategy, which is enabled by
        // default.
        // TODO reconsider the decision for having all strategies enabled by
        // default! They should be instead enabled through configuration and/or
        // hooks/events.
        $context
          ->set('merge_strategy.disabled', [
          'conflict.merge_remote_only_changes',
        ]);
        $conflicts = $this->conflictManager
          ->resolveConflicts($entity, $entity_server, $entity_original, $entity, $context);

        // In case the entity still has conflicts then a user interaction is
        // needed.
        $needs_merge = !empty($conflicts) || $this
          ->needsMerge($entity, $entity_original, $entity_server, TRUE);
        if ($needs_merge) {

          // Prepare the entity for conflict resolution.
          $this
            ->prepareConflictResolution($entity, $entity_server);

          // If the entity supports conflict UI merge then add the path to it to
          // the form state storage, otherwise flag the form with an error as it
          // would've been flagged by the entity constraint "EntityChanged".
          // @see \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityChangedConstraint::$message
          // @see \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityChangedConstraintValidator::validate()
          if ($this->entityType
            ->get('conflict_ui_merge_supported') && !(bool) $form_state
            ->get('manual-merge-not-possible')) {
            $path = implode('.', $form['#parents']);
            $conflict_paths = $form_state
              ->get('conflict.paths') ?: [];
            $conflict_paths[$path] = [
              'entity_type' => $entity_type_id,
              'entity_id' => $entity
                ->id(),
            ];
            $form_state
              ->set('conflict.paths', $conflict_paths);
          }
          else {
            $message = t('The content has either been modified by another user, or you have already submitted modifications. As a result, your changes cannot be saved.');
            $message .= ' ' . t('Unfortunately no conflict resolution could be provided for the set of changes.');
            $form_state
              ->setError($form, $message);
          }
        }
      }
    }
  }

  /**
   * Form level validation handler running after all others.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @see \Drupal\conflict\Entity\ContentEntityConflictHandler::entityFormAlter()
   */
  public function entityMainFormValidateLast(&$form, FormStateInterface $form_state) {

    // Run only as part of the final form level submission or if the form has
    // been completely validated and has no errors.
    if (!$this
      ->isFormLevelSubmission($form_state) || $form_state::hasAnyErrors() || empty($form_state
      ->get('conflict.paths'))) {
      return;
    }

    /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
    $form_object = $form_state
      ->getFormObject();

    // Support https://www.drupal.org/node/2833682.
    if (method_exists($form_object, 'getIntermediateEntity')) {
      $entity = $form_object
        ->getIntermediateEntity();
    }
    else {
      $entity = $form_object
        ->buildEntity($form, $form_state);
    }

    // Exchange the entity with the intermediate one and flag the form so that
    // the conflict resolution form gets appended on form rebuild.
    $form_object
      ->setEntity($entity);
    $form_state
      ->set('conflict.build_conflict_resolution_form', TRUE);
    $form_state
      ->setCached(TRUE);
    $form_state
      ->setRebuild(TRUE);
    $conflict_paths =& $form_state
      ->get('conflict.paths');
    $this->moduleHandler
      ->alter('conflict_paths', $conflict_paths, $form_state);
  }

  /**
   * Performs a check if a merge is required.
   *
   * Note that if the entity doesn't implement the EntityChangedInterface and no
   * extended check ($extended_check = FALSE) is performed, then the method will
   * return TRUE as there is no short way of checking for changes, in which case
   * the extended check should be performed afterwards.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_local_edited
   *   The locally edited entity.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_local_original
   *   The original not edited entity used to build the form.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_server
   *   The unchanged entity loaded from the storage.
   * @param bool $extended_check
   *   Specifies whether an extended check should be performed.
   *
   * @return bool
   *   TRUE if a merge is needed, FALSE otherwise.
   */
  protected function needsMerge(ContentEntityInterface $entity_local_edited, ContentEntityInterface $entity_local_original, ContentEntityInterface $entity_server, $extended_check) {
    if ($this->entityType
      ->isRevisionable()) {
      if ($entity_local_edited
        ->getRevisionId() != $entity_server
        ->getRevisionId() || $entity_local_edited
        ->getLoadedRevisionId() != $entity_server
        ->getLoadedRevisionId()) {
        return TRUE;
      }
    }
    if ($extended_check) {
      return $this
        ->hasConflicts($entity_local_edited, $entity_local_original, $entity_server);
    }
    else {
      $entity_server_langcodes = array_keys($entity_server
        ->getTranslationLanguages());
      $entity_local_edited_langcodes = array_keys($entity_local_edited
        ->getTranslationLanguages());
      if ($entity_server_langcodes != $entity_local_edited_langcodes) {
        return TRUE;
      }
      if ($entity_local_edited instanceof EntityChangedInterface) {
        foreach ($entity_server_langcodes as $langcode) {

          /** @var \Drupal\Core\Entity\EntityChangedInterface $entity_server_translation */
          $entity_server_translation = $entity_server
            ->getTranslation($langcode);

          /** @var \Drupal\Core\Entity\EntityChangedInterface $entity_local_edited_translation */
          $entity_local_edited_translation = $entity_local_edited
            ->getTranslation($langcode);
          if ($entity_server_translation
            ->getChangedTime() != $entity_local_edited_translation
            ->getChangedTime()) {
            return TRUE;
          }
        }
      }
      else {

        // If the entity doesn't implement the  EntityChangedInterface and a
        // non-extended check is performed then we return TRUE, so that
        // auto-merge is executed and an extended check is made afterwards.
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * Merges translatable fields of non-edited translations.
   *
   * Additionally deleted translations will be removed and new translations will
   * be added.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_local_edited
   *   The locally edited entity.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_server
   *   The unchanged entity loaded from the storage.
   */
  protected function autoMergeNonEditedTranslations(ContentEntityInterface $entity_local_edited, ContentEntityInterface $entity_server) {
    $edited_langcode = $entity_local_edited
      ->language()
      ->getId();
    $entity_server_langcodes = array_keys($entity_server
      ->getTranslationLanguages());
    $entity_local_edited_langcodes = array_keys($entity_local_edited
      ->getTranslationLanguages());
    foreach ($entity_server_langcodes as $langcode) {
      if ($langcode == $edited_langcode) {
        continue;
      }

      // @todo we should set that the translation is not new.
      $entity_server_translation = $entity_server
        ->getTranslation($langcode);
      $entity_local_edited_translation = $entity_local_edited
        ->hasTranslation($langcode) ? $entity_local_edited
        ->getTranslation($langcode) : $entity_local_edited
        ->addTranslation($langcode);

      // @todo If the entity implements the EntityChangedInterface then first
      // check the changed timestamps as a shortcut to skip updating fields of
      // translations that haven't changed, but for that we have to ensure
      if ($entity_server_translation
        ->getChangedTime() != $entity_local_edited_translation
        ->getChangedTime()) {
        foreach ($entity_server_translation
          ->getTranslatableFields() as $field_name => $field_item_list) {
          $entity_local_edited_translation
            ->set($field_name, $field_item_list
            ->getValue());
        }
      }
    }
    foreach (array_diff($entity_local_edited_langcodes, $entity_server_langcodes) as $langcode) {
      $entity_local_edited
        ->removeTranslation($langcode);
    }
  }

  /**
   * Merges non-editable fields.
   *
   * As non-editable fields are considered fields that are not contained in the
   * form display or the current user does not have edit access for them.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_local_edited
   *   The locally edited entity.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_server
   *   The unchanged entity loaded from the storage.
   * @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display
   *   The form display that the entity form operates with.
   */
  protected function autoMergeNonEditableFields(ContentEntityInterface $entity_local_edited, ContentEntityInterface $entity_server, EntityFormDisplayInterface $form_display) {
    $components = $form_display
      ->getComponents();
    foreach ($entity_local_edited
      ->getFields() as $field_name => $items) {
      if (!isset($components[$field_name]) || !$items
        ->access('edit')) {
        $items
          ->setValue($entity_server
          ->get($field_name)
          ->getValue(TRUE));
      }
    }
  }

  /**
   * Merges non-touched fields i.e. prevents reverts.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_local_edited
   *   The locally edited entity.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_local_original
   *   The original not edited entity used to build the form.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_server
   *   The unchanged entity loaded from the storage.
   * @param array $form
   *   The form array of the entity form. Might be used to retrieve the path to
   *   the entity in the form state values or user input.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form. Might be used to alter the user input to
   *   reflect new metadata from the server entity.
   */
  protected function autoMergeNotTouchedFields(ContentEntityInterface $entity_local_edited, ContentEntityInterface $entity_local_original, ContentEntityInterface $entity_server, $form, FormStateInterface $form_state) {
    $conflicts = $this
      ->getConflicts($entity_local_original, $entity_local_edited, $entity_server);
    if ($conflicts) {
      $input =& $form_state
        ->getUserInput();
      $auto_merged_untouched_fields = [];
      foreach ($conflicts as $field_name => $conflict_type) {
        if ($conflict_type === ConflictTypes::CONFLICT_TYPE_REMOTE) {
          $entity_local_edited
            ->set($field_name, $entity_server
            ->get($field_name)
            ->getValue());
          $auto_merged_untouched_fields[] = $field_name;

          // Remove the value from the user input as there might be conflicts and
          // the form will be returned back to the user for manually resolving
          // them. In this case we want to show the auto merged values and notify
          // the user about this action.
          $path = $form['#parents'];
          $path[] = $field_name;
          NestedArray::unsetValue($input, $path);
        }
      }
      if ($auto_merged_untouched_fields) {

        // Conflicts can be present only in fields, which are presented in the
        // form, but it might happen that there is custom code updating a field
        // not part of the form, in which case it will be confusing for the user
        // if we notify about an auto merge on that field. Therefore we ensure
        // that we notify the user only about auto merges on fields in the
        // current form.

        /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */
        $form_display = $form_state
          ->getFormObject()
          ->getFormDisplay($form_state);
        $auto_merged_untouched_fields_form = array_intersect($auto_merged_untouched_fields, array_keys($form_display
          ->getComponents()));
        if ($auto_merged_untouched_fields_form) {
          $field_labels = array_map(function ($name) use ($entity_local_edited) {
            return $entity_local_edited
              ->get($name)
              ->getFieldDefinition()
              ->getLabel();
          }, $auto_merged_untouched_fields);

          // Notify the user about the auto merged fields.
          $this
            ->messenger()
            ->addMessage($this
            ->t('The content has been modified meanwhile. Changes for the following fields have been successfully applied: %fields.', [
            '%fields' => \implode(', ', $field_labels),
          ]));
        }
      }
    }
  }

  /**
   * Merges entity metadata.
   *
   * For entities implementing the EntityChangedInterface, the changed time will
   * be merged.
   * For revisionable entities the revision ID will be merged.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_local_edited
   *   The locally edited entity.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_server
   *   The unchanged entity loaded from the storage.
   * @param array $form
   *   The form array of the entity form. Might be used to retrieve the path to
   *   the entity in the form state values or user input.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form. Might be used to alter the user input to
   *   reflect new metadata from the server entity.
   */
  protected function autoMergeEntityMetadata(ContentEntityInterface $entity_local_edited, ContentEntityInterface $entity_server, $form, FormStateInterface $form_state) {
    if ($entity_local_edited instanceof EntityChangedInterface && $entity_local_edited
      ->getChangedTime() != $entity_server
      ->getChangedTime()) {
      $entity_local_edited
        ->setChangedTime($entity_server
        ->getChangedTime());

      // We have to update the changed timestamp stored as hidden value in the
      // form to the new value resulted from the merge, otherwise the old one
      // will be mapped on submit.
      $changed_path = $form['#parents'];
      $changed_path[] = 'changed';
      $input =& $form_state
        ->getUserInput();
      NestedArray::setValue($input, $changed_path, $entity_local_edited
        ->getChangedTime());
    }
    if ($this->entityType
      ->isRevisionable()) {
      $entity_local_edited
        ->set($this->entityType
        ->getKey('revision'), $entity_server
        ->getRevisionId());
      $entity_local_edited
        ->updateLoadedRevisionId();
    }
  }

  /**
   * Checks whether there are conflicts.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_local_edited
   *   The locally edited entity.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_local_original
   *   The original not edited entity used to build the form.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity_server
   *   The unchanged entity loaded from the storage.
   *
   * @return bool
   *
   */
  protected function hasConflicts(ContentEntityInterface $entity_local_edited, ContentEntityInterface $entity_local_original, ContentEntityInterface $entity_server) {
    $entity_type_id = $this->entityType
      ->id();
    $entity_bundle = $entity_local_edited
      ->bundle();
    $langcode = $entity_local_edited
      ->language()
      ->getId();
    $entity_server = $entity_server
      ->getTranslation($langcode);
    $entity_local_original = $entity_local_original
      ->getTranslation($langcode);
    $skip_fields = [];

    // The revision created field is updated constantly and it will always cause
    // conflicts, therefore we skip it here, as it gets updated correctly on
    // submit during entity building from user input.
    // @see \Drupal\Core\Entity\ContentEntityForm::buildEntity().
    $skip_fields = array_flip($this->entityType
      ->getRevisionMetadataKeys());
    foreach ($entity_local_edited
      ->getFields() as $field_name => $field_items_list_local_edited) {
      if (isset($skip_fields[$field_name])) {
        continue;
      }
      $field_definition = $field_items_list_local_edited
        ->getFieldDefinition();

      // There could be no conflicts in read only fields.
      if ($field_definition
        ->isReadOnly()) {
        continue;
      }
      $field_type = $field_definition
        ->getType();
      $field_items_list_server = $entity_server
        ->get($field_name);
      $field_items_list_local_original = $entity_local_original
        ->get($field_name);

      // Check for changes between the server and the locally edited version. If
      // there are no changes between them then it might happen that a field
      // is changed in both versions to the same value, which we do not
      // consider as any conflict.
      if ($this->fieldComparatorManager
        ->hasChanged($field_items_list_server, $field_items_list_local_edited, $langcode, $entity_type_id, $entity_bundle, $field_type, $field_name)) {

        // Check for changes between the server the locally used original
        // version. If the server version has changed compared to the locally
        // used original version then there is a conflict either
        //   - value changed only on the server
        //   - value changed both on the server and locally
        if ($this->fieldComparatorManager
          ->hasChanged($field_items_list_server, $field_items_list_local_original, $langcode, $entity_type_id, $entity_bundle, $field_type, $field_name)) {
          return TRUE;
        }
      }
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function getConflicts(EntityInterface $entity_local_original, EntityInterface $entity_local_edited, EntityInterface $entity_server) {
    $conflicts = [];
    $entity_type_id = $this->entityType
      ->id();
    $entity_bundle = $entity_local_edited
      ->bundle();
    $langcode = $entity_local_edited
      ->language()
      ->getId();
    $entity_server = $entity_server
      ->getTranslation($langcode);
    $entity_local_original = $entity_local_original
      ->getTranslation($langcode);

    // The revision created field is updated constantly and it will always cause
    // conflicts, therefore we skip it here, as it gets updated correctly on
    // submit during entity building from user input.
    // @see \Drupal\Core\Entity\ContentEntityForm::buildEntity().
    $skip_fields = array_flip($this->entityType
      ->getRevisionMetadataKeys());
    foreach ($entity_local_edited
      ->getFields() as $field_name => $field_items_list_local_edited) {
      if (isset($skip_fields[$field_name])) {
        continue;
      }
      $field_definition = $field_items_list_local_edited
        ->getFieldDefinition();

      // There could be no conflicts in read only fields.
      if ($field_definition
        ->isReadOnly()) {
        continue;
      }
      $field_type = $field_definition
        ->getType();
      $field_items_list_server = $entity_server
        ->get($field_name);
      $field_items_list_local_original = $entity_local_original
        ->get($field_name);
      $conflict_type = $this->fieldComparatorManager
        ->getConflictType($field_items_list_local_edited, $field_items_list_server, $field_items_list_local_original, $langcode, $entity_type_id, $entity_bundle, $field_type, $field_name);
      if ($conflict_type) {
        $conflicts[$field_name] = $conflict_type;
      }
    }
    return $conflicts;
  }

  /**
   * Determines if the form level submission has been triggered.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return bool
   *   TRUE if the form has been submitted for final submission, FALSE otherwise.
   */
  protected function isFormLevelSubmission(FormStateInterface $form_state) {

    // @todo find a safer way of determining if this is a form level submission.
    return in_array('::submitForm', $form_state
      ->getSubmitHandlers());
  }

  /**
   * {@inheritdoc}
   */
  public function prepareConflictResolution(EntityInterface $entity, EntityInterface $entity_server = NULL) {

    // Manual merge is needed if even after the auto-merge of non-edited
    // translations, fields with no edit access and entity metadata there
    // are still conflicts in the current translation and/or
    // non-translatable fields.
    $entity->{static::CONFLICT_ENTITY_NEEDS_MANUAL_MERGE} = TRUE;

    // Append the server entity that will be used for building the manual
    // conflict resolution.
    $entity->{static::CONFLICT_ENTITY_SERVER} = $entity_server ?? 'removed';
  }

  /**
   * {@inheritdoc}
   */
  public function finishConflictResolution(EntityInterface $entity, $path_parents, FormStateInterface $form_state) {

    // Exchange the original entity with the server one as after the user
    // interaction (manual merge) the current entity has as origin the server
    // entity.
    $entity_server = $entity->{static::CONFLICT_ENTITY_SERVER};
    $entity->{static::CONFLICT_ENTITY_ORIGINAL} = $entity_server;
    $entity_server_hash = $entity_server->{static::CONFLICT_ENTITY_ORIGINAL_HASH};
    $entity->{static::CONFLICT_ENTITY_ORIGINAL_HASH} = $entity_server_hash;

    // Flag the entity that it doesn't need a manual merge anymore.
    $entity->{static::CONFLICT_ENTITY_NEEDS_MANUAL_MERGE} = FALSE;

    // Exchange the original entity's hash value in the user input as well to be
    // prepared for next submits in case the form isn't submitted immediately,
    // but the work is continued.
    $input =& $form_state
      ->getUserInput();
    $hash_path_parents = $path_parents;
    $hash_path_parents[] = 'conflict_entity_original_hash';
    NestedArray::setValue($input, $hash_path_parents, $entity_server_hash);
  }

  /**
   * {@inheritdoc}
   */
  public function __wakeup() {
    $this
      ->traitWakeup();
    $this->storage = $this->entityTypeManager
      ->getStorage($this->entityType
      ->id());
  }

  /**
   *{ @inheritdoc}
   */
  public function __sleep() {
    $vars = $this
      ->traitSleep();
    unset($vars['storage']);
    return $vars;
  }

}

Classes