Question

Since Drupal 8.0.0-beta12 "all rendering must happen in a render context", as stated by this change record.

This is to avoid bubbleable metadata lost. So far, so good.

Let's say I'm implementing a Commerce payment notification. The important part is a server calls a Drupal URL to notify something. The controller, as far as I know, doesn't need any rendering context because the answer is simple (almost just a status response). Good.

Let's say I want to send an email on this event. I create an EventSubscriber that subscribes to the payment event that happens on this callback. I hook on the event, gather the email data, and call the send function. During sending, Drupal renders the content (is an HTML email!) and I got the error: since there's no rendering context Drupal throws an exception. The point the error is triggered is when Token::replace() is called:

$body = $this->token->replace($this->getBody(), $this->getTokenData());

How to fix this?

Option 1: Use the BubbleableMetadata $bubbleable_metadata fourth param of Token::replace(). This param docs states:

An object to which static::generate() and the hooks and functions that it invokes will add their required bubbleable metadata.

To ensure that the metadata associated with the token replacements gets attached to the same render array that contains the token-replaced text, callers of this method are encouraged to pass in a BubbleableMetadata object and apply it to the corresponding render array.

But because $body is just a plain string I guess the bubbleable metadata is lost, so what's the point of caring?

Option 2: Follow the change record suggestion and use RenderContext::executeInRenderContext(). In that case I'd wrap the whole process:

$renderer = \Drupal::service('renderer');
$context = new RenderContext();
$that = $this;
$response = $renderer->executeInRenderContext($context, function() use ($that, $recipient) {
  $that->mailer->send($recipient);
  });

Again, I'm discarding the cacheable metadata.

I guess discarding that cacheable data is ok because the email is totally uncacheable, the whole body and its parts (because the replaced strings with tokens have URLs that would change on every email).

Are both approaches ok? Is one better than the other? Is there a third approach more suitable for this problem? I guess the problem can be summarized as how to deal with Bubbleable Metadata when there's no rendering context and no render array.

Was it helpful?

Solution

Yes, when rendering email data you have to take care that you don't bubble up metadata to the render context of the main response.

There is a third option, and in most cases the best option: The method Renderer::renderPlain, which not only discards bubbleable metadata, but also makes sure your email doesn't contain leftover placeholders:

Renders final HTML in situations where no assets are needed.

Calls ::render() in such a way that placeholders are replaced.

Useful for instance when rendering the values of tokens or emails, which need a render array being turned into a string, but do not need any of the bubbleable metadata (the attached assets and cache tags).

Some of these are a relatively common use case and happen within a ::renderRoot() call, but that is generally highly problematic (and hence an exception is thrown when a ::renderRoot() call happens within another ::renderRoot() call). However, in this case, we only care about the output, not about the bubbling. Hence this uses a separate render context, to not affect the parent ::renderRoot() call.

(Can be executed within another render context: it runs in isolation.)

And it's always a good idea to build a render array to produce HTML. For example if you have a formatted text field then build a processed text render element: How to safely render node body on a custom variable?

Licensed under: CC-BY-SA with attribution
Not affiliated with drupal.stackexchange
scroll top