#2405469: FileFormatterBase should extend EntityReferenceFormatterBase has a case where ERFormatter needs to assign a custom runtime property on the entity being referenced, containing the ERItem that references it :
$entity->_referringItem = $item

The problem is that ContentEntityBase::__set($name, $value) unconditionally downcasts $value to $value->getValue() before actually assigning it if $value happens to be a TypedData.
--> I assign a FieldItem, but what consuming code reads back is an array.

Downcasting to ->getValue() makes sense if the value is going to be assigned to a FieldItemList that will "re-upcast" it - i.e $name is the name of a "Field API field". For non-Field properties, we should just assign the value as we receive it.

Comments

yched’s picture

Status: Active » Needs review
StatusFileSize
new2.53 KB
new4.53 KB

Patch, with test.

The last submitted patch, 1: 2426509-ContentEntity_set_TypedData-1-test-only.patch, failed testing.

yched’s picture

Test-only patch failed as it should,
Patch passed as it should.

Up for reviews :-)

jibran’s picture

Patch look good to me but can't RTBC this is way over my head.

fago’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -802,25 +802,27 @@ public function &__get($name) {
    +    if ($this->hasField($name)) {
    

    I'm not sure on much this magic method is really called, but this adds at least two method calls to every invocation. Should we check this isn't adding a performance regression?

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -802,25 +802,27 @@ public function &__get($name) {
    +      if ($value instanceof TypedDataInterface && !$value instanceof EntityInterface) {
    

    Alternatively, could we just support down-casting objects of FieldItemListInterface ? I don't think we have to support something else, this just makes sure we can do:
    $node->title = $node2->title;

    I've no idea while there is the check for EntityInterface here in the first place.

yched’s picture

StatusFileSize
new5.05 KB
new1.19 KB

Thanks for the review!

1) Agreed, that was actually on my mental todo list here :-)
__set() is the mirror of __get(), and that one has does an "Inline getFieldDefinition for speed" trick, so it makes sense to reproduce that in __set() too.
Due to the "Support setting values via TypedData objects" part, which is specific to __set(), we can't have the exact same logic order between the two functions - no biggie.

2) "downcast any TypedDataInterface that isn't also an EntityInterface" : honestly, that puzzled me too :-) It's not completely clear to me what we want to explicitely support now.

I think I'd agree with "only downcast FieldItemListInterface objects that get passed for assignment". That way, we support copying values, with:

$entity->field = $other_entity->field

but not anything weirder. Anything else gets passed as is to ItemList->setValue, and if not an array, gets assigned to the first property of the first item.

But then, we have similar downcasting at the lower levels :

- in ItemList::offsetSet()/set(), which supports copying item values ($e->field[0] = $e->field[1]). It only downcasts if TypedDataInterface, because there's no generic interface for "a ListItem". Should FieldItemList override that to only downcast if FieldItemInterface ?

- in FieldItemBase::__set() : same logic as on ContentEntity::__set() : downcast if (TypedDataInterface && !EntityInterface)
I don't clearly get what this one is for, nor what it allows or forbids ? If still relevant, then just like what we're fixing in ContentEntity, shouldn't this only apply to "officially defined properties in the Item" ?
(also, the comment there explicitely mentions that the EntityInterface check is there because "the 'entity' property (of EntityRef fields) is typed data also" - which is no longer the case ;-)

So... patch only takes care of point 1) for now, not sure what we want to do about 2) in ContentEntityBase::__set(), if we want to adjust the lower levels accordingly, and if we do it here or in a separate issue.

yched’s picture

Issue tags: +blocker

Also, since this blocks #2405469: FileFormatterBase should extend EntityReferenceFormatterBase which is critical, adding the tag

effulgentsia’s picture

Priority: Normal » Critical

If #7 is true, raising this priority to match that one.

wim leers’s picture

So… not sure what's left to be done here? fago needs to review/RTBC?

yched’s picture

Yeah, so IMO:
- the blocker/critical fix here should be limited to the scope of patch #6.
- the side topic discussed #5.2 and #6.2 definitely raise interesting flags, but it's better off in a dedicated issue.

Would be nicer to have @fago's approval, but I honestly don't think committing patch #6 would be risky or constroversial either :-)

larowlan’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -802,25 +802,31 @@ public function &__get($name) {
    +      // If a FieldItemList object already exists, set its value.
    +      if (isset($this->fields[$name][$this->activeLangcode])) {
    +        $this->fields[$name][$this->activeLangcode]->setValue($value);
    +      }
    

    would putting this before the

    if (!isset($this->fieldDefinitions)) {
    		+ $this->getFieldDefinitions();
    		}
    

    result in any speed up? Would that mirror the existing code more and save the lookup?

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -802,25 +802,31 @@ public function &__get($name) {
         // The translations array is unset when cloning the entity object, we just
         // need to restore it.
         elseif ($name == 'translations') {
           $this->translations = $value;
         }
    

    out of scope but this could go first in the function and then we could exit early

yched’s picture

re #11

1. That would require us to duplicate the '// Support setting values via property objects." code block, I don't think that's worth the micro-perf gain

2. I think getting / setting field values happens much more often that getting / setting the 'translations' property ?

larowlan’s picture

+1 rtbc then

wim leers’s picture

Status: Needs review » Reviewed & tested by the community

Per #10 & #13.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
    @@ -780,7 +780,7 @@ public function &__get($name) {
    -    // Inline getFieldDefinition() to speed up things.
    +    // Inline getFieldDefinition() to speed things up.
    

    lol

  2. +++ b/core/modules/system/src/Tests/Entity/EntityFieldTest.php
    @@ -264,17 +264,22 @@ protected function doTestReadWrite($entity_type) {
    +    $this->assertFalse($entity->name === $entity2->name, format_string('%entity_type: Copying properties results in a different field object.', array('%entity_type' => $entity_type)));
    ...
    +    $this->assertTrue($entity2->_foo === $entity->name, format_string('%entity_type: Copying properties results in a different field object.', array('%entity_type' => $entity_type)));
    

    Isn't the point here that it results in the the same field object?

yched’s picture

Status: Needs work » Reviewed & tested by the community

1. I know, I hate myself for this :-)

2. No, as discussed in #6.2, the intent is not to actually move a FieldItemList *object* across different entities (that would have very weird effects, since a FieldItemList references its parent entity), but just to copy *field values* :

$entity->field = $other_entity->field;
// Actually does :
$entity->field->setValue($other_entity->field->getValue())

That's a very early feature of the "magic-OO Entity Field API" in D8, this patch here doesn't change it.

Temptatively kicking back to @alexpott

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/system/src/Tests/Entity/EntityFieldTest.php
@@ -264,17 +264,22 @@ protected function doTestReadWrite($entity_type) {
+    $this->assertFalse($entity->name === $entity2->name, format_string('%entity_type: Copying properties results in a different field object.', array('%entity_type' => $entity_type)));
...
+    $this->assertTrue($entity2->_foo === $entity->name, format_string('%entity_type: Copying properties results in a different field object.', array('%entity_type' => $entity_type)));

Testing for difference using === is super weird to me.

yched’s picture

You mean you'd perefer spl_object_hash() ?

Also, note that === is what the test currently uses in HEAD : $this->assertTrue($entity->name !== $entity2->name)
I'm just inverting the boolean logic to make it symmetrical with the test I'm adding for the behavior on non-field property, but using === for object identity is not introduced by this patch :-)

wim leers’s picture

Status: Needs work » Reviewed & tested by the community

Back to RTBC to get feedback from Alex RE: #18.

fago’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new5.04 KB
new1.75 KB
+++ b/core/modules/system/src/Tests/Entity/EntityFieldTest.php
@@ -264,17 +264,22 @@ protected function doTestReadWrite($entity_type) {
+    $this->assertTrue($entity2->_foo === $entity->name, format_string('%entity_type: Copying properties results in a different field object.', array('%entity_type' => $entity_type)));

I think what's puzzling about the line is that the message assertion is wrong. It's actually checking that a non-field property is not touched, thus is identical. Adjusted it.

if ($value instanceof TypedDataInterface && !$value instanceof EntityInterface) {

Whatever reason introduced this, it does not make sense any more as EntityInterface does not extend TypedDataInterface any more. Thus removed the needless double-check.

Else, patch looks good to me also.

ad #6.2: Yeah, this sounds like a separate issue to me as we should ensure it stays consistent across all levels.

yched’s picture

Good point about the assertion message, unfortunate copy/paste.

About the "instanceof EntityInterface" check : we'll also need to remove it from FieldItemBase::__set(), so I was keeping this for the issue where we unify those downcasts on the three levels, but sure, we can do it here as well :-)

yched’s picture

Status: Needs review » Reviewed & tested by the community

Back to RTBC then, to get feedback from Alex RE: #18

+ opened #2430843: Clarify/unify the downcast logic in ContentEntityBase::__set() / ItemList::set() / FieldItemBase::__set() to figure out the questions raised in #5.2 and #6.2

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue addresses a critical bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 1420b77 and pushed to 8.0.x. Thanks!

  • alexpott committed 1420b77 on 8.0.x
    Issue #2426509 by yched, fago: ContentEntityBase::__set() messes with...

Status: Fixed » Closed (fixed)

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