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) { $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; } }