Problem/Motivation

This was uncovered in #2456257: [PP-1] Normalizers must set $entity->_restSubmittedFields for entity POSTing/PATCHing to work and hard-blocks that issue. It was split off to this issue because it's a significant new capability in Entity API that merits thorough reviews.

Proposed resolution

  1. Add EntityWithDirtyFieldsInterface (see "API changes" for details)
  2. Update ContentEntityBase to implement EntityWithDirtyFieldsInterface

Remaining tasks

  1. Patch that passes tests with EntityResource::patch() updated to leverage this, to prove it works. done, see #2456257-69: [PP-1] Normalizers must set $entity->_restSubmittedFields for entity POSTing/PATCHing to work and later.
  2. Thoroughly review the patch.
  3. Test coverage
  4. Review the patch MORE thoroughly.

User interface changes

None.

API changes

Add interface EntityWithDirtyFieldsInterface extends FieldableEntityInterface with a single method: getDirtyFields(). Why not add this to FieldableEntityInterface? Because doing so breaks BC.

Data model changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Wim Leers created an issue. See original summary.

alexpott’s picture

Wim Leers’s picture

Issue summary: View changes
Status: Active » Needs review
FileSize
6.63 KB
Munavijayalakshmi’s picture

+++ b/core/lib/Drupal/Core/Entity/EntityWithDirtyFieldsInterface.php
@@ -0,0 +1,27 @@
+ * This interface extends the general fieldable entity interface. So that that

Word 'That' is mentioned twice in a sequence.

Fixed and attached new patch.

Wim Leers’s picture

#4: That is utterly unhelpful. This patch does not need language nitpicking. It needs thorough reviews.

Worse, the change is *wrong*.

Please stop doing this. It's distracting. It's annoying.

dagmar’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -567,6 +581,13 @@ public function getFields($include_computed = TRUE) {
+    return array_intersect_key($this->getFields(), $this->dirtyFields[$this->activeLangcode]);

Just asking here. According to this https://www.drupal.org/node/2112677 computed fields section says: "This tells the Field API that this field is computed so that it doesn't look for it in the database."

I'm not sure if the docs are accurate, but, would be the same use getFields(FALSE) here?

Wim Leers’s picture

Excellent point: this definitely needs test coverage for how to deal with computed fields!

hchonov’s picture

Status: Needs review » Needs work
+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -157,6 +158,13 @@
+  protected $dirtyFields = [];

You are defining a new entity property, which should be unique per entity object, but when dealing with translations and entity cloning, which is really common in Drupal it will break and leak. To avoid this, you have to break the reference in CEB::__clone if not cloning because of initializing a translation and in CEB::initializeTranslation make the property shared between the entity translation objects.

For common mistakes regarding this you could take a look at

...

+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -701,6 +722,9 @@ protected function updateFieldLangcodes($langcode) {
   public function onChange($name) {
+    // Add the field to the dirty fields list for this translation.
+    $this->dirtyFields[$this->activeLangcode][$name] = TRUE;

You have to be aware of the current behavior of CEB::onChange, which will be fired even if you set the same value on a field e.g. $field->setValue($field->getValue()) and if you are ready to ignore this at the moment then probably it would be nice to document this in the code and open a follow up issue.

xjm’s picture

Removing credit for @Munavijayalakshmi. Thanks for your interest in contributing. Please make sure that your contributions are part of the current discussion on the issue. Read the issue carefully before posting new patches.

Wim Leers’s picture

+++ b/core/lib/Drupal/Core/Entity/EntityWithDirtyFieldsInterface.php
@@ -0,0 +1,27 @@
+ * @todo Merge this interface with FieldableEntityInterface before Drupal 9.0.

This should become a deprecation instead.

hchonov’s picture

Issue tags: +DevDaysSeville

We just had a discussion about the new interface on IRC and personally at the DrupalDevDays Seville with tstockler and berdir and they both agree that we add new methods to content entities by adding them to ContentEntityInterface and in D9 they should be moved to the according interface - in this case to FieldableEntityInterface. We've done this for an example with the getLoadedRevisionId method.

This means we do not need the new interface and instead we have to add the new method to ContentEntityInterface.

Wim Leers’s picture

I like that! A lot!

I'm wondering if we should document that on ContentEntityInterface and add @todos for this?

hchonov’s picture

I think that a @todo is fine.

tstoeckler’s picture

Issue tags: -DevDaysSeville

Here are some thoughts on the patch. I still want to make a list/table of "possible things you can do to entities" and the expected return value of getDirtyFields() in each case for the issue summary.

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -157,6 +158,13 @@
    +   * @var string[]
    

    Is this not "string[][]"? (is that allowed? Or should it be array[]?)

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -157,6 +158,13 @@
    +  protected $dirtyFields = [];
    

    @hchonov pointed this out in person yesterday when we talked about this: Since this should be "synchronized" between all translations of an entity (as it already keys field names per translation), we should convert it to a reference in ContentEntityBase::initializeTranslation() - and then also break that reference in ContentEntityBase::__clone(). See the handling of the other properties in those methods, for inspiration.

  3. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -254,7 +262,11 @@ protected function getLanguages() {
    +    // @todo TEST COVERAGE FOR CHANGES IN THIS HUNK
    +    if ($this->isDefaultTranslation()) {
    +      $this->newRevision = TRUE;
    +    }
    
    +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
    @@ -135,6 +135,8 @@ public function createTranslation(ContentEntityInterface $entity, $langcode, arr
    +    // @todo TEST COVERAGE FOR CHANGES IN THIS HUNK
    +    $translation->postCreate($this);
    

    Ahh these hunks belong together, now I understand what is going on.

    That's interesting. On the one hand this is the minimal fix to resolve this, on the other hand this could arguably be seen as a BC break. I.e. if I see the current code in ContentEntityBase::postCreate() and want to provide a default revision message for my entity I would override postCreate() and unconditionally (!) call $this->setRevisionMessage('Just created a new entity!') there. That would now then also be called for new translations, which would be unfortunate.

    The other (slightly more invasive) way would be to add a postCreateTranslation() method or similar, although that also seems non-ideal as then one could argue we would also need equivalent hooks for translation updating and deleting.

  4. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -390,6 +402,8 @@ public function preSaveRevision(EntityStorageInterface $storage, \stdClass $reco
    +    $this->dirtyFields[$this->activeLangcode] = [];
    

    As far as I can tell, this should clear the list of dirty fields in all languages, because we always save all translations of an entity.

  5. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -567,6 +581,13 @@ public function getFields($include_computed = TRUE) {
    +    return array_intersect_key($this->getFields(), $this->dirtyFields[$this->activeLangcode]);
    

    So this seems to account for the case that there is something in $this->dirtyFields that is not a field name? How can that happen? Or am I missing something?

  6. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -701,6 +722,9 @@ protected function updateFieldLangcodes($langcode) {
    +    $this->dirtyFields[$this->activeLangcode][$name] = TRUE;
    

    @hchonov brought up the interesting case of:

    $entity = $storage->load($entity_id);
    $entity->get('foo')->value === 'initial value';
    $entity->set('foo', 'some other value');
    $entity->set('foo', 'initial value');
    $entity->getDirtyFields();
    

    The question is whether 'foo' should be returned at that point. To fix this, we would somehow get the "original" item (not sure how, this could be tricky) and then compare with $items->equals($original_items). Since this would only result in "false negatives" (i.e. fields that are marked dirty that aren't actually dirty) and not "false positives" (i.e. fields that are not marked dirty even though they are) I personally think it would be fine to handle this case in a follow-up, but it would be nice to have confirmation that something like that would be accepted in a future patch. Not sure what your thoughts about this are.

  7. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -974,7 +998,9 @@ public function &__get($name) {
    +   * Always:
    +   * - uses default language
    +   * - notifies parent
    
    @@ -989,7 +1015,7 @@ public function __set($name, $value) {
    -        $this->fields[$name][$this->activeLangcode]->setValue($value);
    +        $this->fields[$name][$this->activeLangcode]->setValue($value, TRUE);
    

    This is the default value, so not sure why the setValue() call is changed.

  8. +++ b/core/lib/Drupal/Core/Entity/EntityWithDirtyFieldsInterface.php
    @@ -0,0 +1,27 @@
    +interface EntityWithDirtyFieldsInterface extends FieldableEntityInterface {
    ...
    +  public function getDirtyFields();
    

    Discussed this with @Berdir, @hchonov and a head-shaking @klausi and @berdir, @hchonov and I agreed that instead of this new interface this should be added to ContentEntityInterface ;-)

tstoeckler’s picture

Issue tags: +DevDaysSeville
tstoeckler’s picture

Sorry, just realized that I totally missed comment #8, so I duplicated some points that @hchonov brought up when we personally talked about it. Just in case anyone is (rightly) confused.

Wim Leers’s picture

See the related #2821077 — I left a comment at #2821077-11: PATCHing entities validates the entire entity, also unmodified fields, so unmodified fields can throw validation errors where I'm wondering whether ContentEntityBase::validate() should actually be relying on the tracking we do here to only validate fields that have been modified?

Wim Leers’s picture

Issue tags: +API-First Initiative

This is blocking lots of things in API-first.

What are the next steps here?

Wim Leers’s picture

Bump. #18 still holds true.

jonathanshaw’s picture

Reading the reviews in #8 and #14 it looks like the reviewers think the patch can be updated to take into account their concerns. (though there is slight uncertainty about #14.3).

@WimLeers Looking at it in ignorance from the outside, it looks like the ball is in your court. Can you say more about in what way the next step is not clear to you?

hchonov’s picture

About #14.3

The other (slightly more invasive) way would be to add a postCreateTranslation() method or similar, although that also seems non-ideal as then one could argue we would also need equivalent hooks for translation updating and deleting.

@tstoeckler, I would say it will be much better if we isolate this and introduce postCreateTranslation() instead of misusing the postCreate method which is intended to be called only after an entity is created. There is already a case in core where the concepts have been mixed-up causing some difficulties e.g. deleting an entity or only a translation will lead to calling the delete method on field items in both cases.

We already have hooks for creating and deleting translations - hook_entity_translation_insert and hook_entity_translation_delete :). For updating a translation the hook_entity_update is sufficient as there we get the entity in the translation in which the entity is being saved which over the FAPI is the entity in the language that has been edited.

Regarding entity creation we have:

  • the create method in the storage
  • the postCreate method on the entity
  • the hook hook_entity_create

Regarding entity translation creation we have:

  • the createTranslation method in the storage
  • the hook hook_entity_translation_create

In order to make the Entity API consistent we just have to introduce the postCreateTranslation() method on the entity.

hchonov’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -254,7 +262,11 @@ protected function getLanguages() {
+    // @todo TEST COVERAGE FOR CHANGES IN THIS HUNK
+    if ($this->isDefaultTranslation()) {
+      $this->newRevision = TRUE;
+    }
+    $this->dirtyFields[$this->activeLangcode] = [];

Actually I am still not really sure what this is about. The dirty fields have to be emptied on the new translation object? Why would they not be empty on creating a new translation for that new language code?

Wim Leers’s picture

IS updated to clarify which issues are blocked on this.

Wim Leers’s picture

#21:

In order to make the Entity API consistent we just have to introduce the postCreateTranslation() method on the entity.

This sounds reasonable.

@tstoeckler, do you want to proceed with that?

Wim Leers’s picture

Also, since this was split off from #2456257, and I just rerolled that patch. It failed in a new way. Quoting #2456257-89: [PP-1] Normalizers must set $entity->_restSubmittedFields for entity POSTing/PATCHing to work:

#2863336: Default revision flag doesn't propagate to all entity translation objects added a new test (\Drupal\KernelTests\Core\Entity\ContentEntityCloneTest::testEntityPropertiesModifications()), which is failing due to the changes in this issue. Perhaps this is the test coverage that @tstoeckler was asking for in #80.2?

(I'd forgotten for a moment that this issue must go in first, i.e. that we had split off the Entity Field API-related changes to this issue.)

So, reuploading this patch (and rebasing at the same time), to see if that new test also fails for this patch.

Status: Needs review » Needs work

The last submitted patch, 25: 2862574-25.patch, failed testing.

tstoeckler’s picture

Re #24: I don't think I have time to work on this patch myself at the moment. If I can allocate some time I will try to tackle the first part of #14 first to basically put this patch in a bit more context as that would be helpful in assessing the benefit and risk of this patch.

hchonov’s picture

@WimLeers, could you please explain #22?

Wim Leers’s picture

#25 failed as expected, great! :)

#27: okay!

#28: I wrote this >2 months ago, so I don't have a full explanation available. It was introduced in #2456257-74: [PP-1] Normalizers must set $entity->_restSubmittedFields for entity POSTing/PATCHing to work, based on feedback by @tstoeckler and my prior experience/attempts. The best I can muster:

  1. the $this->dirtyFields tracking needs to be reset for the given language after creating a new entity object (which can be either loading an entity at all, or loading a translation of the entity)
  2. but note that postCreate() is not yet being called in ContentEntityStorageBase::createTranslation(), so I added a ::postCreate() call there
  3. … but if the $this->newRevision = TRUE remains unconditional as it is in HEAD, then tests fail, because in HEAD postCreate() was only called for loading an entity the first time, now it's being called also when creating an entity translation object

This is why #21's proposal of adding postCreateTranslation() sounds sensible to me (without backing it up with facts, just from a superficial POV), because it allows us to not need to do things conditionally in postCreate().

Does that help?

catch’s picture

This is closely linked to the 'revision_translation_affected' functionality added by #2453153: Node revisions cannot be reverted per translation which I don't see mentioned here.

Wim Leers’s picture

Wim Leers’s picture

Issue tags: +Triaged core major

We just discussed this in an Entity/Field API API-First Initiative blocker call. The consensus is that this is major in and of itself, because it's an important gap in the Entity/Field API.

However, it's also a highly risky change. Because there are so many edge cases where this needs to work correctly: revisions, translations, unserializing, and it must work correctly for all field types. So this issue will need a lot of time.

@tstoeckler has recommended that the issues that need this functionality use \Drupal\Core\Field\FieldItemListInterface::equals() for now. That's also used by "foundational" Entity API functionality, including by \Drupal\Core\Entity\ContentEntityStorageBase::hasFieldValueChanged() to optimize SQL writes when saving entities, so if anything is broken in there, then it'd be critical. Therefore #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior should be solvable using that. And we'd just choose to postpone #2456257: [PP-1] Normalizers must set $entity->_restSubmittedFields for entity POSTing/PATCHing to work until this lands, and accept the code smell and negative consequences for contrib modules.

timmillwood’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -157,6 +158,13 @@
+  protected $dirtyFields = [];

I don't like the term "dirtyFields", what's dirty about them? I had to ask what it meant. How about "changedFields" or "updatedFields"?

Wim Leers’s picture

"dirty" is the term used in many many other libraries and frameworks.

timmillwood’s picture

Ok, maybe I just need "get off the island" and learn new terminology.

Wim Leers’s picture

Well, there's lots of questionable things shared across many cultures, this may very well be one of them :) I kinda agree with you, but yeah — do we want Drupalisms or follow the perhaps questionable common naming pattern?

Anyway, the name is something we can bikeshed when this is actually working :)

Berdir’s picture

Also, the problem with changed is that the fields might not have actually *changed*, maybe the same value was set again that was there before. So I think getChangedFields() would imply a functionality that doesn't actually exist.

(yes, onChange() is also not correct in that sense, but onDirty or onDirtyingField() is kinda... dirty ;))

plach’s picture

This is a "touching" issue ;)

tstoeckler’s picture

Also, the problem with changed is that the fields might not have actually *changed*, maybe the same value was set again that was there before.

Well that is not actually clear to me, to be honest. I had always thought that as part of tracking which fields are dirty we compare the to-be-set value with the current one and if they are equal, the field is not dirty? So basically my personal long-term plan was that once we have this API that at some point in the future that ContentEntityBase::hasTranslationChanges() could basically become super cheap, i.e.

public function hasTranslationChanges() {
  return !empty($this->dirtyFields[$this->getLangcode()]);
}

or something. And for that it would be important to track whether the field values actually changed. But maybe (apparently?) that's not what others are thinking? I would be interested to hear other ideas.

Berdir’s picture

At least that's not what the patch is doing as far as I see and the only way to do that through onChange() would be to change all the field implementations to only call onChange() if they actually detect a change, which would be a BC change in theory (not sure if it would actually break something) and require changes in many field types.

So yes, that would be an interesting thing to have but I'm not sure how we could actually make that work :)

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -990,7 +1016,7 @@ public function __set($name, $value) {
           if (isset($this->fields[$name][$this->activeLangcode])) {
    -        $this->fields[$name][$this->activeLangcode]->setValue($value);
    +        $this->fields[$name][$this->activeLangcode]->setValue($value, TRUE);
           }
           // If not, create one.
    

    $notify defaults to TRUE, why is this change necessary?

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php
    @@ -135,6 +135,8 @@ public function createTranslation(ContentEntityInterface $entity, $langcode, arr
         $this->invokeHook('translation_create', $translation);
    +    // @todo TEST COVERAGE FOR CHANGES IN THIS HUNK
    +    $translation->postCreate($this);
    

    Interesting, we've had quite some discussions already about what should and shouldn't be called per-translation.

    See #2577609: Add per-entity-translation pre/post save methods (and hooks?) so that preSave() and postSave() implementations don't need to iterate all translations.

    There are bugs by not doing it, but I fear that adding this might also introduce new/different bugs.

    I only found two postCreate() implementations in all my entities and they're both not translatable, so I have no specific example of what could change.

  3. +++ b/core/lib/Drupal/Core/Entity/EntityWithDirtyFieldsInterface.php
    @@ -0,0 +1,27 @@
    + * This interface extends the general fieldable entity interface. So that
    + * interface is not modified during the Drupal 8.x cycle.
    + *
    + * @todo Merge this interface with FieldableEntityInterface before Drupal 9.0.
    

    what we've done before is just add those methods to ContentEntityInterface with docs to move up in 9.x, but maybe only on issues that weren't committed yet because I can't find an example right now. (the argument is that fieldable interface is a generic interface that might have other implementations out there, while ContentEntityInterface is a 1:1 interface for ContentEntityBase)

Berdir’s picture

#1926714: Track changes for entity property changes and make available to hooks is also related, maybe we can close that as a duplicate now. It clearly is very old :)

hchonov’s picture

catch’s picture

#38 seems to be suggesting 'touched' fields as opposed to 'dirty' fields, seems like a good suggestion: (https://en.wikipedia.org/wiki/Touch_(Unix))

plach’s picture

@catch:

Yep, sorry for being cryptic :)

plach’s picture

Btw, I share @tstoeckler's perplexity about the usefulness of this patch as it is currently. If I understand it correctly a regular entity form submission would mark every field for which we have a widget as dirty/touched/younameit, regardless of it being actually modified. Is this going to be used only for REST? I guess it couldn't be used to build a proper conflict management solution supporting merging of non-conflicting changes. If so, we will need another issue and a similar API to detect actual field changes. However I suppose that once we have the latter, that can address also the use cases for which we are introducing the former.

That said, I'm wondering whether the only reason to keep the current approach is that it's way easier to implement and it will allow to unblock/fix the issues in the OP way more quickly.

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

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now 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.

Wim Leers’s picture

Today I ran into https://www.w3.org/TR/2011/WD-html5-20110525/the-input-element.html#conc...

Each input element has a boolean dirty value flag.

So this is the name used truly everywhere, even in the HTML spec!

(I hope this helps address #33.)

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.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.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

hchonov’s picture

Wim Leers’s picture

Issue summary: View changes

The issue summary was still saying this blocks #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior. This used to be true originally, but an alternative solution was found. The issue summary is now once again accurate.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

geek-merlin’s picture

Great API addition. And: I share the point made in #37 / #39 / #46 that the current approach (back-and-forth changes that result in no change are still marked dirty) does not seem useful.

Why not simply do a sophisticated version of

array_keys(DiffArray::diffAssocRecursive($entity->toArray(), $entity->original->toArray()))

?

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

johnwebdev’s picture

Status: Needs work » Needs review
FileSize
5.84 KB

This would still be a really useful API addition. Let's begin with a reroll.
(comments triage)

  • Remove the interface and add the method directly to ContentEntityInterface (#41.3, #14.8)
  • Naming (touched vs dirty)
  • Determine when a field is considered dirty, a initial change, current patch, or when the value is actually changed (#37, #39, #46)
  • Adress translations, serialisation, cloning (#32)

Status: Needs review » Needs work

The last submitted patch, 57: 2862574-57.patch, failed testing. View results

johnwebdev’s picture

Status: Needs work » Needs review
FileSize
6.93 KB
4.52 KB

This patch:

  • Adresses #41.3, #14.8
  • Adresses #14.2
  • Adds tests for cloning

I did a small comment triage in #57, that I intend to work more on later.

johnwebdev’s picture

Status: Needs review » Needs work
FileSize
9.04 KB
2.37 KB

Setting back to NW. This new patch adds more test coverage, ::testDirtyFields is expected to fail (due to suggested change in #37, #39, #46), so leaving at NW for now.

Actually I am still not really sure what this is about. The dirty fields have to be emptied on the new translation object? Why would they not be empty on creating a new translation for that new language code?

So while writing tests, I finally understood why ::postCreate was implemented here.

    $entity = EntityTestMul::create([
      'name' => $this->randomString(),
      'langcode' => 'en',
    ]);

    $entity->set('name', 'Test');
    $this->assertNotEmpty($entity->getDirtyFields());

    $translation = $entity->addTranslation('sv');

    $this->assertEmpty($translation->getDirtyFields());

::addTranslation calls ContentEntityStorageBase::createTranslation, which calls Entity::getTranslation() that returns the new translation object. However, ::createTranslation then sets new values on that newly created object which gets stored in dirtyFields array. So the current solution works around that by, emptying the array again.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

geek-merlin’s picture

Title: Add ability to track an entity object's dirty fields » Add ability to track an entity object's dirty fields (and see if it has changed)

(Needed a while to find this again, so added googlefood)

geek-merlin’s picture

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.