Problem/Motivation

Create an entity and save it. Now call setNewRevision(TRUE) on the entity and before saving call setNewRevision(FALSE). The first call to setNewRevision removes the revision_id so trying to stop the creation of a new revision is not possible by calling setNewRevision a second time. Saving the entity now results in a new revision.

Proposed resolution

Make the function setNewRevision remember the changes it made and revert them if called again with TRUE.

Remaining tasks

review.

User interface changes

none

API changes

none

Data model changes

none

Comments

hchonov’s picture

StatusFileSize
new3.8 KB

and here the test and the fix together.

The last submitted patch, setNewRevision-test_only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 1: setNewRevision-test_and_fix.patch, failed testing.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

wim leers’s picture

This should be triaged by entity system maintainers.

hchonov’s picture

This could be solved later by #2620980: Add static and persistent caching to ContentEntityStorageBase::loadRevision() which will introduce a new function ::getOriginalRevisionId, so calling setNewRevision(FALSE) might reset the revision id, if it is not set.

berdir’s picture

Title: setNewRevision is faulty » [PP-1] setNewRevision is faulty
Status: Needs work » Postponed

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

tstoeckler’s picture

Title: [PP-1] setNewRevision is faulty » ContentEntityBase::setNewRevision(FALSE) is broken if ::setNewRevision(TRUE) was called previously
Status: Postponed » Active
Issue tags: -Needs triage for core major current state +Triaged core major, +DevDaysSeville

Discussed this today with @Berdir, @amateescu, @plach and @xjm and agreed that this is a major issue.

Currently if you in the presave-phase of saving an entity decide to not create a new revision for the entity and, thus, call setNewRevision(FALSE) if some code (such as a form) has deided before that a revision should be created. There is no workaround for this, either. A use-case of this is disabling creating a new revision of an entity when nothing actually changed, even if revisioning is enabled by default otherwise. This is currently not possible due to this bug.

However, with #2620980: Add static and persistent caching to ContentEntityStorageBase::loadRevision() in, this should be fairly easy to fix now.

Also changing the title to be a bit more descriptive.

hchonov’s picture

tstoeckler’s picture

Yes, you're right, thanks ;-)

tstoeckler’s picture

Actually this is not as easy as I originally thought because of the setting of ->setRevisionTranslationAffected(). I think we have a pre-existing bug related to that, but I'm still in the process of reading the entirety of #2453153: Node revisions cannot be reverted per translation and also playing around with this locally. But it's going to be a bit tricky either way, I think.

tstoeckler’s picture

Opened #2889530: revision_translation_affected is not re-calculated when re-saving a revision for #14. Not sure if this should be blocked on that. If we just fix ignore the revision_translation_affected flag for the scope of this patch, then doing $entity->setNewRevision(TRUE)->setNewRevision(FALSE) will lead to the revision_translation_affected flag being (re-)calculated incorrectly. Calculating it correctly, though, requires #2889530: revision_translation_affected is not re-calculated when re-saving a revision. So what we could also do (which is what I think I prefer, but not sure) is simply store the current values of the revision_translation_affected flag when someone calls ->setNewRevision(TRUE)in a member variable this is solely used to re-instate those values 2889530when someone calls ->setNewRevision(FALSE) later in the same request (and ignored/thrown away otherwise).

wim leers’s picture

What's next here?

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

amateescu’s picture

Status: Active » Needs review
StatusFileSize
new1.84 KB

So what we could also do (which is what I think I prefer, but not sure) is simply store the current values of the revision_translation_affected flag when someone calls ->setNewRevision(TRUE)in a member variable this is solely used to re-instate those values when someone calls ->setNewRevision(FALSE) later in the same request (and ignored/thrown away otherwise).

An alternative to this could be to set the translation affected flag to NULL for a new revision in the storage itself, in which case the code to handle $entity->setNewRevision(TRUE); $entity->setNewRevision(FALSE); becomes much simpler :)

Status: Needs review » Needs work

The last submitted patch, 19: 2544790-19.patch, failed testing. View results

hchonov’s picture

An alternative to this could be to set the translation affected flag to NULL for a new revision in the storage itself

This would not work, at least not as it is in the current patch, because removing the following line from ContentEntityBase::setNewRevision()

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -269,15 +269,9 @@ public function setNewRevision($value = TRUE) {
    -          $this->getTranslation($langcode)->setRevisionTranslationAffected(NULL);
    
  2. leads to the first if condition in ContentEntityStorageBase:: populateAffectedRevisionTranslations() evaluating to FALSE instead to TRUE anymore

  3. +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
    @@ -592,6 +592,11 @@ protected function populateAffectedRevisionTranslations(ContentEntityInterface $
             if (!isset($affected) && $translation->hasTranslationChanges()) {
               $translation->setRevisionTranslationAffected(TRUE);
    

in which case the storage will not anymore set the revision translation affected flag to TRUE for an entity that is being saved with a new revision and has translation changes compared to the original unchanged entity.

amateescu’s picture

@hchonov, tbh this is the first time I've looked at that code and I was just assuming things "blindly", so I'm glad that at least we have test coverage for it :) Since you have some actual knowledge in that area, do you think the general idea of moving that code to the storage is possible somehow?

hchonov’s picture

@amateescu, as the revision translation affected flag is handled by the entity storage I think that we could probably set back the original values of the flag based on $entity->original in case the entity is being saved without a new revision - this should cover the case $entity->setNewRevision(TRUE); $entity->setNewRevision(FALSE); The other piece of code in CEB::setNewRevision() probably could be moved as well to the storage in a condition for new revision.

However I think we have a problem if we save the entity without creating a new revision, as in this case we would have problems what will be shown on the revision page, because if we do not create a new revision we have to use as $original the entity revision based on which the current revision has been created instead the current one - the current one makes sense as $original only in case we are saving a new revision. The idea of the revision translation affected flag is to indicate changes between revisions. To overcome this issue the current approach in the storage will recompute the affected flag only if it has been set to NULL by e.g. CEB::setNewRevision(), this however does not cover the following case :

$entity->setTitle('a')->setNewRevision()->save();    // rev ID 2
$entity->setTitle('b')->setNewRevision()->save();   // rev ID 3
$entity->setTitle('a')->save();                                 // rev ID 3

After those steps the revision page should only list the revision ID 2, but it will still be showing the revision ID 3 even if revision 3 doesn't contain any changes compared to revision 2. We could fix this but the requirement for this is #2833084: $entity->original doesn't adequately address intentions when saving a revision.

I suggest the following before we introduce a way of retrieving the parent revision:

  protected function populateAffectedRevisionTranslations(ContentEntityInterface $entity) {
    if ($this->entityType->isTranslatable() && $this->entityType->isRevisionable()) {
      $languages = $entity->getTranslationLanguages();
      foreach ($languages as $langcode => $language) {
        $translation = $entity->getTranslation($langcode);
        // If the entity is being saved in a new revision we have to recompute
        // the revision translation affected state.
        if ($entity->isNewRevision()) {
          $new_affected = $translation->hasTranslationChanges() ?: NULL;
          $translation->setRevisionTranslationAffected($new_affected);
        }
        // Until we have a way of retrieving the parent revision we have to put
        // back the original state of the revision translation affected flag in
        // order to cover the case where the entity has been flagged to be saved
        // with a new revision and afterwards flagged to be saved without a new
        // revision e.g.
        // $entity->setNewRevision(TRUE); $entity->setNewRevision(FALSE);
        else {
          $orignal_value = $entity->original->getTranslation($langcode)->isRevisionTranslationAffected();
          $translation->setRevisionTranslationAffected($orignal_value);
        }
      }
    }

I suggest the following after we introduce a way of retrieving the parent revision:

  protected function populateAffectedRevisionTranslations(ContentEntityInterface $entity) {
    if ($this->entityType->isTranslatable() && $this->entityType->isRevisionable()) {
      $languages = $entity->getTranslationLanguages();
      foreach ($languages as $langcode => $language) {
        $translation = $entity->getTranslation($langcode);
        // If the entity is being saved in a new revision we have to recompute
        // the revision translation affected state.
        if ($entity->isNewRevision()) {
          $new_affected = $translation->hasTranslationChanges() ?: NULL;
          $translation->setRevisionTranslationAffected($new_affected);
        }
        // If the entity is saved in the same revision we have to recompute the
        // revision translation affected state based on the revision the current
        // revision derived from.
        else {
          // Recompute the revision translation affected state based on the
         // parent revision, not on the current revision as the flag indicates
         // changes between revisions!
         $original = $entity->original;
         $parent_revision = $this->getParentRevision($entity);
         $entity->original = $parent_revision;

         $new_affected = $translation->hasTranslationChanges() ?: NULL;
         $translation->setRevisionTranslationAffected($new_affected);

         $entity->original = $original;
        }
      }
    }
  }
hchonov’s picture

Version: 8.4.x-dev » 8.5.x-dev
StatusFileSize
new5.88 KB

Uploading a patch that fixes the problem that a new revision is created and also moves the logic for the revision translation affected flag from CEB::setNewRevision() to ContentEntityStorageBase::populateAffectedRevisionTranslations().

The only uncovered case that remains is saving an entity without new revision where the revision translation affected flag of the unchanged translation is TRUE, but this could not be solved here and until we have a way of retrieving the parent revision.

hchonov’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 24: 2544790-24.patch, failed testing. View results

wim leers’s picture

Go @hchonov & @amateescu for getting this going again! 🎉👌

amateescu’s picture

I don't think we need to provide any safety net for the case outlined in #23. In the example you gave, the third call not creating a new revision when a field value was actually changed is a bug in the code that decided to not save a new revision, so not really core's responsibility :)

If we agree on that, we just need to remove the @todo from the patch. Other than that, the code looks great and well documented!

hchonov’s picture

Status: Needs work » Needs review
StatusFileSize
new6.18 KB
new4.7 KB

If we agree on that, we just need to remove the @todo from the patch. Other than that, the code looks great and well documented!

I've removed the @todo, but I've left the note why we could not do it :).

In this patch I had to slightly change the logic to make the existing test coverage in Drupal\KernelTests\Core\Entity\ContentEntityChangedTest pass.

hchonov’s picture

StatusFileSize
new6.45 KB
new535 bytes
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -269,15 +269,11 @@ public function setNewRevision($value = TRUE) {
+    elseif (!$value && $this->newRevision) {
+      // If ::setNewRevision(FALSE) is called after ::setNewRevision(TRUE) we
+      // have to recover the loaded revision ID on the field.
+      $this->set($this->getEntityType()->getKey('revision'), $this->getLoadedRevisionId());

Because of the new logic in ::setNewRevision() where we recover the revision ID we have to reset the new revision flag in post save in order to cover the use case (existing test) where an entity is saved in hook_entity_insert() - entity_test_entity_insert() and ::setNewRevision(FALSE) is called on it.

This fixes the failures in Drupal\KernelTests\Core\Entity\EntityLoadedRevisionTest

hchonov’s picture

I think I would need some support for the failing test \Drupal\Tests\content_moderation\Functional\ModerationFormTest::testContentTranslationNodeForm().

The reason why the test fails is that in \Drupal\content_moderation\EntityTypeInfo::formAlter() the following call returns NULL :
$translation = $this->moderationInfo->getAffectedRevisionTranslation($latest_revision);
because the node entity does not have any affected translation in the latest revision.

Looking at the test code which should be responsible for creating the latest revision I am confused why the test initially expected a new revision with the revision translation affected flag set to TRUE:

    // Translate it, without updating data (revision 2).
    $this->drupalGet($translate_path);
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
    $this->assertSession()->optionExists('moderation_state[0][state]', 'archived');
    $this->drupalPostForm(NULL, [
      'moderation_state[0][state]' => 'draft',
    ], t('Save (this translation)'));

    // Add another draft for the translation (revision 3).
    $this->drupalGet($edit_path, ['language' => $french]);
    $this->assertSession()->optionExists('moderation_state[0][state]', 'draft');
    $this->assertSession()->optionExists('moderation_state[0][state]', 'published');
    $this->assertSession()->optionNotExists('moderation_state[0][state]', 'archived');
    $this->drupalPostForm(NULL, [
      'moderation_state[0][state]' => 'draft',
    ], t('Save (this translation)'));

    // Editing the original translation should not be possible.
    $this->drupalGet($edit_path);

What I see here is that the node entity is being translated to french and the value of 'moderation_state[0][state]' is set to 'draft'. Afterwards the node edit page is retrieved for the french translation and the form is saved with the value of 'moderation_state[0][state]' set to 'draft' - this means the entity is saved with no difference in comparison to the previous revision. Without understanding the big picture underneath for me it looks like this is how it should behave.

@amateescu, could you please help me find out if the current patch is breaking something or we just found a bug in the test or CM?

The last submitted patch, 29: 2544790-29.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 30: 2544790-30.patch, failed testing. View results

hchonov’s picture

Status: Needs work » Needs review
StatusFileSize
new6.7 KB
new1.14 KB

This will hopefully fix all the failing tests but the ModerationFormTest.

amateescu’s picture

@hchonov, it can very well be a bug in CM's test coverage. I'll take a look in a few hours when I get to my laptop :)

Status: Needs review » Needs work

The last submitted patch, 34: 2544790-34.patch, failed testing. View results

hchonov’s picture

Status: Needs work » Needs review
StatusFileSize
new8.27 KB
new4.07 KB

@amateescu, the problem was neither CM nor it's test coverage. The problem was in the new approach. When we move the whole logic to the storage we still have one problem - the flag could be set outside the storage as well and in the storage we have to find out if the flag has been enforced from outside and in such case skip recomputing it. For this we use the original entity and there is no problem of detecting a change to the flag, but an enforced value is not that easy to determine - e.g. the flag is already TRUE and before saving the entity with a new revision without translation changes the flag is enforced to TRUE. Here I make use of the storage, where the value of the flag is the corresponding integer value of TRUE when the entity is loaded. This allows for strict comparison if ::setRevisionTranslationAffected() is called only with boolean values outside the storage. To cover the use case where the same entity object is saved multiple times we have to call ::setRevisionTranslationAffected() with the corresponding integer value of TRUE inside the storage so that on subsequent calls we'll be still able to perform strict comparison.

I am not sure if this is an acceptable solution, but I couldn't think of anything else that is not breaking the existing test coverage.

amateescu’s picture

Status: Needs review » Needs work

Ugh, that's.. very ugly :/ I also tried various things locally with no success, so I think what we realized at this point is that doing all the work in the storage handler is not really possible because of the reasons you mentioned in #37 (being able to set the flag from both outside and inside the storage), so let's go back to the original proposal: keep the previous values in a protected variable and restore them whenever setNewRevision(FALSE) is called after a setNewRevision(TRUE).

hchonov’s picture

I completely agree with you that it is very very ugly solution :).

If we store the original values in a protected variable on the entity object, then there is one question that arises - what happens if between setNewRevision(TRUE) and setNewRevision(FALSE) the revision translation affected flag is changed? Could we just like this revert the value? I am not really sure what the right answer is.

I think we still should find a way to decouple the method setNewRevision() from any knowledge about the revision translation affected flag.

Probably we could add a second argument to the method setRevisionTranslationAffected() - $enforced, which by default is TRUE, but the storage will call the method with the argument set to FALSE. This has as a consequence that method calls outside the storage will by default enforce a value. If the value is enforced then it will be kept in a protected entity object property. Then we either need a new method isRevisionTranslationAffectedEnforced() or a second argument to isRevisionTranslationAffected() - $is_enforced, which by default is FALSE and returns the field value, but when called with TRUE from inside the storage returns whether the value has been enforced from outside. Sure, it has the drawback that the method might be overwritten in a class extending from ContentEntityBase in which case it will not work.
I haven't completely thought this through, but what do you think about something like this @amateescu? I mean not necessary this suggestion, but based on it you might probably (hopefully) have a new better idea? :)

hchonov’s picture

@amateescu, I have another proposal -

let's introduce a new field type for the revision translation affected flag, which extends from the BooleanItem, but has one second property "enforced" without schema for that property. ContnentEntityBase::setRevisionTranslationAffected() will always set the enforced property on the field to TRUE and in the storage we'll set it to FALSE immediately after the call to ContnentEntityBase::setRevisionTranslationAffected(). This will allow us to cleanly detect if the flag has been modified from within or from outside the entity storage.

Why is this better than a protected variable? Because no matter what crazy stuff we do with the entity, both properties will be returned when we call $entity->revision_translation_affected->getValue() and we don't have to take care of proper cloning of the protected variable, which will be needed as the variable should be shared among all entity translation objects, because other translation objects might be destroyed by calling $entity->clearTranslationCache(). I think this should also cover use cases like entity_create($entity_type, $entity->toArray());. With this approach the knowledge wherefrom the value has been set will be always accompanying the value.

amateescu’s picture

@hchonov, last night I re-read #2453153: Node revisions cannot be reverted per translation entirely, like @tstoeckler did in #14/#15 and in that issue there was at some point a proposal to have a field field type with this kind of logic, but it was rejected.

I was also thinking of something along the lines of "enforced" values, but I didn't elaborate it as much as you did in #39 so I think that proposal has the best chances of being accepted by other entity maintainers :) In short, let's go with #39 and see how it looks like in a patch. :)

hchonov’s picture

Status: Needs work » Needs review
StatusFileSize
new12.52 KB
new8.67 KB

Let's see if the tests are happy with this slightly different approach :)

amateescu’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -334,12 +339,27 @@ public function setRevisionTranslationAffected($affected) {
+  public function isRevisionTranslationAffectedEnforced($new_value = NULL) {

I think we're trying to move away from the pattern where a getter is also a setter if you give it a parameter.

As a general observation on the code (without stepping through each line in an IDE).. it looks like we're doing a bit too much, are you sure you're not trying to also fix #2889530: revision_translation_affected is not re-calculated when re-saving a revision in this patch?

hchonov’s picture

StatusFileSize
new10.22 KB
new8.04 KB

@amateescu, you are right, by the way I've changed the storage I was actually solving as well #2889530: revision_translation_affected is not re-calculated when re-saving a revision. I've reduced the changes to the minimum required for the current issue - at least I hope so, the tests have the final word :).

I've introduce a setter for the enforcement.

amateescu’s picture

Super nice! The latest patch looks so much better and easier to understand :)

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -157,6 +157,15 @@
    +  protected $isRevisionTranslationAffectedEnforced = [];
    

    How about renaming this to enforceRevisionTranslationAffected to keep it similar with the existing enforceIsNew variable from the \Drupal\Core\Entity\Entity class?

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -269,15 +278,11 @@ public function setNewRevision($value = TRUE) {
    +      // have to recover the loaded revision ID on the field.
    

    I would remove the 'on the field' part at the end of the sentence, it doesn't say anything useful. Also, how about 'we have to restore' instead of 'we have to recover'?

  3. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -334,6 +339,25 @@ public function setRevisionTranslationAffected($affected) {
    +  public function setRevisionTranslationAffectedEnforced($enforced) {
    

    I would prefer your original proposal to have an $enforced = TRUE parameter for setRevisionTranslationAffected() instead. What do you think? It is a flag that should only be used by the storage, so outside calls won't care too much about the second argument..

    IMO, it is not such an important part of the public API to warrant its own method that will clobber an auto-completed list of methods in an IDE for example.

hchonov’s picture

1.& 2. I agree.

3. I like my original proposal better as well, but changing a method is BC break in case somebody already overrides the method in an entity class extending from ContentEntityBase. To prevent a BC break I think it would be better to introduce the new setter method and deprecate it. Then in D9 replace the method by adding the new argument $enforced = TRUE to setRevisionTranslationAffected(). Personally I cannot think of a reason for overriding the method in custom or contrib, but this is only my personal opinion. Should we first ask a committer if we're allowed to change the method or should we directly do it and then see what a committer thinks about this?

amateescu’s picture

Adding a new parameter with a default value to a method is not a BC break as far as I know, but sure, let's ask a release manager first :)

hchonov’s picture

According to https://www.drupal.org/core/d8-allowed-changes#definitions adding a new parameter to a method is a BC break:

A backward-compatibility break is a type of API change that requires changes in code implementing a public API. This can be anything from changing the parameters of a procedural function to renaming an interface or removing a hook.

Adding a parameter with a default value doesn't help us in case the method is overwritten, as then we cannot call the method with an argument different than the default one - we can, but it will not make a difference, as the overwritten method will not pass the argument to the parent method when calling it.

According to https://www.drupal.org/core/d8-bc-policy adding a new method to ContentEntityInterface is not a BC break:

Where there is a 1-1 relationship between a class and an interface, inherit from the class. Where a base class or trait is provided, use those. This way your class should inherit new methods from the class or trait when they're added.

amateescu’s picture

Status: Needs review » Needs work

Damn.. ok, new method it is then :/

NW for the other two points.

hchonov’s picture

Status: Needs work » Needs review
StatusFileSize
new7.97 KB
new10.39 KB

@amateescu, personally I prefer that we just add a new parameter to ::setRevisionTranslationAffected(). I just wanted that it is clear that such a change is a BC break. As we both prefer adding a new default parameter I am uploading two patches in two subsequent comments - one modifying the ::setRevisionTranslationAffected() and one introducing a new method instead. I would say, that we let a committer decide if a BC break at that place is worthy and allowed.

In this comment is the patch modifying the method ::setRevisionTranslationAffected() instead of introducing a new method for enforcing the revision translation affected flag value.

hchonov’s picture

StatusFileSize
new3.96 KB
new10.34 KB

In this comment is the patch introducing a new method for enforcing the revision translation affected flag value instead of modifying the method ::setRevisionTranslationAffected().

amateescu’s picture

+++ b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php
@@ -38,7 +38,7 @@ public function onPresave(ContentEntityInterface $entity, $default_revision, $pu
-      $entity->set('revision_translation_affected', TRUE);
+      $entity->setRevisionTranslationAffected(TRUE);

Sorry to point this out after all the discussions that we went through in this issue, but this a big problem. We can not have different logic apply when we set the value of a field through its "dedicated" setter vs. the "regular" entity API setter.

I'm not yet sure what's the best way to proceed here :(

hchonov’s picture

StatusFileSize
new4.53 KB
new13.11 KB

Sorry to point this out after all the discussions that we went through in this issue, but this a big problem. We can not have different logic apply when we set the value of a field through its "dedicated" setter vs. the "regular" entity API setter.

Really nice, that you've seen this and the change in content moderation was necessary :).

This means that we need the setter method for the enforcement instead of an additional parameter on the ::setRevisionTranslationAffected() method.

Now we have two ways of solving this -
1. Listen for changes on the revision translation affected field in ContentEntityBase::onChange(). This will not work if the field value is set with $notify = FALSE;, however I am not really sure that we should take care of the case where the field value is being changed without notifying the parent.
2. Introduce a special field type for the revision translation affected field.

2. will cover more use cases, but 1. is the lightweight solution providing enough coverage, therefore the new patch is implementing 1..

hchonov’s picture

amateescu’s picture

Status: Needs review » Needs work

Re #53: Awesome! I imagined some kind of onChange() usage but I didn't think it would be this easy :) The patch looks almost ready, just a few more things to fix:

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -156,6 +156,23 @@
    +
    +  /**
    +   * The revision translation affected entity key.
    

    Let's remove the extra space :)

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
    @@ -33,6 +33,12 @@ public function hasTranslationChanges();
    +   * by calling ::isRevisionTranslationAffectedEnforced(), which will be used by
    

    The method name has changed in the meantime but I would remove the "by calling ...()" part, it doesn't really matter how we enforce it.

  3. +++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
    @@ -51,6 +57,33 @@ public function setRevisionTranslationAffected($affected);
    +   * @internal
    +   *
    +   * @return bool
    +   *   TRUE if revision translation affected flag is enforced, FALSE otherwise.
    ...
    +   * @internal
    +   *
    +   * @return $this
    

    I think @internal needs to go after @return.

  4. +++ b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php
    @@ -38,7 +38,7 @@ public function onPresave(ContentEntityInterface $entity, $default_revision, $pu
    -      $entity->set('revision_translation_affected', TRUE);
    +      $entity->setRevisionTranslationAffected(TRUE);
    

    Let's revert this change to make sure that #53 actually fixed the problem :)

  5. +++ b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php
    --- a/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
    +++ b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
    

    This doesn't need to live in a browser test, we're only using API calls here. I would suggest to move this new test method to EntityRevisionTranslationTest.

  6. +++ b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
    @@ -159,4 +159,31 @@ public function testEntityRevisionParamConverter() {
    +   * Test the setNewRevision method.
    

    This can be just @covers setNewRevision.

  7. +++ b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
    @@ -159,4 +159,31 @@ public function testEntityRevisionParamConverter() {
    +      $entity = entity_create($entity_type,
    +        [
    ...
    +        ]
    +      );
    

    I think our coding standards say to open the array on the line above and close it on the last line.

  8. +++ b/core/modules/system/tests/src/Functional/Entity/EntityRevisionsTest.php
    @@ -159,4 +159,31 @@ public function testEntityRevisionParamConverter() {
    +      $this->assertEqual($entity->getRevisionId(), $entity_rev_id, 'A new entity revision was not created.');
    

    Let's use assertEquals() and put $entity_rev_id as the first param.

hchonov’s picture

Status: Needs work » Needs review
StatusFileSize
new5.57 KB
new12.46 KB

1. Done.
2. Done & changed the comment a bit. What do you think about the new version?
3. I think you are right, but I wasn't sure myself as looking at couple of places in core it is the other way around. Still I've changed it now :).
4. Done. Should we really open a new issue just for that change? :)
5. Done.
6. Done.
7. Done.
8. Done.

Thank you for the extended review, @amateescu!

amateescu’s picture

Status: Needs review » Reviewed & tested by the community

Re #56.2: That's much better.
Re #56.4: Nope, I just wanted to have both kind of calls in core (dedicated and regular setter), so reverting that change to CM is enough for me :)

I think this is finally ready!

catch’s picture

Status: Reviewed & tested by the community » Needs work

Quite minor issues with the comments, but this is a little bit hard to follow and I had to read some of the comments several times.

I started typing up nitpicks with method names, but just could not think of anything to replace them with.

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
    @@ -587,10 +587,17 @@ protected function populateAffectedRevisionTranslations(ContentEntityInterface $
    +        if (!isset($current_affected) || ($entity->isNewRevision()) && !$translation->isRevisionTranslationAffectedEnforced()) {
    +          // When setting the revision translation affected flag we have to
    +          // ensure that it is not being enforced. By default it will be
    +          // enforced automatically when being set, which allows us to determine
    

    'ensure' here sounds like we're checking if it's enforced or not. Something like 'When setting the revision translation affected flag we have to explicitly set it to not be enforced'?

  2. +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityRevisionTranslationTest.php
    @@ -157,4 +157,33 @@ public function testDefaultRevision() {
    +    // All revisable entity variations have to have the same results.
    

    We use revisionable everywhere else in core so should probably be consistent.

  3. +++ b/core/lib/Drupal/Core/Entity/ContentEntityInterface.php
    @@ -51,6 +57,33 @@ public function setRevisionTranslationAffected($affected);
    +   *   so that on entity save the entity storage will not recompute it.
    +   *   Otherwise the storage will recompute it. Note that this method call will
    +   *   not have any influence on the storage if the current value of the
    +   *   revision translation affected flag is NULL in which case the storage will
    +   *   always recompute it.
    +   *
    

    The second part of this comment is a bit confusing - why won't it have any effect? How do we end up in that situation?

hchonov’s picture

Status: Needs work » Needs review
StatusFileSize
new3.97 KB
new12.56 KB

@catch:
1. Done.
2. Done.
3. The value of NULL of the flag is being used to force the storage to recompute it. From the documentation of ContentEntityInterface::setRevisionTranslationAffected():

@param bool|null $affected
* The flag value. A NULL value can be specified to reset the current value
* and make sure a new value will be computed by the system.

I've changed the comment of ContentEntityInterface::setRevisionTranslationAffectedEnforced() a bit and added @see to ContentEntityInterface::setRevisionTranslationAffected().

amateescu’s picture

Status: Needs review » Reviewed & tested by the community

Yay for doc improvements :)

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.5.x, thanks!

wim leers’s picture

Status: Fixed » Reviewed & tested by the community

This commit was not yet actually pushed. 🤐 😛

  • catch committed 3bc8b66 on 8.5.x
    Issue #2544790 by hchonov, amateescu, tstoeckler: ContentEntityBase::...
catch’s picture

Status: Reviewed & tested by the community » Fixed

Now it is!

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.