You are here

class ContentTranslationSynchronizedFieldsConstraintValidator in Drupal 9

Same name and namespace in other branches
  1. 8 core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraintValidator.php \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraintValidator

Checks that synchronized fields are handled correctly in pending revisions.

As for untranslatable fields, two modes are supported:

  • When changes to untranslatable fields are configured to affect all revision translations, synchronized field properties can be changed only in default revisions.
  • When changes to untranslatable fields affect are configured to affect only the revision's default translation, synchronized field properties can be changed only when editing the default translation. This may lead to temporarily desynchronized values, when saving a pending revision for the default translation that changes a synchronized property. These are actually synchronized when saving changes to the default translation as a new default revision.

@internal

Hierarchy

Expanded class hierarchy of ContentTranslationSynchronizedFieldsConstraintValidator

See also

\Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint

\Drupal\Core\Entity\Plugin\Validation\Constraint\EntityUntranslatableFieldsConstraintValidator

File

core/modules/content_translation/src/Plugin/Validation/Constraint/ContentTranslationSynchronizedFieldsConstraintValidator.php, line 35

Namespace

Drupal\content_translation\Plugin\Validation\Constraint
View source
class ContentTranslationSynchronizedFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {

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

  /**
   * The content translation manager.
   *
   * @var \Drupal\content_translation\ContentTranslationManagerInterface
   */
  protected $contentTranslationManager;

  /**
   * The field translation synchronizer.
   *
   * @var \Drupal\content_translation\FieldTranslationSynchronizerInterface
   */
  protected $synchronizer;

  /**
   * ContentTranslationSynchronizedFieldsConstraintValidator constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
   *   The content translation manager.
   * @param \Drupal\content_translation\FieldTranslationSynchronizerInterface $synchronizer
   *   The field translation synchronizer.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, ContentTranslationManagerInterface $content_translation_manager, FieldTranslationSynchronizerInterface $synchronizer) {
    $this->entityTypeManager = $entity_type_manager;
    $this->contentTranslationManager = $content_translation_manager;
    $this->synchronizer = $synchronizer;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('entity_type.manager'), $container
      ->get('content_translation.manager'), $container
      ->get('content_translation.synchronizer'));
  }

  /**
   * {@inheritdoc}
   */
  public function validate($value, Constraint $constraint) {

    /** @var \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint $constraint */

    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $value;
    if ($entity
      ->isNew() || !$entity
      ->getEntityType()
      ->isRevisionable()) {
      return;
    }

    // When changes to untranslatable fields are configured to affect all
    // revision translations, we always allow changes in default revisions.
    if ($entity
      ->isDefaultRevision() && !$entity
      ->isDefaultTranslationAffectedOnly()) {
      return;
    }
    $entity_type_id = $entity
      ->getEntityTypeId();
    if (!$this->contentTranslationManager
      ->isEnabled($entity_type_id, $entity
      ->bundle())) {
      return;
    }
    $synchronized_properties = $this
      ->getSynchronizedPropertiesByField($entity
      ->getFieldDefinitions());
    if (!$synchronized_properties) {
      return;
    }

    /** @var \Drupal\Core\Entity\ContentEntityInterface $original */
    $original = $this
      ->getOriginalEntity($entity);
    $original_translation = $this
      ->getOriginalTranslation($entity, $original);
    if ($this
      ->hasSynchronizedPropertyChanges($entity, $original_translation, $synchronized_properties)) {
      if ($entity
        ->isDefaultTranslationAffectedOnly()) {
        foreach ($entity
          ->getTranslationLanguages(FALSE) as $langcode => $language) {
          if ($entity
            ->getTranslation($langcode)
            ->hasTranslationChanges()) {
            $this->context
              ->addViolation($constraint->defaultTranslationMessage);
            break;
          }
        }
      }
      else {
        $this->context
          ->addViolation($constraint->defaultRevisionMessage);
      }
    }
  }

  /**
   * Checks whether any synchronized property has changes.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity being validated.
   * @param \Drupal\Core\Entity\ContentEntityInterface $original
   *   The original unchanged entity.
   * @param string[][] $synchronized_properties
   *   An associative array of arrays of synchronized field properties keyed by
   *   field name.
   *
   * @return bool
   *   TRUE if changes in synchronized properties were detected, FALSE
   *   otherwise.
   */
  protected function hasSynchronizedPropertyChanges(ContentEntityInterface $entity, ContentEntityInterface $original, array $synchronized_properties) {
    foreach ($synchronized_properties as $field_name => $properties) {
      foreach ($properties as $property) {
        $items = $entity
          ->get($field_name)
          ->getValue();
        $original_items = $original
          ->get($field_name)
          ->getValue();
        if (count($items) !== count($original_items)) {
          return TRUE;
        }
        foreach ($items as $delta => $item) {

          // @todo This loose comparison is not fully reliable. Revisit this
          //   after https://www.drupal.org/project/drupal/issues/2941092.
          if ($items[$delta][$property] != $original_items[$delta][$property]) {
            return TRUE;
          }
        }
      }
    }
    return FALSE;
  }

  /**
   * Returns the original unchanged entity to be used to detect changes.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity being changed.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface
   *   The unchanged entity.
   */
  protected function getOriginalEntity(ContentEntityInterface $entity) {
    if (!isset($entity->original)) {
      $storage = $this->entityTypeManager
        ->getStorage($entity
        ->getEntityTypeId());
      $original = $entity
        ->isDefaultRevision() ? $storage
        ->loadUnchanged($entity
        ->id()) : $storage
        ->loadRevision($entity
        ->getLoadedRevisionId());
    }
    else {
      $original = $entity->original;
    }
    return $original;
  }

  /**
   * Returns the original translation.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The entity being validated.
   * @param \Drupal\Core\Entity\ContentEntityInterface $original
   *   The original entity.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface
   *   The original entity translation object.
   */
  protected function getOriginalTranslation(ContentEntityInterface $entity, ContentEntityInterface $original) {

    // If the language of the default translation is changing, the original
    // translation will be the same as the original entity, but they won't
    // necessarily have the same langcode.
    if ($entity
      ->isDefaultTranslation() && $original
      ->language()
      ->getId() !== $entity
      ->language()
      ->getId()) {
      return $original;
    }
    $langcode = $entity
      ->language()
      ->getId();
    if ($original
      ->hasTranslation($langcode)) {
      $original_langcode = $langcode;
    }
    else {
      $metadata = $this->contentTranslationManager
        ->getTranslationMetadata($entity);
      $original_langcode = $metadata
        ->getSource();
    }
    return $original
      ->getTranslation($original_langcode);
  }

  /**
   * Returns the synchronized properties for every specified field.
   *
   * @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions
   *   An array of field definitions.
   *
   * @return string[][]
   *   An associative array of arrays of field property names keyed by field
   *   name.
   */
  public function getSynchronizedPropertiesByField(array $field_definitions) {
    $synchronizer = $this->synchronizer;
    $synchronized_properties = array_filter(array_map(function (FieldDefinitionInterface $field_definition) use ($synchronizer) {
      return $synchronizer
        ->getFieldSynchronizedProperties($field_definition);
    }, $field_definitions));
    return $synchronized_properties;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ContentTranslationSynchronizedFieldsConstraintValidator::$contentTranslationManager protected property The content translation manager.
ContentTranslationSynchronizedFieldsConstraintValidator::$entityTypeManager protected property The entity type manager.
ContentTranslationSynchronizedFieldsConstraintValidator::$synchronizer protected property The field translation synchronizer.
ContentTranslationSynchronizedFieldsConstraintValidator::create public static function Instantiates a new instance of this class. Overrides ContainerInjectionInterface::create
ContentTranslationSynchronizedFieldsConstraintValidator::getOriginalEntity protected function Returns the original unchanged entity to be used to detect changes.
ContentTranslationSynchronizedFieldsConstraintValidator::getOriginalTranslation protected function Returns the original translation.
ContentTranslationSynchronizedFieldsConstraintValidator::getSynchronizedPropertiesByField public function Returns the synchronized properties for every specified field.
ContentTranslationSynchronizedFieldsConstraintValidator::hasSynchronizedPropertyChanges protected function Checks whether any synchronized property has changes.
ContentTranslationSynchronizedFieldsConstraintValidator::validate public function
ContentTranslationSynchronizedFieldsConstraintValidator::__construct public function ContentTranslationSynchronizedFieldsConstraintValidator constructor.