Problem/Motivation

In \Drupal\entity_embed\EntityHelperTrait::renderEntityEmbed():

    $this->moduleHandler()->alter(array("{$context['data-entity-type']}_embed", 'entity_embed'), $build, $entity, $context);
    $entity_output = $this->renderer()->render($build);

This triggered my instinctive oh, that's interesting, but … does it deal with cacheability metadata correctly? reaction.

Problem 1: the documentation doesn't say anything about cacheability, and how altering this affects also non-embedded rendering

It should say something like:

+ * Note that any alterations made here must update the cacheability metadata in
+ * $build: otherwise alterations made for embedding an entity in a certain view
+ * mode will also affect that entity in that view mode when not embedded.
+ *
+ * It is recommended to NOT use this hook and instead create embedding-specific
+ * view modes.

Because that is the problem: Entity Embed allows you to embed an entity using a certain view mode. Great! But this hook allows you to alter it. And if you do so without modifying #cache[keys], you end up with a race condition:

  1. if entity X in view mode Y is rendered first outside of Entity Embed, then the Entity Embed filter will also render it like that, without the alterations made by this hook, because the CID will be the same
  2. if entity X in view mode Y is rendered first by Entity Embed, then it will also be rendered like that outside of Entity Embed filter, with the alterations made by this hook, because the CID will be the same

Conclusion: it'd be better to not use this hook to alter existing view modes (entity view displays), but instead have site builders create custom view modes/entity view displays. Note that this does not mean you MUST do that. It's only necessary to do so if you want the embedded entities to be rendered differently than one of the existing view modes. So you can still choose to render the teaser view mode, for example.

Problem 2: hook_entity_embed_alter() does not operate on a fully built render array despite it claiming so

So I went to look and found this in entity_embed.api.php:

function hook_entity_embed_alter(array &$build, \Drupal\Core\Entity\EntityInterface $entity, array &$context) {
  // Remove the contextual links.
  if (isset($build['#contextual_links'])) {
    unset($build['#contextual_links']);
  }
}

This cannot work. Contextual links do not exist yet at this stage. So the example is broken.

The documentation says:

  * This hook is called after the content has been assembled in a structured
 * array and may be used for doing processing which requires that the complete
 * block content structure has been built.

This is wrong: the content structure has NOT yet been built at this stage (the whole point is that viewing an entity results in a very thin, very cheap render array, which uses a #pre_render callback to build it *fully*, and which is only called on a cache miss, so that on a cache hit, we do as little work as possible). And also, it's not a block, but an entity — but that's a nitpick :)

Conclusion: First of all this is broken — what this aims to do can only be done using hook_entity_view_alter(), which is called by EntityViewBuilder. But using that hook means you wouldn't be able to know that this entity render array is actually being rendered for embedding purposes, because hook_entity_view_alter() doesn't carry any contextual information. Nor could/should it do so, because this is only invoked after the entity's render array is fully built, and so it only happens on a cache miss, which means the CID was already known, which means modifying it only for the "embed" context would be wrong. So hook_entity_build_defaults_alter() would be better because that'd allow you to alter the cache keys, but there too you don't have that contextual information that this is an entity display being rendered for embedding.
And that is the whole point: given a certain entity display, it is always rendered in the same way. Entity Embed shouldn't allow manipulating that. Entity view modes/entity view displays are specifically designed to allow you to create different ones for different situations. Embedding is a different situation.

Proposed resolution

Either:

  1. Document the limitations.
  2. Remove hook_entity_embed_alter(), recommend creating custom view modes instead.

Remaining tasks

TBD.

User interface changes

None.

API changes

TBD.

Data model changes

None.

CommentFileSizeAuthor
#2 2593315-2-docs.patch1.84 KBwim leers

Comments

Wim Leers created an issue. See original summary.

wim leers’s picture

StatusFileSize
new1.84 KB

Having written the docs patch, I'm increasingly convinced that just removing this is much better. Much clearer: no confusion, no complexity. And steers people in the right direction: it encourages them to use the patterns that even Drupal 7 already had, that Drupal 8 has strengthened. It'd be great to have Entity Embed set the right example.

yched’s picture

The IS sounds like a fair assessment...

wim leers’s picture

The test coverage at \Drupal\entity_embed\Tests\EntityEmbedHooksTest::testEntityEmbedHooks() also did not test this "infection" of the non-embedded version of an entity display.

wim leers’s picture

Status: Active » Needs review
wim leers’s picture

Thanks for confirming, @yched!

dave reid’s picture

Status: Needs review » Needs work

The problem is not everything is going to use custom view modes. We already have several ways to render items that do not include the rendered entity + view mode process, like the entity label, file, or image formatters. Those do not have any points of alteration in the output process, so we provide one. I don't think we should remove this hook, but we should fix it.

wim leers’s picture

You're saying the problem is that we invoke this hook_entity_embed_alter() even when using "regular" view modes/entity view displays? I.e. that this hook should only be invoked for render arrays built by these Entity Embed Display Plugins?

dave reid’s picture

dave reid’s picture

dave reid’s picture

Status: Needs work » Needs review

I think I was confused about both having a patch there and a linked PR. I would support the patch here but have closed the PR, because there are many cases where "the proper" entity view altering hooks are not invoked (the instances I listed: entity label field formatter, file field formatters, image field formatters, which can all be re-used as embed display plugins).

wim leers’s picture

Indeed, which is why I wrote this in #8:

I.e. that this hook should only be invoked for render arrays built by these Entity Embed Display Plugins?

dave reid’s picture

No, I don't think it should be conditional based on which plugin is being used. That shouldn't be our choice to make. We should direct people to the correct hooks when they can, which the patch does, but the users need to know that those hooks will not always be used for other plugins.

dave reid’s picture

Title: Remove hook_entity_embed_alter(): it does not and can not work as advertised, and best practice is to use custom view modes » Improve documentation of hook_entity_embed_alter()
Category: Bug report » Task
wim leers’s picture

Hm… IMHO it's extremely misleading right now. The hook is presented like it works for everything. But it really doesn't.

I think it'd be better if the individual plugins provided alter hooks to allow their stuff to be altered, so the developer implementing that alter hook knows what data structure to expect.

IMO it's better to not offer all the hooks people might ever want to use, and add them later, when it's clear that there's a use case for them, and when it's clear what the constraints are.

It's possible to add hooks. It's impossible to remove hooks.

wim leers’s picture

I wonder what Berdir and slashrsm think about this.

berdir’s picture

I'm not sure. Your argumentation is based on the assumption that everything is a rendered entity. We discussed yesterday that's not the case.

There are valid cases even then. For example, what you could do is *switch* the view mode. Or add a pre_render render callback and do stuff there. And you could alter other embed display plugins in other ways.

I'm not sure how many use cases there are for this, but it seems like a useful thing if we update the documentation to what you can and can't do.

slashrsm’s picture

Status: Needs review » Needs work
wim leers’s picture

Component: Code » Documentation