Problem/Motivation
In #2451397: Entity denormalization fails to retrieve bundle Drupal\serialization\Normalizer\EntityNormalizer::denormalize()
had to add _restSubmittedFields
to the entity object so that Drupal\rest\Plugin\rest\resource\EntityResource::post()
and ::patch()
could work at all.
That work-around sets a new, undefined, public-but-for-internal-use-only property on Entity
objects:
$entity->_restSubmittedFields
The reason that work-around was introduced: field-level access checking must occur, but only for those fields that are being edited (POST
ed or PATCH
ed, respectively). Doing field access checking on all fields regardless of the field data that is actually being provided in the request body would result in incorrect REST API behavior. (For example: an optional field that is not being created or updated would still be validated, and might result in a validation error. We ran into this exact problem at #2405091: Cannot create user entities - {"error":"Access denied on creating field pass"} , which had a work-around added for it.)
To know which fields to check access for, i.e. the fields that were actually sent (in the request body), we need to pass information from an earlier point in the call stack to a later point in the call stack. When the entity is being denormalized (i.e. from array-of-sent-data to Entity
PHP object), we know which fields are present (in that array), and we need to pass that to the REST resource plugin.
In pseudocode:
\Drupal\rest\RequestHandler::handle($request) {
$method = $request->getMethod();
$entity_data = $serializer->decode($request->getBody())
// In here we must set $entity->_restSubmittedFields…
$entity = $serializer->denormalize($entity_data);
// So that the method on the plugin that we call here knows which fields were actually sent.
return $entity_rest_resource_plugin->$method($entity);
}
Consequences
Unfortunately, this has some negative consequences:
- Code smell: Key Drupal 8 functionality (CRUD on entities via the REST API) depends on a hack that is introducing global state.
- Contributed project soft blocker: each new serialization format's denormalizer must duplicate this hack, or otherwise that format won't work with the REST module. This required work-around means denormalizers MUST be aware of the REST module. (You can see this in core in the HAL module.)
Root cause
The root cause actually does not lie in the Serialization or REST modules. It lies in the Entity/Field/Typed Data API. Of course, the Entity/Field/Typed Data API was originally not designed with a REST API in mind. So the problem is that WSCCI did not pay the full integration cost. IOW: this is WSCCI Initiative technical debt from 2015 that we never paid off. It was rushed in because apparently in 2015, it was still impossible to POST
or PATCH
entities.
The capability that is missing in the Entity/Field/Typed Data API which REST functionality needs is tracking of changed aka "dirty" fields:
- For
POST
requests, we need to know the changes compared to the initial/default field values - For
PATCH
requests, we need to know the changes compared to the stored field values.
Proposed resolution
Either:
- 1. Alternative/less invasive work-around, but still work-around
- Rather than putting the burden on individual
Entity
denormalizers, we can decorate theserializer
service and make this do it automatically for all entity denormalizers. This is still keeping this work-around though, and not solving the root cause. - 2. Solve the root cause: add new capability to Entity/Field/Typed Data API
- First land #2862574: Add ability to track an entity object's dirty fields (and see if it has changed), then refactor REST-related code to no longer use
$entity->_restSubmittedFields
.
Remaining tasks
- Land #2862574: Add ability to track an entity object's dirty fields (and see if it has changed).
- Get patch to green. The patch should only change REST-related code.
- Review
- Commit
User interface changes
None.
API changes
None.
Data model changes
None.
Summary of the discussion
A summary is helpful because this issue is quite confusing:
- @alexpott proposed solution 1 in #1.
- Immediately after solution 1 was proposed, starting in #4, an alternative approach was explored: solving the root cause, by adding dirty tracking to
ContentEntityBase
. - Over the course of 2015, it was discussed more, as there were similar needs relating to updating the
changed
timestamp for translatable entities when translations change. But then that was solved in a different way. Comments #7–#11. - But then another Entity API bug became a blocker. (#11).
- A bunch of straight rerolls without digging deeper: #14 – #19.)
- @yched re-stated the problem in a clear way in #21.
- @yched and @mkalkbrenner questioning the reliability of Typed Data's
onChange()
API in #22–#29. - Everything until here is from April 2015 to October 2015.
- The next activity is in May 2016 — i.e. long after D8 was released.
- @fago points out in #34–#35 yet different problems. Not only was Typed Data not designed with certain things in mind, nor was
ContentEntityBase::values
. But he dispels part of what @yched said.
@fago generally likes the approach in the patch so far. He confirms that::onChange()
was designed to allow for determining changes, but that it was never implemented. - Silence. Until February 2017. In #38, Wim Leers points out that thanks to #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method (which happened during the period of silence), we now have test coverage to ensure we don't introduce BC breaks unknowingly. And he points out a related issue: #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior.
- From #39 to #49 he tries to push the patch forward, given @fago's feedback.
- Entity API maintainer @tstoeckler asked in #51 for the non-REST changes to happen in a separate issue.
- Wim Leers continued his work in #52–#71.
- Serialization module maintainer @damiankloip reviewed it in #72 + #73, was overall +1
- Wim Leers addressed the review in #74– #79.
- Entity API maintainer @tstoeckler again reviewing this in #80, Wim Leers addressing it in #81 and then moving all Entity API changes to a new issue: #2862574: Add ability to track an entity object's dirty fields (and see if it has changed).
- A hunk of this patch landed in #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX, so Wim Leers started rerolling this in #83–#93, not realizing this was actually still blocked on #2862574: Add ability to track an entity object's dirty fields (and see if it has changed).
Comment | File | Size | Author |
---|---|---|---|
#86 | 2456257-86.patch | 17.69 KB | Wim Leers |
#14 | 2456257-14.patch | 8.98 KB | Jaesin |
#19 | save_only_german.png | 77.91 KB | marthinal |
#19 | save_german_and_italian.png | 111.91 KB | marthinal |
#19 | 2456257-14-rerolled.patch | 8.99 KB | marthinal |
Comments
Comment #1
alexpottWow this is not nice. We can wrap the serializer and and then add the required property in
_restSubmittedFields
. This approach that any additional serializers added in contrib will work with the Rest module without having to handle the internal detail of setting_restSubmittedFields
.Comment #2
alexpottComment #3
damiankloip CreditAttribution: damiankloip commentedThis was only added in the ContentEntityNormalizer before, this will add this to every object (even though REST will not do that). I think it would be clearer about what is happening if all of that was inside the if()?
The Serializer also has a final serialize() method, on top of deserialize which you already have as final.
Comment #4
alexpottA totally different and I think cleaner approach - plus this list of changed fields could be used elsewhere.
Comment #6
alexpottlol I renamed the reset method at the last moment...
Comment #7
mkalkbrennerI worked on a similar functionality as introduced by patch #6.
But for the use case of #2428795: Translatable entity 'changed' timestamps are not working at all this approach from #6 is too simple:
The issue mentioned above needs to track changes per translation. Additionally a field should not be marked as changed until its value changed for real. (onChange seems to be called if you set a value again with its current value.)
Basically tracking of changed / dirty fields (changed values / additions / removals) is a good idea. The list of changed fields (per language) is essential to solve or ease the implementation of these issues at least:
#2297817: Do not attempt field storage write when field content did not change
#2465913: Write to the storage only affected entity field values
#2465901: [META] Make entity revision translation work
#2428795: Translatable entity 'changed' timestamps are not working at all
Comment #8
mkalkbrennerI integrated all the $changedFields stuff in my latest patch #42 for #2428795: Translatable entity 'changed' timestamps are not working at all and extended it to be "translation aware". If this one gets committed, the remaining patch here should look like the attached patch. I didn't break the API suggested here in #6.)
Comment #10
mkalkbrennerpostponed for #2428795: Translatable entity 'changed' timestamps are not working at all
Comment #11
mkalkbrennerI found a way to "simplify" #2428795: Translatable entity 'changed' timestamps are not working at all, so that I don't need the tracking of changes anymore. Therefore the patch from #6 here is again the complete one ;-)
But during my tests I run into a different issue: #2474075: Fix Node::preSave() and document that preSave() and postSave() are not working with ContentEntity translations.
That one is now a blocker for this issue as soon as the entity is translated!
I repeat my post from https://www.drupal.org/node/2474075#comment-9857739 to explain what happens.
Basically this doesn't work for translated entities:
I'll try to explain why.
The patch by @alexpott introduces a new property for entities:
If on purpose or not, this property is not one of the "synchronized" ones across the cloned translations in ContentEntityBase::initializeTranslation().
Therefor the tracking of changes in onChange() and the accessor getChangedFields() work separately on each translation (which is great BTW).
But as a consequence the resetChangedFieldList() method needs to called individually per translation!
If we keep the not obvious nature of methods like preSave() in combination with cloned translated entity objects we have two ways to fix the patch in #6:
1. synchronize the changedFields property across translations in initializeTranslation()
or
2. loop over all translations in postSave()
Nevertheless, many contrib developers might step into that trap when they add properties to custom content entities. Especially if they don't think about translation up front.
Comment #14
Jaesin CreditAttribution: Jaesin at Chapter Three commentedRE-rolled the patch from #6.
Comment #15
Jaesin CreditAttribution: Jaesin at Chapter Three commentedTest the re-roll.
Comment #17
mkalkbrenner@Jaesin: If you're working on this one, please read carefully through my comments in #2474075: Fix Node::preSave() and document that preSave() and postSave() are not working with ContentEntity translations first.
Otherwise this solution might fail on translated entities.
Comment #18
damiankloip CreditAttribution: damiankloip commentedSpoke to Alex, we agree this should be major now.
Comment #19
marthinal CreditAttribution: marthinal commentedRerolled #14
@mkalkbrenner I'm not sure how #2474075: Fix Node::preSave() and document that preSave() and postSave() are not working with ContentEntity translations affects here. I was testing using your test but changing things:
If I only save $german then here the results using debug() for the field keys.
Saving $german and then $italian from the test.
I'm not sure about the problem with postSave() for the current issue here. Maybe I'm missing something :)
Please can you add here a test or example about how #2474075: Fix Node::preSave() and document that preSave() and postSave() are not working with ContentEntity translations is affecting here?
Thanks!
Comment #21
yched CreditAttribution: yched commentedThe underlying design issue is that EntityResource::post() and ::patch() need to know which fields were present in the incoming request, which is known at an earlier point in the callstack, and in a separate API (EntityNormalizer::denormalize()). _restSubmittedFields is just a hacky way to pass the information between those two points in the callstack - which de-facto couples the Normalizer and the EntityResource.
As a possible fix : EntityResource::post() and ::patch() are called by \Drupal\rest\RequestHandler::handle(), which also passes the $unserialized data as a param (the post() and patch() methods currently just overlook the corresponding argument).
--> Could we deduce the "list of fields that were present in the incoming request" just by looking at the $unserialized data ?
Comment #22
damiankloip CreditAttribution: damiankloip commentedYes, I think I talked about something similar to this when the problem first came about. Handle this in rest only. I just want _restSubmittedFields out of serializtion module :) No way should it be anywhere near there.
The changed fields addition seems generally nice, and something most ORMs support, for example. @yched, is there something you don't like about that approach?
Comment #23
marthinal CreditAttribution: marthinal commented@yched $unserialized is the Entity. So, we can detect fields when denormalizing but not directly from $unserilized.
Comment #24
mkalkbrennerNote: this property is not synchronized across all translations of an entity.
This implementation only resets $changedFields for the last translation edited of an entity while all other translations keep their list of changed fields after save. Due to the fact that an entity save always saves every translation, you need to iterate over all translations here to reset all lists.
I wonder if this tracking of changed fields is still required at all.
The property ContentEntityBase::values "holds the original, unchanged values of the entity".
Can't we just iterate over all fields and use FieldItemListInterface::equals() to detect the changed fields?
Comment #25
yched CreditAttribution: yched commented@marthinal : Oh, right, crap. We could do something with the denormalized *array* data, but the fully unserialized Entity is useless here.
Just tricky debugging sessions with @fago a couple month ago which left me under the impression that the onChange() notification chain was complicated and brittle, so a general preference for solutions that avoid relying on it for critical stuff...
But that qualifies as FUD, probably. And we have no alternative approach here, so I'll shut up :-)
Comment #26
mkalkbrennerI repeat my question from comment #24:
I wonder if this tracking of changed fields is still required at all.
The property ContentEntityBase::values "holds the original, unchanged values of the entity".
Can't we just iterate over all fields and use FieldItemListInterface::equals() to detect the changed fields?
Comment #27
yched CreditAttribution: yched commented@mkalkbrenner
Hm - I'm not sure if that's a reliable, designed-that-way feature (I doubt it's guaranteed by non-regression tests, for example ?), nor if this is 100% guaranteed fo the entire lifecycle of an entity.
ContentEntityBase::values is protected, that's just internal to how content entities optimize the instanciation of the FieldItemList / FieldItem objects. I don't think it's intended for use by the outside world ?
Comment #29
mkalkbrennerBut is
FieldableEntityInterface::onChange()
reliable?If you set a field value on an entity, it triggers a setter which finally calls
Map::onChange()
:This method finally calls onChange() on the entity (parent). But the API allows to explicit prevent that notification.
On the other hand there are already todos in ContentEntityBase:
Isn't it better to implement
@todo: Add methods for getting original fields and for determining changes
instead of implementing another change tracking in parallel?Comment #30
yched CreditAttribution: yched commented#29 Yep, TypedData's ::setValue($value, $notify = TRUE) allows the data to "react to the change but do not notify upstream". IIRC, that is intended for internal stuff like "modifying ERItem->entity needs to modify ER->target_id accordingly, and reversedly", where you don't want to fire the notification twice and end up in a loop. When used wrong, this can break the notification chain, yes. Then again, it's our official API for this kind of stuff :-)
Regarding the @todo about ContentEntityBase::$values :
Yes, but It dates from the very first TypedData / EntityNG commit (heh, exactly 3 years ago), so not sure it's still relevant after all the code changes since then :-)
Also, $this->values is not fully immutable :
- __sleep() writes the actual FieldItemList values back into $this->values. Not sure if that affects the $entity after it's been serialized, or if that just affects the serialized data.
- removeTranslation() removes stuff from $this->values
- updateOriginalValues() writes to $this->values (intended to be called only after save())
So, between the two approaches... would be good to hear @fago's thoughts :-)
Comment #31
Wim LeersWow, quite a significant WTF!
Assigning to @fago per #30.
Comment #33
Wim LeersPinged @fago to get his feedback: https://twitter.com/wimleers/status/727145004804792324
Comment #34
fagoThanks for the ping wim!
hm, in most case - yes. But I'm not sure that's always the case, nor was it designed or documented to work like that. It could very well be that some entity type implements logic - e.g. onChange() - that causes the values to be updated. And then there are cases like sleep() which write to it - as yched mentions. That said, I do not think we should rely on that.
hehe. We already use it some critical cases afaik, like for clearing static caches. It should be fine to use it for that also + we managed to solve those complicated cases. So time to take advantage of it now?
I generally like the approach take of the patch - although there are probably more special cases and docs we'd have to think about and figure out. E.g. - what are the changed fields of a created entity? (Probably all). Yeah, and as mentioned we need take care of updating all translations.
Yes, onChange() was intended to allow for that - but we never got to implementing it.
A few remarks regarding the patch:
No need to instantiate all field objects - better use getFieldDefinitions()
nitpick: a list - that the list is representated as array is an implementation detail.
docs need work
Needs work + explanation when it's valid to call that.
I do not know why the patch changes the access calls here - that seems out of scope for this patch and is wrong. There is no such thing as delete or update access on fields, field access only knows view and edit operations.
Comment #35
fagooh, then the patches introduces an API change by adding those methods to the interface. I think we can avoid that by moving it into a new,optional interface, implemented by ContentEntityBase.
Comment #38
Wim LeersWe really need to get this going again. We need this to be fixed.
Since #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method, we have thorough test coverage that should catch BC breaks in this area. This is also closely related to #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior.
For my own understanding, I rolled a debugging patch. With this applied, you can do something like:
and hence get clear output about the difference between the current algorithm in HEAD (the
_restSubmittedFields
hack we want to get rid of) versus what a sane implementation (usingFieldItemList::isEmpty()
andFieldItemList::equals()
) would look like. My implementation is super naïve, but I'm kind of scared by the complexity in the prior patches.Any takers?
Comment #39
Wim Leers#38 conflicted with #2751325: All serialized values are strings, should be integers/booleans when appropriate. Rerolled.
Comment #40
Wim LeersI've re-read this entire issue again.
Given that @fago wrote this in #34:
… I'm now thinking that that approach in the latest patch (#19) actually is okay. Despite me having written in #38.
First: a straight rebase of #19. A lot has changed since then.
Comment #41
Wim LeersOops, I didn't mean to delete this. I already fixed that locally but failed to add it to the patch.
Comment #43
Wim LeersThe problem with the patch in #41 (aside from dealing with translations etc, that still needs to be done!) is that it even marks initial field values as "changed". The reason is two-fold:
$entity->$name = $values[$name];
inContentEntityStorageBase::initFieldValues()
uses the magicalContentEntityBase::__set
setter, which sets a value with$notify = TRUE
.$entity->get($name)->applyDefaultValue();
inContentEntityStorageBase::initFieldValues()
uses\Drupal\Core\Field\FieldItemList::applyDefaultValue()
, without setting the$notify
parameter, so the default of$notify = TRUE
is usedThe solution is therefore:
setValue()
, so we can pass$notify = FALSE
applyDefaultValue()
with$notify = FALSE
ContentEntityBase::_set()
to state explicitly that it will use$notify = TRUE
(optional change, but makes this less confusing going forward)With this change… all REST tests seem to be passing :) (REST does not yet have entity translation support, nor entity translation test coverage.)
Comment #44
Wim LeersForgot patch. Let's see how many Entity/Field API tests fail.
Comment #47
Pavan B S CreditAttribution: Pavan B S at Valuebound commentedLine exceeding 80 characters
Line exceeding 80 characters
Made changes as per the coding standard, please review.
Comment #48
Wim Leers@Pavan: If you see that an issue is clearly still in an early, experimental, researching phase… please do not reroll it in this incredibly unhelpful way. This is very annoying. Because it's very distracting.
Comment #49
Wim LeersEntityResourceTestBase
tests, plusRestRegisterUserTest
. Because it's checking fieldedit
access for more fields than it should.EntityResourceTestBase
tests in therest
module now pass. YAY!hal
module fail.This should fix the failing
EntityResourceTestBase
tests in thehal
module. Next: fixing the other failing tests — if at all possible.(Interdiff relative to #44.)
Comment #51
tstoecklerThis is useful for all kinds of things, not just Rest. I think there are also issues about this already. In any case, if this is needed for this to land, please extract it into a separate patch/issue and mark this as postponed, so it can be discussed/reviewed properly in its own right. It is quite an important change, but also a very risky one, so it needs a lot of thought.
Comment #52
Wim Leers20 HAL failures in #44. 5 in #49. That's a good step in the right direction.
Root cause analysis: part 1
While awaiting that, I was working on determining the root cause for the failing
ContentEntityFieldMethodInvocationOrderTest
test.This is what's causing all those entity translation test failures, including the one for
ContentEntityFieldMethodInvocationOrderTest
: almost all those new test failures in #44 (and almost all remaining test failures in #49).initFieldValues()
is called from two places:\Drupal\Core\Entity\ContentEntityStorageBase::doCreate()
\Drupal\Core\Entity\ContentEntityStorageBase::createTranslation
Root cause analysis: part 2
I had hoped that letting
doCreate()
callinitFieldValues()
with$notify = TRUE
and lettingcreateTranslation()
callinitFieldValues()
with$notify = FALSE
would solve the problem. But it did not.Turns out that
ContentEntityBase::onChange()
does magic with translations. For example when you do:create()
creates a default translation (x-default
) and assigns the three given values to the respective fields. Upon assigninglangcode
,ContentEntityBase::onChange()
is called. The first case in its switch statement then detects that this is indeed the default translation (x-default
) and then sets the default langcode to the value we passed:de
.From that point onward, all translation-related code will now correctly handle this default translation (
x-default
) as thede
translation. SoContentEntityBase::getTranslation('de')
will return it as expected.All of this only works as long as
ContentEntityBase::onChange()
is called when setting thelangcode
field value, i.e. only if$notify = TRUE
while setting that value.Therefore we have no choice but to let
$notify = TRUE
. But that will again meanEntityResource::patch()
will explicitly handle many fields as "updated"/"edited"/"modified"/"changed", despite them not having changed at all!Solution
ContentEntityStorageBase
made by #44resetChangedFieldList()
at the end of bothdoCreate()
andcreateTranslation()
Comment #54
Wim LeersCaveats
resetChangedFieldList()
ContentEntityBase()
itself. And in fact, this is possible when callingEntity::create()
! We can letContentEntityStorageBase::postCreate()
call the reset method :)However, it becomes a bit more difficult when considering
ContentEntityStorageBase::addTranslation()
. But…ContentEntityBase::addTranslation()
callsContentEntityStorageBase::createTranslation()
.createTranslation()
is much likedoCreate()
, because both callinitFieldValues()
. Also,createTranslation()
invokeshook_translation_create()
. So… I think the solution in the end is quite obvious:createTranslation()
should also callEntity::postCreate()
!\Drupal\Core\Entity\FieldableEntityInterface::onChange()
and\Drupal\Core\TypedData\TraversableTypedDataInterface::onChange()
.However, what we need for the purpose of REST is not
, what we need is . So perhaps the "changed" ingetChangedFields()
andresetChangedFieldList()
is not narrow enough. Perhaps what we want is "modified", which we then define as .Because "changed" is used for all sorts of TypedData-related magic, with automatic calculation of property values etc. See for example
\Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem::onChange()
. That's specifically not what we need.Further evidence that "changed" in
getChangedFields()
is different: we're callingresetChangedFieldList()
after we've calledinitFieldValues()
, and hence after having calledhook_entity_field_values_init()
+hook_ENTITY_TYPE_field_values_init()
. i.e. it's about changes compared to the initial values.ContentEntityStorageBase
orEntity
?Entity
… but we can be pragmatic, and first only add it toContentEntityBase
, and then perhaps move it up a level in the abstraction chain later.Only the first caveat is addressed in this issue. This means I was able to remove
FieldableEntityInterace::resetChangedFieldList()
. That means disruption is smaller, and more importantly: we reduce the error surface area, because we prevent accidental or incorrect calls to this method!Leaving caveats 2 and 3 for future rerolls. Now at least the thinking/concerns are documented while they're still fresh.
Note that the number of failing tests should remain the same. This is pure clean-up.
Comment #56
Wim LeersD'oh, forgot one hunk! Canceled the test of #54.
Comment #57
Wim LeersThis apparently causes fields to be validated in a slightly different order. Easy fix.
Should bring it down to 6 failures.
Comment #60
Wim LeersHm, let's revert #54 + #56 for now. Then we should really be at 6. I can revisit that clean-up later. Getting to 0 fails is more important right now.
Comment #61
Wim LeersComment #63
Wim LeersOne more small change to HAL tests, similar to the changes made previously in #57. Now really down to 6 failures.
Comment #64
Wim LeersTurns out all remaining REST failures (3 of the 6 remaining failures) have the same root cause as #2768651-85: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX! Importing the fix from there. Not importing the test coverage. Because I expect #2768651 to land long before this.
Comment #65
Wim LeersAnd the final 3 failures all have the same hilarious root cause!
\Drupal\user\Plugin\rest\resource\UserRegistrationResource::post()
has this:This used to work fine because we were relying on
_restSubmittedFields
incheckEditFieldAccess()
.But now we're relying on "changed field" tracking inside the entity object itself. Which means that
$account->block()
or$account->activate()
cause thestatus
field to be changed!So the solution is simple: first call
checkEditFieldAccess()
, then block/activate the account!Comment #67
Wim LeersNote that this is blocking #2824851 — see #2824851-61: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior.
Comment #69
Wim Leers#64 has a new/unexpected failing test:
Drupal\Tests\serialization\Unit\Normalizer\EntityNormalizerTest
. I wrote this in #64:Turns out I need to import those test changes too. Doing that here then.
Comment #71
Wim LeersWoot, green! All of the feedback from @fago in #34 still needs to be addressed. But now we at least now this is a workable solution.
Looking forward to some initial reviews.
Comment #72
damiankloip CreditAttribution: damiankloip commentedOverall I think this is looking pretty good. It is so good to have this moving again, and to finally have a generic solution for 'changed' fields. Not needing to rely on the serializer - coupling REST, serialization, and entities more than we need to.
We use the word 'changed' in quite a few places in Drupal. I wonder if there are some better alternatives we could use here... A common terminology is to use 'dirty'. So 'getDirtyFields' or 'hasDirtyFields' etc.. That's just the first thing the sprang to mind, as a few other frameworks, like Laravel use similar. Other suggestions totally welcome!
I wonder if we need this method to be public? I see it is needed directly after an entity is saved and it gets persisted to storage.
YES PLEASE.
Ordering issues in the tests?
Yes, I think I noticed this before. That REST module was assuming all entities would be able to use this fieldable behaviour. Totally +1 on this.
While we're here we could move this logic into another method to improve the readability a bit?
This is just horrible... and this is waaaay nicer.
Regading @fago's comments in #34, It seems like a good idea, and correct to have all fields on a newly created entity as 'changed'. I guess our definition of changed is 'different to the persisted value'? Do we need to store the original values in another property on content entities maybe? Otherwise, we would have to load the original all the time? If we cannot rely on the $values property that is..
Comment #73
damiankloip CreditAttribution: damiankloip commentedNot sure if it's simpler (to me it is), but this could just use an
array_intersect_key()
instead?Comment #74
Wim Leers#72 :)
getDirtyFields()
is a much better name! Done.public
.However, when I first tried to fix that, in #54, things failed horribly, so I reverted that in #60.
Trying again now. (AFAICT #54 failed because it caused
->newRevision = TRUE
to be set on every translation of the entity too, rather than only doing it for the default translation.#73: done.
Still to do: #34 + #72.5. Doing that next.
Comment #76
Wim LeersApparently besides #34, there was also #35. Doing that first.
Comment #78
Wim Leers(Rebased this one. So patch should apply again.)
#34:
Per your recommendations, the patch is already not relying on
ContentEntityBase::$values
, and is instead relying ononChange()
.… so I'm not sure that what you're suggesting is actually better?
\Drupal\Core\Entity\FieldableEntityInterface::getFields()
, so also fixed this there.\Drupal\Core\Entity\FieldableEntityInterface::getFields()
, so also fixed this there.That still leaves the "track dirty fields per translation" stuff that was mentioned by @mkalkbrenner in #11 + #24 — I thought @fago also talked about it in #34, but he does not. Doing that next.
Comment #79
Wim LeersAnd this then makes the dirty field tracking per-translation.
Comment #80
tstoecklerJust read the patch, have only followed the last couple of comments, not read the whole issue:
This should now mention the "per-translation" part, also the typehint is now no longer valid.
If this is in fact necessary, it should be its own issue including dedicated test coverage.
Why is the intersect needed? What would be in
$this->dirtyFields
that's not a valid field name?Again this needs dedicated test coverage.
Here, as well.
I already mentioned this above, but this must definitely be its own issue. It is needed for a bunch of things that have nothing to do with Rest. And as you already found out with a bunch of the changes above this really is a tricky change, that needs a lot of thought and a lot of tests.
Maybe we can tackle some of the things that I said should be split off above together with this, not sure.
This should be
EntityWithDirtyFieldsInterface
now?Comment #81
Wim Leers#80
$this->dirtyFields
only contains booleans:TRUE
is the default value for that parameter. I'm just making it explicit.Currently creating a new
entity system
issue for this.Comment #82
Wim LeersThis is blocked on #2862574: Add ability to track an entity object's dirty fields (and see if it has changed).
Comment #83
Wim LeersSince this is also blocking #2824851, that issue is now blocked on two issues: #2824851-62: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior.
Comment #84
Wim LeersThis is not a functional bug, this is cleaning up architectural/technical debt.
Comment #85
Wim Leers#2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX landed, so this is now unblocked!
This is the same patch as #81, minus the hunks in
EntityNormalizer
(added in #64) and inEntityNormalizerTest
(added in #69). The changes to those files were imported from #2768651: Let TimestampItem (de)normalize to/from RFC3339 timestamps, not UNIX timestamps, for better DX, and since that now landed, those hunks are effectively already committed :) The bugfix this was blocked on is committed!Hurray, let's see if this is still green :)
Comment #86
Wim Leers3 failures. Just like some changes to
CommentHalJson*Test
were necessary, the same is true forNodeHalJson*Test
.Comment #89
Wim Leers#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?Comment #90
Wim LeersUgh, I forgot to bump this from PP-1 to PP-2 in #82. This is still blocked on #2862574: Add ability to track an entity object's dirty fields (and see if it has changed).
Comment #91
Wim LeersComment #92
tstoecklerUnless I misread something, unassigning per #90.
Comment #93
Wim LeersYep, sorry, I should've done that!
Comment #94
Wim LeersIS revamped.
Comment #95
Wim LeersComment #96
Wim LeersAttempt to improve the title.
Comment #97
Wim LeersAdding a link to support one particular example in the IS.
Comment #98
Wim LeersI thought for a moment that #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior might help solve this without needing #2862574: Add ability to track an entity object's dirty fields (and see if it has changed), but AFAICT that's not actually possible.
I thought this might work:
… but it can't. Because that will even trigger for fields that weren't submitted. And I thought I could work around that by then doing
in the loop, but that would effectively introduce the information disclosure security vulnerability that #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior so carefully avoided.
Therefore, this is still blocked on #2862574: Add ability to track an entity object's dirty fields (and see if it has changed).
However, this definitely doesn't block #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior anymore. Since that wasn't the reason this was made (this was marked major in #18, 25 months ago), keeping this major for now.
Comment #100
Wim LeersJust to prove or unprove the reasoning in #98, I tried doing what I described I first thought could work and then thought could not work. Because #2824851: EntityResource::patch() makes an incorrect assumption about entity keys, hence results in incorrect behavior landed yesterday, so if I'm right, the approach in #98 should result in a failing test.
The change:
And it did fail (that's the original field value, the new field value, the response body, and the PHPUnit output, respectively):
But that's just for the
nid
field, which we're not sending. What if we do the naïve thing and ignore any fields whose new value is empty and original value is non-empty?Then we fail on UUID, which is auto-generated:
What if we also explicitly ignore changes to the UUID field, because that can never be changed? Then we fail on the revision timestamp, which is also auto-generated:
Conclusion: we really need to have some way to know which fields have been sent/changed.