3 namespace Drupal\content_translation\Plugin\Validation\Constraint;
5 use Drupal\content_translation\ContentTranslationManagerInterface;
6 use Drupal\content_translation\FieldTranslationSynchronizerInterface;
7 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
8 use Drupal\Core\Entity\ContentEntityInterface;
9 use Drupal\Core\Entity\EntityTypeManagerInterface;
10 use Drupal\Core\Field\FieldDefinitionInterface;
11 use Symfony\Component\DependencyInjection\ContainerInterface;
12 use Symfony\Component\Validator\Constraint;
13 use Symfony\Component\Validator\ConstraintValidator;
16 * Checks that synchronized fields are handled correctly in pending revisions.
18 * As for untranslatable fields, two modes are supported:
19 * - When changes to untranslatable fields are configured to affect all revision
20 * translations, synchronized field properties can be changed only in default
22 * - When changes to untranslatable fields affect are configured to affect only
23 * the revision's default translation, synchronized field properties can be
24 * changed only when editing the default translation. This may lead to
25 * temporarily desynchronized values, when saving a pending revision for the
26 * default translation that changes a synchronized property. These are
27 * actually synchronized when saving changes to the default translation as a
28 * new default revision.
30 * @see \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint
31 * @see \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityUntranslatableFieldsConstraintValidator
35 class ContentTranslationSynchronizedFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
38 * The entity type manager.
40 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
42 protected $entityTypeManager;
45 * The content translation manager.
47 * @var \Drupal\content_translation\ContentTranslationManagerInterface
49 protected $contentTranslationManager;
52 * The field translation synchronizer.
54 * @var \Drupal\content_translation\FieldTranslationSynchronizerInterface
56 protected $synchronizer;
59 * ContentTranslationSynchronizedFieldsConstraintValidator constructor.
61 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
62 * The entity type manager.
63 * @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
64 * The content translation manager.
65 * @param \Drupal\content_translation\FieldTranslationSynchronizerInterface $synchronizer
66 * The field translation synchronizer.
68 public function __construct(EntityTypeManagerInterface $entity_type_manager, ContentTranslationManagerInterface $content_translation_manager, FieldTranslationSynchronizerInterface $synchronizer) {
69 $this->entityTypeManager = $entity_type_manager;
70 $this->contentTranslationManager = $content_translation_manager;
71 $this->synchronizer = $synchronizer;
77 public static function create(ContainerInterface $container) {
79 $container->get('entity_type.manager'),
80 $container->get('content_translation.manager'),
81 $container->get('content_translation.synchronizer')
88 public function validate($value, Constraint $constraint) {
89 /** @var \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint $constraint */
90 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
92 if ($entity->isNew() || !$entity->getEntityType()->isRevisionable()) {
95 // When changes to untranslatable fields are configured to affect all
96 // revision translations, we always allow changes in default revisions.
97 if ($entity->isDefaultRevision() && !$entity->isDefaultTranslationAffectedOnly()) {
100 $entity_type_id = $entity->getEntityTypeId();
101 if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) {
104 $synchronized_properties = $this->getSynchronizedPropertiesByField($entity->getFieldDefinitions());
105 if (!$synchronized_properties) {
109 /** @var \Drupal\Core\Entity\ContentEntityInterface $original */
110 $original = $this->getOriginalEntity($entity);
111 $original_translation = $this->getOriginalTranslation($entity, $original);
112 if ($this->hasSynchronizedPropertyChanges($entity, $original_translation, $synchronized_properties)) {
113 if ($entity->isDefaultTranslationAffectedOnly()) {
114 foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
115 if ($entity->getTranslation($langcode)->hasTranslationChanges()) {
116 $this->context->addViolation($constraint->defaultTranslationMessage);
122 $this->context->addViolation($constraint->defaultRevisionMessage);
128 * Checks whether any synchronized property has changes.
130 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
131 * The entity being validated.
132 * @param \Drupal\Core\Entity\ContentEntityInterface $original
133 * The original unchanged entity.
134 * @param string[][] $synchronized_properties
135 * An associative array of arrays of synchronized field properties keyed by
139 * TRUE if changes in synchronized properties were detected, FALSE
142 protected function hasSynchronizedPropertyChanges(ContentEntityInterface $entity, ContentEntityInterface $original, array $synchronized_properties) {
143 foreach ($synchronized_properties as $field_name => $properties) {
144 foreach ($properties as $property) {
145 $items = $entity->get($field_name)->getValue();
146 $original_items = $original->get($field_name)->getValue();
147 if (count($items) !== count($original_items)) {
150 foreach ($items as $delta => $item) {
151 // @todo This loose comparison is not fully reliable. Revisit this
152 // after https://www.drupal.org/project/drupal/issues/2941092.
153 if ($items[$delta][$property] != $original_items[$delta][$property]) {
163 * Returns the original unchanged entity to be used to detect changes.
165 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
166 * The entity being changed.
168 * @return \Drupal\Core\Entity\ContentEntityInterface
169 * The unchanged entity.
171 protected function getOriginalEntity(ContentEntityInterface $entity) {
172 if (!isset($entity->original)) {
173 $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
174 $original = $entity->isDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId());
177 $original = $entity->original;
183 * Returns the original translation.
185 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
186 * The entity being validated.
187 * @param \Drupal\Core\Entity\ContentEntityInterface $original
188 * The original entity.
190 * @return \Drupal\Core\Entity\ContentEntityInterface
191 * The original entity translation object.
193 protected function getOriginalTranslation(ContentEntityInterface $entity, ContentEntityInterface $original) {
194 $langcode = $entity->language()->getId();
195 if ($original->hasTranslation($langcode)) {
196 $original_langcode = $langcode;
199 $metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
200 $original_langcode = $metadata->getSource();
202 return $original->getTranslation($original_langcode);
206 * Returns the synchronized properties for every specified field.
208 * @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions
209 * An array of field definitions.
212 * An associative array of arrays of field property names keyed by field
215 public function getSynchronizedPropertiesByField(array $field_definitions) {
216 $synchronizer = $this->synchronizer;
217 $synchronized_properties = array_filter(array_map(
218 function (FieldDefinitionInterface $field_definition) use ($synchronizer) {
219 return $synchronizer->getFieldSynchronizedProperties($field_definition);
223 return $synchronized_properties;