Problem/Motivation

Entity::getTypedData() instantiates and caches a typed data adapter wrapping the entity object. However ContentEntityBase introduces the concept of entity translation object, but still relies on the parent to instantiate a typed data adapter. This implies that the first translation object on which ::getTypedData() is called populates cache and field items no longer can retrieve the translation object they are actually attached to. This breaks the Entity Translation API badly.

Proposed resolution

Reset typed-data cache when instantiating a new translation object.

Remaining tasks

  • Validate the proposed solution
  • Write a patch
  • Review it

User interface changes

None

API changes

None

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

plach’s picture

This feels critical to me but I'll leave it to major until @fago and/or @yched have seen this.

yched’s picture

Sounds bad indeed - just feels a bit weird that no test currently catch this ?

Is it possible to narrow down a simple code snippet that breaks ?

Gábor Hojtsy’s picture

Issue tags: +Needs tests

Definitely needs tests.

rpayanm’s picture

fago’s picture

ouch. Agreed this needs tests and feels like a critical. But then, it has no implications right now so maybe major is enough?

>Is it possible to narrow down a simple code snippet that breaks ?
Yep, was about to ask that as well.

Berdir’s picture

jespermb’s picture

Hi,

I created a patch for ContentEntityBase based on the specification in this issue.

Hope it helps.

jespermb’s picture

Status: Active » Needs review

Set status to needs review.

Status: Needs review » Needs work

The last submitted patch, 7: ContentEntityBase-2414721-7.patch, failed testing.

Berdir’s picture

$this->typedDataByLangcode or so might be a better name?

You will also need to unset it if changes happen, search for places where typedData is currently set to NULL.

plach’s picture

Would it make sense to use the existing class member, although it's defined differently? Maybe we can redeclare it in ContentEntityBase?

jespermb’s picture

Status: Needs work » Needs review
FileSize
1.89 KB

Updated the name to typedDataByLangcode and added resets.

It would properly make sence to keep the typedData name but one could argue that using a different name makes it more clear what to expect and could prevent confusion.

Gábor Hojtsy’s picture

Priority: Major » Critical

@plach and @fago both said critical, so let's update the priority then.

Berdir’s picture

+++ b/core/lib/Drupal/Core/Entity/ContentEntityBase.php
@@ -278,6 +285,17 @@ protected function clearTranslationCache() {
+  public function getTypedData() {
+    if (!isset($this->typedDataByLangcode[$this->activeLangcode])) {
+      $class = \Drupal::typedDataManager()->getDefinition('entity')['class'];
+      $this->typedDataByLangcode[$this->activeLangcode] = $class::createFromEntity($this);
+    }
+    return $this->typedDataByLangcode[$this->activeLangcode];
+  }
+

The question is, can we rewrite this to use $this->language()->getId()? If so, then we could possibly move this implementation up into Entity.

Then we could use $this->typedData (or rename that to byLangcode, I don't care), but we'd only have one property and one place to unset them.

As a matter of fact, even though the implementation is in Entity, it only works for content entities at the moment anyway, because we have no adapter that works for config entities.

Berdir’s picture

See my last comment in #2416923: Does not translate when is an anonymous user. @plach said there that this might fix the problem mentioned there, but I can no longer reproduce that bug on HEAD in my contrib module.

Given that, I think we are still lacking an actual problem caused by this that would qualify this being critical?

larowlan’s picture

yched’s picture

+1 for not having two separate properties in Entity and ContentEntityBase.

Berdir’s picture

Yeah, agreed, but I think we should either try to only have one implementation of getTypedData() then, or if that is not possible for some reason (it should be), then we should keep the structure consistent and make them both an array, with a hardcoded language key in the base class?

yched’s picture

+1 to #18 too :-)

Berdir’s picture

Status: Needs review » Needs work

Setting to needs work then.

jespermb’s picture

Status: Needs work » Needs review
FileSize
1.11 KB

I reworked the patch so Entity->getTypedData now uses a keyed array instead.

Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Entity/Entity.php
    @@ -43,11 +43,11 @@
    +   * @var array
    

    We try use something like \Drupal\Core\TypedData\ComplexDataInterface[] whenever we know what the array contains.

  2. +++ b/core/lib/Drupal/Core/Entity/Entity.php
    @@ -535,11 +535,12 @@ public function toArray() {
    +    $lang_code = $this->language()->getId();
    

    And we usually don't shorten variable names like that, I'd suggest $language_code instead. ($langcode is also pretty common and kind of violates what I just said)

plach’s picture

I think I can provide a failing test: I found this while manually testing a field-based view of translated nodes as an anonymous user.

$langcode is the only form we use (or should use) in core.

jespermb’s picture

FileSize
1.09 KB

I made the changes you suggested.

If you could provide a failing test, I will have a look at it. :)

plach’s picture

Sure, I will try later tonight. Thanks for working on this :)

Berdir’s picture

Assigned: Unassigned » plach
Status: Needs review » Needs work

Setting to needs work for tests :)

plach’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
1.65 KB
2.41 KB
3.5 KB

I found a better way to fix this, I think.

Tests attached.

plach’s picture

Issue summary: View changes

The last submitted patch, 27: et-adapters-2414721-27-test.patch, failed testing.

The last submitted patch, 27: et-adapters-2414721-27.patch, failed testing.

The last submitted patch, 27: et-adapters-2414721-27.patch, failed testing.

The last submitted patch, 27: et-adapters-2414721-27.patch, failed testing.

plach’s picture

That failure is unrelated, tests are green locally.

plach’s picture

Issue tags: +sprint
yched’s picture

Status: Needs review » Reviewed & tested by the community

Latest approach seems right indeed - since the translated entities are cloned, no need to keep arrays of TypedDataadapters keyed by language in each of them.

Looks RTBC to me

alexpott’s picture

What is the relationship between this and

  /**
   * Magic method: Implements a deep clone.
   */
  public function __clone() {
    // Avoid deep-cloning when we are initializing a translation object, since
    // it will represent the same entity, only with a different active language.
    if (!$this->translationInitialize) {
      // The translation is a different object, and needs its own TypedData object.
      $this->typedData = NULL;
      $definitions = $this->getFieldDefinitions();
      foreach ($this->fields as $name => $values) {

in ContentEntityBase... I noticed this hunk in #2227227: FieldTypePluginManager cannot instantiate FieldType plugins, good thing TypedDataManager can instantiate just about anything

plach’s picture

That is covering the case where the entity object is deep-cloned, it's not related with instantiating a translation object, which is instead just an alternative wrapper around the same field objects. However, since in both cases we end-up having a different entity object, in both cases we need a new adapter.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -Needs Drupal 8 critical triage

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

  • alexpott committed 42083a1 on 8.0.x
    Issue #2414721 by jesperjb, plach: EntityAdapter should be instantiated...
plach’s picture

Assigned: plach » Unassigned
Issue tags: -sprint

Awesome, thanks

Gábor Hojtsy’s picture

Yay, thanks all!

Status: Fixed » Closed (fixed)

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