Comment does some voodoo to its entity_id base field to allow the target entity type to vary per bundle. This works really well and I've copied over that behavior in the Group module, but I recently ran into an issue with it which can also be reproduced in Comment.
If you create a Comment entity and for any reason call $comment->entity_id->entity
to load the entity the comment belongs to, this returns NULL unless there is also a node (or user) with the same ID as the value of entity_id. This is because the bundle field definition simply clones the original base field and adds a target_type setting.
What happens is that the bundle field now has the propertyDefinitions
property from the base field and the base field was set to target nodes (or users) because it didn't specify a target_type. See EntityReferenceItem::defaultStorageSettings() as to why that is.
So how does this break stuff?
Simple, try using $comment->entity_id->entity->label()
on a newly created comment which is attached to any non-node content entity while the Node module is enabled and no nodes are created yet.
So how do we fix it?
Either allow us to unset the propertyDefinitions
property (and perhaps schema
as well) on a BaseFieldDefinition. This seems to be the goal of #2346329: hook_entity_base_field_info_alter() and hook_entity_bundle_field_info_alter() are documented to get a parameter that doesn't implement an interface with setter methods?
Or, do not simply clone the base field definition but recreate it. This is the temporary fix I've implemented in Group: #2734105: Adding a group for the first time crashes because of incorrect field definitions
The code that fixed it:
/** @var \Drupal\Core\Field\BaseFieldDefinition $original */
$original = $base_field_definitions['entity_id'];
// Recreated the original entity_id field so that it does not contain any
// data in its "propertyDefinitions" or "schema" properties because those
// were set based on the base field which had no clue what bundle to serve
// up until now. This is a bug in core because we can't simply unset those
// two properties, see: https://www.drupal.org/node/2346329
$fields['entity_id'] = BaseFieldDefinition::create('entity_reference')
->setLabel($original->getLabel())
->setDescription($original->getDescription())
->setConstraints($original->getConstraints())
->setDisplayOptions('view', $original->getDisplayOptions('view'))
->setDisplayOptions('form', $original->getDisplayOptions('form'))
->setDisplayConfigurable('view', $original->isDisplayConfigurable('view'))
->setDisplayConfigurable('form', $original->isDisplayConfigurable('form'))
->setRequired($original->isRequired());
Comment | File | Size | Author |
---|---|---|---|
#35 | interdiff-29-35.txt | 1.45 KB | hchonov |
#35 | 2734345-35.patch | 8.7 KB | hchonov |
#29 | 2734345-29.patch | 8.49 KB | hchonov |
#29 | 2734345-29-test-only.patch | 4.49 KB | hchonov |
#28 | interdiff-27-28.txt | 5.92 KB | hchonov |
Comments
Comment #2
kristiaanvandeneyndeComment #5
danmuzyka CreditAttribution: danmuzyka at Phase2 for Workday, Inc. commentedI can confirm that
\Drupal\comment\Entity\Comment::bundleFieldDefinitions()
sets thetarget_type
property of theentity_id
field incorrectly. The problem is the way that theclone
keyword behaves in PHP. As http://php.net/clone explains:$base_field_definitions['entity_id']
has a property nameditemDefinition
, which is an instance of the class\Drupal\Core\Field\TypedData\FieldItemDataDefinition
. This object property is actually a reference to another object, so even though$base_field_definitions['entity_id']
is cloned into a new object, theitemDefinition
property of the new object remains a reference to the same object as the original instance of$base_field_definitions['entity_id']
references. Therefore, the following line of code actually overrides the field definition of the entity_id field on all comment bundles that have already been built during the current HTTP request:The result is that the bundle-specific overrides for all of the comment bundle field definitions that were already built during a given HTTP request are overridden by the settings for the next comment bundle's field definitions. However, before the process of building a comment bundle's field definitions overwrites the already-built field definitions of other comment bundles, those definitions have been saved to cache by
\Drupal\Core\Entity\EntityFieldManager::getFieldDefinitions()
. On the next HTTP request, after the cache has been warmed,EntityFieldManager::getFieldDefinitions()
loads the correct field definitions for a given bundle out of cache. Therefore, this bug affects the copy of the field definitions in PHP memory during a single HTTP response after a cache clear, or when thediscovery
cache bin is disabled/set to null.I'm attaching a KernelTest that demonstrates this bug. I'll post a stopgap patch with a workaround in a separate comment.
Comment #6
danmuzyka CreditAttribution: danmuzyka at Phase2 for Workday, Inc. commentedHere is the patch.
Comment #7
danmuzyka CreditAttribution: danmuzyka at Phase2 for Workday, Inc. commentedAttaching both the patch and the version with the test only to trigger the testbot on the test only version, to demonstrate the failing condition.
Comment #9
kristiaanvandeneyndeSo currently the last bundle being built rules all previous bundles during the initial cache building, but because they are being cached individually they work well after being loaded from cache. Nice find!
The test looks great for this. Why do we need to install the 'sequences' schema, though?
I'm not 100% sure about the proposed fix. I mean, what stops other modules or places in core from having the same problem? Perhaps we should put some warning in the method documentation, explaining it is strongly discouraged to clone a base field definition.
You could move that huge comment to the doxygen block for
FieldableEntityInterface::bundleFieldDefinitions()
and refer toComment::bundleFieldDefinitions()
for an example.Also, is there no other way but to replicate code from
EntityFieldManager::buildBaseFieldDefinitions()
? This is always a bad idea because it may become out of date as the original code is updated/fixed/etc. The approach I listed in the summary works well too, albeit a bit verbose.Perhaps we need to introduce a magic
__clone()
method onBaseFieldDefinition
to allow for cloning after all while making sure the copy is a deep copy?TL;DR: Nice find, great test, rethink approach: Clone properly or document more?
Comment #10
kristiaanvandeneyndeSemi-related: #2346347: Finalize API for creating, overriding, and altering code-defined bundle fields
Comment #11
BerdirHm, why don't we just implement __clone() on the BaseFieldDefinition object and possibly other related definition objects and make sure that we actually do a proper deep clone? Then it would just work?
Comment #12
kristiaanvandeneyndeYes, exactly that!
Comment #13
danmuzyka CreditAttribution: danmuzyka at Phase2 for Workday, Inc. commentedThanks for the feedback @kristiaanvandeneynde and @Berdir ! Assigning to myself to work on an approach based on defining a
__clone()
method on theBaseFieldDefinition
.Comment #14
danmuzyka CreditAttribution: danmuzyka at Phase2 for Workday, Inc. commentedI've attached a new patch (and an interdiff) that addresses the bug by implementing a deep clone using
BaseFieldDefinition::__clone()
.Comment #15
danmuzyka CreditAttribution: danmuzyka at Phase2 for Workday, Inc. commentedI am seeing on my local environment that the deep clone is not cloning all of the way down the chain of object properties...assigning back to myself to fix...
Comment #16
kristiaanvandeneyndeYeah you'll need to implement __clone in every object that it might use as a property. And then some :)
Comment #17
danmuzyka CreditAttribution: danmuzyka at Phase2 for Workday, Inc. commentedHere's a slightly different approach to the deep clone.
Given that
BaseFieldDefinition->itemDefinition->fieldDefinition
is a recursive reference back to theBaseFieldDefinition
object, we need a way to overwrite that reference in theFieldItemDataDefinition
class without having to instantiate a new instance of that class, and potentially losing any property values (e.g., custom values set by alter hooks). This patch adds aFieldItemDataDefinition::setFieldDefinition()
public method to overwrite the existing value ofBaseFieldDefinition->itemDefinition->fieldDefinition
.Comment #18
BerdirI see, this is tricky.
A bit weird to add this method without having it on an interface, but the get() above is the same (which btw is only used twice in search_api and nowhere else that I can find).
Can we add a note that this is only used internally by BaseFieldDefinition::__clone() and should not be used otherwise?
Also, misses description and variable name of the @param.
Comment #19
kristiaanvandeneyndePatch looks good aside from Berdir's nitpick. I do agree that the circular reference is an annoying feature to have when trying to deep clone properly.
Comment #20
danmuzyka CreditAttribution: danmuzyka at Phase2 for Workday, Inc. commentedHere's an update to the patch with the changes to the comment. Let me know if this looks OK or not.
Comment #21
kristiaanvandeneyndeLooks good to me!
I'd argue that it's not "The new field definition to assign this item definition." but rather "The new field definition to assign to this item definition.", but don't let such a small nitpick stand in the way of an RTBC stamp.
Comment #22
xjmNot sure why this issue was moved back to 8.2.x, but that branch has no further development since Feb. 1. Since this includes API additions and internal breaks/disruptions, it should be targeted for 8.4.x. Thanks!
Comment #23
alexpottThe patch on #20 does not apply to 8.4.x
Is there any reason this can't be
That way the setter is not needed.
Comment #24
GoZ CreditAttribution: GoZ at Barbe-Rousse commentedReroll patch applying #23 suggestion.
Comment #26
jofitz CreditAttribution: jofitz at ComputerMinds commentedNeeds work, but no longer needs a reroll.
Comment #27
jofitz CreditAttribution: jofitz at ComputerMinds commentedIn answer to @alexpott's question in #23, if
FieldItemDataDefinition::create($this)
is used then $this->itemDefinition->definition['settings'] is not set (it is withclone $this->itemDefinition
) so I have re-rolled #20.Comment #28
hchonovI've just came across this bug. The problem is not only in the comment module but in the field API itself, so I am setting the component to the field system. I've added one additional test, which is independent of the comment module.
By cloning the item definition only in BaseFieldDefinition we are covering only the cloning of base field definitions objects, but we have to cover the cloning of the parent class as well. I've moved this piece of code to the parent class.
I've also created a new interface covering the two
FieldItemDataDefinition
methods -getFieldDefinition()
andsetFieldDefinition()
and marked properly thesetFieldDefinition()
method as internal.I am also uploading a test only patch.
Comment #29
hchonovBy mistake I've uploaded a not entirely clean test only patch and left some part of the fix inside, therefore I am uploading the test only and the full patch again.
Comment #32
kristiaanvandeneyndeMakes sense, back to RTBC. Perhaps a committer can fix the minor nitpick mentioned in #21?
Comment #33
larowlananother nit: wraps too early here. (can be fixed on commit)
Comment #34
BerdirWondering if we should expliictly re-define the itemDefinition property in this class with the type it expectedly has here, or maybe even throw an exception if it doesn't match, to have a better error if someone did something weird?
This class also has a propertyDefinitions property. Not sure if we should also handle that here, but just unsetting that might make sense as it is basically just a static cache and the property definitions might also depend on the item definition.
Other than that this looks OK.
Comment #35
hchonovI've just updated it in the new patch.
What do you mean by "something weird"? :)
Although I don't think that this is dangerous I think we should take care of this as well. As we are defining proper cloning of the class BaseFieldDefinition I think it is justified to cover it in this issue, therefore I've included it in the new patch.
Comment #36
kristiaanvandeneyndeLooks good! Patch is functional so back to RTBC.
Re #34
Isn't that already taken care of by the setter/getter typehints? The only thing I can think of is that one would assign another BaseFieldDefinition to the property, even though the property is seemingly used to refer to a singular definition, not a list definition.
Not sure I can follow otherwise, the whole DataDefinition>ListDataDefinition>BaseFieldDefinition is a bit obscure to me. I.e.: I haven't really looked into the inner workings yet. If you're sure this patch needs the change you proposed, perhaps you can show us what you meant by it Berdir?
Comment #37
BerdirWhat I meant is that someone could in theory have a subclass of BaseFieldDefinition that has an itemDefinition property that is *not* implementing the new interface. I think that's highly unlikely and protected properties are AFAIK also kind of excluded from BC, not sure.
PhpStorm currently also complains that setFieldDefinition() does not exist in DataDefinitionInterface when you look at the __clone() method.
That is why we coul do this:
To at least document that we expect the item definition to be an instance of that. Or be even more strict and throw an exception if it is not, instead of letting it fatal.
I think the exception is probably overkill for a protected property but adding the property would be kind of useful?
Comment #38
kristiaanvandeneyndeOh right, because we are calling setFieldDefinition() from BaseFieldDefinition and that method is only set on the new FieldItemDataDefinitionInterface interface. Yeah we should make clear that the BFD expects a FieldItemDataDefinitionInterface. When using BFD::create() it always will be, but you never know when someone might create one through the constructor.
Good catch!
Comment #39
hchonovHmm I am not really sure about this. I mean I like the idea with the typyhinting, but if we do this here then others might argue that we have to do it at each place where we extend from a class and expect different interfaces for properties or return values e.g. following this logic we have to redefine the method EntityStorageInterface::load() in the ContentEntityStorageInterface so that it returns a value of the type ContentEntityInterface.
Comment #40
kristiaanvandeneyndeAlso a valid point. I'll put it back to RTBC seeing as BaseFieldDefinition is actually quite safe in a sense that its create() method makes sure we are dealing with a FieldItemDataDefinition. And given how we don't do the same typehinting elsewhere (see #39)...
Comment #44
catchCommitted 4f24c91 and pushed to 8.5.x. Thanks!
Not at all sure about an 8.4.x backport given the new interface etc., if this is blocking a contrib module we might have to make the new interface @internal or something. Moving straight to fixed but re-open if you really think this is needed.
Comment #45
kristiaanvandeneynde@catch Group 8 currently uses the ugly workaround shown in the issue summary. Once this patch is "live", I can actually replace all of that code with a simple clone call. So while it doesn't necessarily need to be backported, it would help me remove some slow, ugly code from Group some three to six months earlier than 8.5.0
Comment #46
bojanz CreditAttribution: bojanz at Centarro commentedThis also affects Commerce, I'd love a core fix instead of having to copy Group's workaround.
Comment #47
kristiaanvandeneynde@bojanz I believe it was fixed in core (8.5.x) by the commit in #42 :)
So the code in Comment (the entity) that used to break will now properly function:
Which is why I'd love to see this in 8.4.x as well so Group and Commerce can already have the concise version when 8.4.0 lands.