EntityReferenceFormatterBase's prepareView() and getEntitiesToView() work hand in hand and store internal data in unofficial properties in each item :

- $item->originalEntity contains the entity that was multi-loaded in prepareView()
The name of the property, and the associated code comments, are fairly unclear about why this is used, rather than the usual $this->entity property.

- $item->access is set by getEntitiesToView(), as a static cache for "I already checked 'view' access on that entity, and it's TRUE, no need to check it again", in case the ER field is displayed several times in the request.
But :
It's incomplete, only "view access is TRUE" is preserved, access gets re-computed each time if it's FALSE.
It duplicates EntityAccessControlHandler's own static cache. Two static caches on top of each other is useless and error-prone.

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Task: internal cleanup in EntityReferenceFormatterBase.
Issue priority Normal: internal cleanup / simplification
Disruption None, this is only internal communication between prepareView() and getEntitiesToView(). getEntitiesToView() is the official method to use by ERFormatters, and its result remains unchanged

Comments

larowlan’s picture

+1 this broke der

amateescu’s picture

@larowlan, can you elaborate a bit on what you mean by "this"? :)

larowlan’s picture

The original entity bit

yched’s picture

@larowlan, can you elaborate a bit on what you mean by "der"? :p

yched’s picture

And also, how exactly originalEntity breaks it...

jibran’s picture

yched’s picture

Aw - but then dynamic_entity_reference is its own field type, different from core's entity_reference, and thus cannot use its formatters and widgets (that assume that all entities are of the same entity type).

Although, note to @amateescu : if we had a static ERItemList::loadReferencedEntities(ERItemList[]) that both $items->referencedEntites() and ERFormatterBase::prepareView() used, as we discusssed in #2370703: ER's "autocreate" feature is mostly broken (and untested), then dynamic_entity_reference could simply override that static method, ERFormatterBase would work, and dynamic_entity_reference wouldn't require re-implementing all formatters :-)

jibran’s picture

Category: Task » Bug report
Priority: Normal » Major
Issue tags: +Contributed project blocker
Parent issue: » #2370703: ER's "autocreate" feature is mostly broken (and untested)
Related issues: +#2366093: Unable to set field value programmatically.

It is a bug report as per #1 and contrib blocker so it is a major.

yched’s picture

I don't think the dynamic_entity_reference is #2366093: Unable to set field value programmatically., this issue here is about formatters.

larowlan’s picture

Sorry, yes when the original issue went in (#2346315: Translated entity references not rendered in the entity display language) it made the DER formatter need a similar change - I don't think we should need to set random object properties to get a formatter to work - so agree with the issue space here.

See http://cgit.drupalcode.org/dynamic_entity_reference/commit/?id=5e70ba5 for the DER commit.

amateescu’s picture

Status: Active » Needs review
StatusFileSize
new2.16 KB

Let's try it out then :) Just wondering if we'll get the same errors as #2370703-12: ER's "autocreate" feature is mostly broken (and untested) or more.

jibran’s picture

sorry for the noise :)

Status: Needs review » Needs work

The last submitted patch, 11: 2374019.patch, failed testing.

jibran’s picture

yched’s picture

That's a lot of fails, but would still be nice IMO :-)

Status: Needs work » Needs review

jibran queued 11: 2374019.patch for re-testing.

Status: Needs review » Needs work

The last submitted patch, 11: 2374019.patch, failed testing.

jibran’s picture

Assigned: Unassigned » jibran

I'll try to look at some fails. If I can wrap my head around it.

yched’s picture

Awesome, thanks @jibran !

jibran’s picture

Status: Needs work » Needs review
StatusFileSize
new1.15 KB
new2.17 KB

Let's see how much this fix. I have reverted the access change just to check the fails.

Status: Needs review » Needs work

The last submitted patch, 21: 2374019-21.patch, failed testing.

jibran’s picture

Status: Needs work » Needs review
StatusFileSize
new6.33 KB
new7.51 KB

Some test fixes. Interdiff is against #11.

Status: Needs review » Needs work

The last submitted patch, 23: 2374019-23.patch, failed testing.

jibran’s picture

Status: Needs work » Needs review
StatusFileSize
new711 bytes
new7.52 KB

Here is the final fix hopefully green.

jibran’s picture

+++ b/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceFormatterBase.php
@@ -37,7 +37,7 @@ protected function getEntitiesToView(FieldItemListInterface $items) {
-      if ($entity->access('view')) {
+      if ($entity && $entity->access('view')) {

While viewing host entity after deleting referenced entity I was getting non-object error hence this fix. We are still setting $entity = $item->entity; which is NULL in this case. It fixes the issue but is it a correct fix? IMO EntityReferenceFormatterBase::prepareView() should not set it. And we don't have tests for this case in core luckily EntityCacheTagsTestBase tests this case.

Status: Needs review » Needs work

The last submitted patch, 25: 2374019-25.patch, failed testing.

jibran’s picture

Assigned: jibran » Unassigned
Status: Needs work » Needs review
StatusFileSize
new2.82 KB
new10.35 KB

This is a green patch. I want to say good night but it is almost noon here.

yched’s picture

Assigned: Unassigned » amateescu

Yay ! Thanks @jibran !

The need to explicitely grant "access content" to anon users in tests sure is a bit tedious. Wondering how we could make that less painful.

Other than that, what do you think, @amateescu ?

jibran’s picture

The need to explicitely grant "access content" to anon users in tests sure is a bit tedious. Wondering how we could make that less painful.

We can create a trait for that.

amateescu’s picture

Assigned: amateescu » Unassigned
Issue tags: -Entity Reference, -Contributed project blocker

Not sure a trait with a single method will be very useful here, maybe just a helper method (with $roles and $permissions parameters) on our base test classes.

About these custom properties.. the patch in #11 was written just from curiosity, I'm sorry if it was seen as an approval of the issue scope :)

Now that I'm giving it more than a 2 second thought, I think that removing 'access' is probably fine. It is used mainly to simplify tests but also a static cache for access checking at render time, if the same ER field item is render multiple times. That static cache can be useful but I guess we can move it to a better place like the entity access handler, if it doesn't have one already.

However, 'originalEntity' was added with a very specific purpose in #2346315-23: Translated entity references not rendered in the entity display language. Since EntityReferenceFormatterBase::getEntitiesToView() returns potentially translated entities, I think it's necessary (or very useful?) to keep at hand the original (untranslated) entity too.

jibran’s picture

Thanks @yched and @amateescu for the review. So is it still a NR, NW or RTBC?

Not sure a trait with a single method will be very useful here, maybe just a helper method (with $roles and $permissions parameters) on our base test classes.

EntityReferenceAutoCreateTest extends WebTestBase
EntityReferenceFieldTranslatedReferenceViewTest extends WebTestBase
EntityReferenceFormatterTest extends EntityUnitTestBase
EntityReferenceRdfaTest extends EntityUnitTestBase
EntityViewBuilderTest extends EntityUnitTestBase

So in which base class perhaps TestBase?

Now that I'm giving it more than a 2 second thought, I think that removing 'access' is probably fine. It is used mainly to simplify tests but also a static cache for access checking at render time, if the same ER field item is render multiple times. That static cache can be useful but I guess we can move it to a better place like the entity access handler, if it doesn't have one already.

So this contradicts from IS

- getEntitiesToView() sets $item->access = TRUE as a static cache for "I already checked 'view' access on that entity, and it's TRUE, no need to check it again", in case the ER field is displayed several times in the request. However :
It's incomplete, only "view access is TRUE" is preserved, access gets re-computed each time if it's FALSE.
EntityAccessControlHandler already has its own static cache for access checks, so it seems useless and error-prone to have another caching logic somewhere else ?

Can we please have a consensus here?

However, 'originalEntity' was added with a very specific purpose in #2346315-23: Translated entity references not rendered in the entity display language. Since EntityReferenceFormatterBase::getEntitiesToView() returns potentially translated entities, I think it's necessary (or very useful?) to keep at hand the original (untranslated) entity too.

Do you want me to revert that change?

yched’s picture

- about ditching 'access' :
Yeah, AFAICT, EntityAccessControlHandler already does static caching, so no need to add another one ?

- about ditching 'originalEntity' :

[@amateescu] Since EntityReferenceFormatterBase::getEntitiesToView() returns potentially translated entities, I think it's necessary (or very useful?) to keep at hand the original (untranslated) entity too

Not sure I get that - the patch here doesn't change the fact that $this->entity always contain the untranslated entity. The translated entities are only returned by getEntitiesToView(), and not written anywhere in the $item, that still only contains the untranslated one (which is why I don't see why originalEntity is needed to begin with)

jibran’s picture

StatusFileSize
new10.34 KB

Reroll after formatter moved to core.

amateescu’s picture

Not sure I get that - the patch here doesn't change the fact that $this->entity always contain the untranslated entity. The translated entities are only returned by getEntitiesToView(), and not written anywhere in the $item, that still only contains the untranslated one (which is why I don't see why originalEntity is needed to begin with)

Fair enough, I was talking from distant memories so that's why it probably didn't make sense. Now I'm looking at the actual code/patch and I see the following removed code:

+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceFormatterBase.php
@@ -30,26 +30,15 @@ protected function getEntitiesToView(FieldItemListInterface $items) {
-      // The "originalEntity" property is assigned in self::prepareView() and
-      // its absence means that the referenced entity was neither found in the
-      // persistent storage nor is it a new entity (e.g. from "autocreate").
-      if (!isset($item->originalEntity)) {
-        $item->access = FALSE;
-        continue;
-      }

This comment looks to me quite explanatory on why EntityReferenceFormatterBase::prepareView() sets a custom property (originalEntity). Basically, prepareView() has to be performant and handle multiple-loading of referenced entities, but it should not unset any $item for which the referenced entity is not available anymore, so it has to signal this case somehow to the following code paths which have to loop over $items and decide if there's something to render or not.

yched’s picture

Basically, prepareView() has to be performant and handle multiple-loading of referenced entities, but it should not unset any $item for which the referenced entity is not available anymore, so it has to signal this case somehow to the following code paths which have to loop over $items and decide if there's something to render or not

Damn. Good point :-)

For items referencing stale non-existing entities,
prepareView() did not find the entity and did not pre-populate $item->entity,
so getEntitiesToView() accessing $item->entity will re-attempt to load it.

That's more general issue with the auto-loading : if the target id is invalid, all calls to $item->entity will always attempt to load it from the db over and over again.
And we don't have a way to say "give me the entity in $item->entity if it's already there but don't try to load it if it's not there". Something $item->get($property, $trigger_autocompute = FALSE)...

Damn damn damn.

Then yes, maybe prepareView() needs to put the loaded entities in a separate custom property that can simply be NULL, so that getEntitiesToView() can just read it without triggering auto-loading.

That sucks hard, but can't think of better proposal for now :-(
At least, that custom property could be named in a way that's more consistent with the reasons above (preloadedEntity ?), and the corresponding code comments should also more cleanly reflect that.

yched’s picture

Shameless plug - if you're not following it already, #2405469: FileFormatterBase should extend EntityReferenceFormatterBase might interest you folks :-)

jibran’s picture

What is the next step here now?

yched’s picture

So according to #35 / #36, next step would be :
- keep the 'originalEntity' property, but rename it to 'preloadedEntity'
- Update the comment about it to explain that it is only used so that subsequent code doesn't use ->entity, which would attempt to reload from the db for items with target_ids that do not exist anymore.

amateescu’s picture

+1 for #39 :)

yched’s picture

StatusFileSize
new11.89 KB
new3.1 KB

Thinking with #2405469: FileFormatterBase should extend EntityReferenceFormatterBase in mind :

Maybe instead of 'preloadedEntity' we could just add a 'loaded = TRUE' property for the valid entities, and then getEntitiesToView() ignores the items where empty($item->loaded), and can still use the regular "$item->entity" instead of a weird alternate property ?

This is basically the "get $item->entity only if its already there, but do not try to load it if it's not" feature mentioned in #36, but done manually since the current "computed properties" API doesn't provide it.

Attached patch does that. Feels simpler IMO :-)
(interdiff with #34, but reading the patch directly is probably easier)

Status: Needs review » Needs work

The last submitted patch, 41: 2374019-ERFormatter_custom_properties-41.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new11.89 KB
new1.08 KB

Note to self : stop being a smart ass and quickly wrapping up patches in a dummy text editor.

Also :

+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/EntityReferenceFormatterBase.php
@@ -71,6 +64,11 @@ public function prepareView(array $entities_items) {
+        // (...) All items are initialized at FALSE.
+        $item->loaded = TRUE;

LOL / facepalm

amateescu’s picture

+        // (...) All items are initialized at FALSE.
+        $item->loaded = TRUE;

I just submitted the same but you were faster :D

amateescu’s picture

Also, this means we can change the tests to set ->loaded = TRUE instead of all the things we're changing now?

yched’s picture

Also, this means we can change the tests to set ->loaded = TRUE instead of all the things we're changing now?

Hmmm, not currently : ->loaded = TRUE means we still need to check access().

Not sure if we really need to switch the translation *before* checking access(), BTW. Can entity access be granted or denied depending on the language the entity is in ?

Status: Needs review » Needs work

The last submitted patch, 43: 2374019-ERFormatter_custom_properties-43.patch, failed testing.

amateescu’s picture

Not sure if we really need to switch the translation *before* checking access(), BTW. Can entity access be granted or denied depending on the language the entity is in ?

I have exactly 0 clues about that :/ I'd say to just make sure we're doing it in the same order as HEAD.

yched’s picture

Status: Needs work » Needs review

#43 came back green.

yched’s picture

StatusFileSize
new11.9 KB
new1.67 KB

Rather use a _ prefix for an internal, non official property ?

amateescu’s picture

Status: Needs review » Reviewed & tested by the community

No opinion on the _ prefix so the patch looks ready to go.

amateescu’s picture

Can entity access be granted or denied depending on the language the entity is in ?

I just spoke to @plach in IRC about this and the answer is yes. So we're doing it right by checking access after getting the translation.

yched’s picture

So that still leaves the tedious need to explicitely setup "anon users have 'access content' perm" in a lot of tests (#29 / #30 / #31).

It feels a bit absurd to have to do that in WebTests, isn't it the case by default ? It is the case in standard.profile, less sure about testing.profile, since I can't seem to find how standard does it :-)
in current HEAD, ResponsiveImageFieldDisplayTest, for example, has to explicitely *remove* the perm...

I guess it's more tricky in KernelTests (and of course UnitTests, but that's not really an issue there anyway).

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs issue summary update

It's not entirely clear what bug is being fixed from the issue summary. Also can the beta evaluation be added. Thanks.

/**
 * Implements hook_install().
 */
function node_install() {
  // Enable default permissions for system roles.
  // IMPORTANT: Modules SHOULD NOT automatically grant any user role access
  // permissions in hook_install().
  // However, the 'access content' permission is a very special case, since
  // there is hardly a point in installing the Node module without granting
  // these permissions. Doing so also allows tests to continue to operate as
  // expected without first having to manually grant these default permissions.
  if (\Drupal::moduleHandler()->moduleExists('user')) {
    user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access content'));
    user_role_grant_permissions(DRUPAL_AUTHENTICATED_RID, array('access content'));
  }

That's how standard does it :)

+++ b/core/modules/entity_reference/src/Tests/EntityReferenceAutoCreateTest.php
@@ -36,6 +37,10 @@ class EntityReferenceAutoCreateTest extends WebTestBase {
+    $user_role = Role::load(DRUPAL_ANONYMOUS_RID);
+    $user_role->grantPermission('access content');
+    $user_role->save();

So I'm not sure why this is necessary since node is being installed. I think we need more investigation.

yched’s picture

Category: Bug report » Task
Priority: Major » Normal
Issue summary: View changes

Right, this got recategorized as a major bug in #8, but that aspect was clear in #9 / #10.

This is a normal internal cleanup task - although it will make the critical #2405469: FileFormatterBase should extend EntityReferenceFormatterBase easier to work on.
Clarified the IS, and added the beta evaluation.

Will try to investigate the Test stuff a bit more.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new8.18 KB
new7.51 KB

OK, so :

- Looks like the changes about 'access content' in the couple WebTests were not needed, they still pass locally if I revert the changes.

- For the other tests using entity_test entities, it seems it would make sense for entity_test.module to do the same as node_install() does (thanks @alexpott for the hint in #55) : grant the 'view test entity' perm to ANON and AUTH roles by default, and have tests explicitely revoke it if that's what they want to test.
Patch does that, let's see what fails.

Status: Needs review » Needs work

The last submitted patch, 57: 2374019-ERFormatter_custom_properties-57.patch, failed testing.

yched’s picture

Status: Needs work » Needs review
StatusFileSize
new9 KB
new6.55 KB

it seems it would make sense for entity_test.module to grant the 'view test entity' perm to ANON and AUTH roles by default

OK never mind, that's far beyond the scope of this issue. There are a ton of tests out there that currently have to manually grant 'view test entity', this patch just adds a couple more. We'll live.

This mostly reverts #57. For clarity, interdiff is with the previously RTBCed patch in #51.

jibran’s picture

Status: Needs review » Reviewed & tested by the community

I agree this is test code let's not dwell about it.

+++ b/core/modules/field/src/Tests/EntityReference/EntityReferenceFormatterTest.php
@@ -119,6 +125,11 @@
+    // Revoke the 'view test entity' permission for this test.
+    Role::load(DRUPAL_ANONYMOUS_RID)
+      ->revokePermission('view test entity')
+      ->save();

This is a cleaver change. I like it.

  • alexpott committed 6d75fd5 on 8.0.x
    Issue #2374019 by yched, jibran, amateescu: Cleanup the use of custom...
alexpott’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -Needs issue summary update

It duplicates EntityAccessControlHandler's own static cache. Two static caches on top of each other is useless and error-prone.

I'm committing this under the beta fragility maintainer discretion proviso. Committed 6d75fd5 and pushed to 8.0.x. Thanks!

Status: Fixed » Closed (fixed)

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