Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Found out in #1977206: Default serialization of ConfigEntities, which is based on the concept of:
- serializing a configEntity only keeps the values from $entity->toArray() (= CMI content)
- unserializing it passes those values through $entity->_construct()
But :
- ConfigEntity::_construct() calls $this->setOriginalId($this->id()) if $this->id() is not empty
- Entity::setOriginalId() does $this->enforceIsNew(FALSE)
But a config entity can totally have a non-null id and still be "not saved yet".
So that $this->enforceIsNew(FALSE) is incorrect for ConfigEntities ?
Comment | File | Size | Author |
---|---|---|---|
#31 | 2405165.31.patch | 2.94 KB | alexpott |
#31 | 29-31-interdiff.txt | 4.22 KB | alexpott |
#29 | 2405165.29.patch | 7.16 KB | alexpott |
#29 | 28-29-interdiff.txt | 749 bytes | alexpott |
#23 | 2405165.23.patch | 2.85 KB | alexpott |
Comments
Comment #1
yched CreditAttribution: yched commentedIs it as simple as this ?
Comment #2
yched CreditAttribution: yched commentedHem, still need to return $this
Comment #4
alexpottNeeds tests and perhaps a comment as to why we are not calling the parent.
Comment #5
yched CreditAttribution: yched commented@alexpott : Yep - I wanted to get some feedback first :-)
Comment #6
yched CreditAttribution: yched commentedBut didn't intend to change the status back.
Comment #7
yched CreditAttribution: yched commentedStill needs tests, but for now this should let us simplify the code added in EntityDisplayBase::__wakeup() in #2094499: Convert "Manage (form) display" forms to be the official entity edit forms for EntityView[Form]Display objects
Comment #8
yched CreditAttribution: yched commentedBack to NW for tests
Comment #9
tim.plunkettI read the IS a couple times but I'm still not 100% grokking this.
If enforceIsNew(FALSE) is problematic for ConfigEntityBase, why not override that method instead?
Comment #10
yched CreditAttribution: yched commentedTBH, this area (isNew() / enforceIsNew() / setOriginalId()) confuses me :-)
Fact is : the relationtip between entity IDs and "being new" is completely reversed in ContentEntities and ConfigEntities :
- ContentEntities receive their ID (computed, sequential) by being saved.
- ConfigEntities need to have an ID (user-chosen machine name, or compound of machine names like "entity_type.bundle.name") before they can be saved. Completely fresh entities (like the ones instatiated to build a "create" form) don't have that ID yet, but they need to get it at some point (e.g required field in the form).
- To further complicate things, setOriginalId() seems to be mostly about handling renames (entity ID changing), and ContentEntities / ConfigEntities are vastly different on that regard too :-/
Then :
- I'm not sure why ConfigEntity::_construct() calls $this->setOriginalId($this->id()). Not saying it's wrong, just I don't know the reason.
- setOriginalId() enforcing "is new" feels right for ContentEntities, but definitely not for ContentEntities, that's what the patch fixes.
- I'm not sure if enforceIsNew(TRUE/FALSE) means anything for a ConfigEntity to begin with - but TBH I'm not sure what it means for a ContentEntity either ("I already have nid 12, but treat me as if I'm new anyway" / "I don't have a nid yet but don't treat me as new" ?)
I definitely think there is some clarification to be made in there, but I think the patch, as is, is correct in itself ?
Comment #11
yched CreditAttribution: yched commented- Rerolled after #2413841: EntityDisplayBase::__wakeup() should avoid calling toArray()
- With tests (interdiff is the test-only patch)
Comment #13
yched CreditAttribution: yched commentedTest-only patch failed as it should,
patch passed as it should.
Comment #14
yched CreditAttribution: yched commentedBump + reroll just in case
Comment #15
alexpottThis makes sense to me.
Comment #16
xjm@alexpott re #15, lucky you. ;)
Advance warning that this is not a review; it is notes from me trying to figure out what the heck is going on. This is the most confounding 2K patch I've dug into in awhile. ;) Feel free to ignore this comment for now and sorry for the noise...
So the docs for
setOriginalId()
read:(Int or string or
NULL
, really? And null carries specific meaning? Aiee.)Then in
enforceIsNew()
:So it references Migrate specifically, on docs for EntityInterface,(not actually, see #17) and passing a null value is equivalent to passing TRUE. Oh goody. ;)All
Entity::enforceIsNew()
does is set the protected class property to whatever you passed.Entity::isNew()
checks it to do:(similarly in the overridden
User::isNew()
which specially allows you to have a user ID of 0 that's not considered new, because Anonymous).While
ConfigEntityBase::isNew()
does:The method is also called in:
setOriginalId()
(see below).createDuplicate()
(always TRUE on the duplicate).EntityStorageBase::create()
(always TRUE on the new entity).EntityStorageBase::save()
(always FALSE on the newly saved entity).ConfigEntityStorage::createFromStorageRecord()
(always TRUE on... whatever the heck that method does. "Allows the configuration entity storage to massage storage values before creating an entity." K.)SqlContentEntityStorage::doSave()
in the situation whenEntity::isNew()
is true (which, keep in mind, also checks$enforceIsNew
, because it's good to reinforce that your new entities enforced to be new are new. The comment says:Then at the end of the method it sets
enforceIsNew(FALSE)
because once it's saved it's not new anymore. (Remember this happens again inEntityStorageBase::save()
, right afterdoSave()
gets done and caches are reset.)getEntity()
method, set toTRUE
when the entity is missing an ID or previous data.FALSE
instead, because I guess there's an assumption that an entity revision has an existing entity already (it loads it the line above).ShortcutSet::postSave()
, setting it to TRUE when the set is not the default set (contrary to the comment that says it is the default set...), after creating a duplicate of the set and saving it in place of the set. (Keep in mindcreateDuplicate()
already does that.)UserStorage::save()
, before callingSqlContentEntityStorage::save()
, to get the next serial ID when the user is new and enforce that it's new, because something to do with Anonymous having a user ID of 0? Haven't figured this out yet.The property is also set directly in
ContentEntityBase::initializeTranslation()
as part of cloning the entity for a new translation, and inEntityDisplayBase::__sleep()
and__wakeup()
(because it's a protected property we need to care about after serializing).And then the property gets checked in
ConfigEntityBase::isNew()
andEntity::isNew()
Then the base implementation in
Entity::setOriginalId()
is:And... that's it. Huh? I would have expected that it, you know, set the original ID somewhere, like the method docs and name say it does. I guess the idea is that the method is a no-op for entity types that do not support renames, and to support renames, you override it. But what happens when you use
Entity::setOriginalId($not_a_null_value)
without the method being overridden? It doesn't setenforceIsNew()
at all -- neither true nor false -- and doesn't do anything else either. That is at least confusing. If we don't support the concept of an "original ID" without the method being overridden, shouldn't we throw an exception or something?Keeping in mind
Entity::isNew()
does:...which means that if I call
setOriginalId()
with some value (that is discarded), it will indicate the entity is new if no nonzero ID is set. Conversely, if I callsetOriginalId()
with a null value... oh wait, the exact same thing happens, because it setsenforceIsNew(FALSE)
which is also empty.Moving on to
ConfigEntityBase
.ConfigEntityBase::setOriginalId()
in HEAD does:... so the missing setting of an original ID that confused me above, plus setting
$enforceIsNew
toFALSE
, the thing that this patch changes. AndConfigEntityBase::isNew()
then does:So a config entity is by default only new when you enforce that it is new. The docblock for it says:
...which makes no sense to me yet.
setOriginalId()
gets called inEntityStorageBase::save()
(to set the ID of the entity you just saved as the original entity ID, ahhhh) and infile_copy()
underneath a@todo
referencing #2241865: Do not create a new file entity in order to overwrite an existing entity. Edit: and inConfigEntityBase::__construct()
when the entity ID is not null or an empty string, I guess setting up in case we rename stuff.Getting more coffee...
Comment #17
xjmSo the thing about Migrate is a red herring;
enforceIsNew()
has existed with that documentation since #1495024: Convert the entity system to PSR-0 and before back to the original entity OO conversion.Comment #18
xjmOf note:
#2218033: Move getOriginalId()/setOriginalId() to EntityInterface
Comment #19
xjmAlso of note: #2241655: EntityStorageInterface::create() should always create a "new" entity
Comment #20
xjmAnd also #2088241: ConfigEntityInterface::setOriginalID() should return $this.
Comment #21
xjm...And
setOriginalId()
was added in #1798944: Convert config_test entity forms to EntityFormController, which seems wildly out of scope given the title of that issue, but there it is.Comment #22
xjmsave()
method for config entities.EntityInterface
instead of just config entities, with the following reasoning:That patch also added the concept of it being a no-op in the base Entity implementation, with config entities overriding it.
So I think I convinced myself that this issue's patch is indeed the correct change and not reverting some other, previously intended behavior. However, in #4, @alexpott suggested that we add a comment in
ConfigEntityBase::setOriginalId()
explaining why we are not calling the parent. I agree. So let's add that (per the reasoning in the summary), and then I think this can go in.Thanks for your patience, and thanks @amateescu for chatting about the issue in IRC too. :)
Comment #23
alexpott@xjm nice work.
Since this is such a minor change setting back to rtbc.
Comment #24
xjmThanks @alexpott.
Regarding the docs, I guess this is fine for now, but I'm slightly concerned that we'd have to reference this issue in the codebase rather than something actually in the codebase. Usually we only add links to issues as temporary @todos. Reading it, I'd expect an open issue to clean stuff up more, not a fixed one. I think so long as we have the comment we can remove the issue link (as the comment itself can be git blamed at any rate if someone wants the backstory).
Ideally there would be some longer code documentation we could point people to. Which doesn't exist, and isn't in scope here, and I don't even know what it would be, but... something to explain how
setOriginalId()
andenforceIsNew()
andisNew()
interact (and maybe to fix them... somehow... I don't exactly know what would be to fix).Since I don't even know what issue I would file, leaving at RTBC for now.
Comment #26
alexpott#25 is a weird testbot blow up - lots of
simplexml_import_dom(): Invalid Nodetype to ...
Comment #28
alexpott@xjm this is not the only place this is done in the code - see, for example, https://api.drupal.org/api/drupal/core!includes!database.inc/function/db...
And now if someone followed the link they'd find your helpful comments.
Comment #29
alexpottTalked with @xjm in IRC - @xjm pointed out that links to issues in docs are not helpful and confusing. Opened #2478811: Document the entity life cycle and the differences between config and content and changed to a
@todo
Since it is a docs only change leaving at rtbc.
Comment #30
xjmThis includes another patch. Thought for like three
git reset --hard
I had both applied.Comment #31
alexpottOh drats... I thought I'd checked for this. alexpott--
Comment #33
xjmCommitted and pushed to 8.0.x -- thanks!
Sneaked in a comma on commit:
Comment #34
yched CreditAttribution: yched commentedyay - and thanks for the detective work @xjm !