Drupal 11: How To Alter Entity View Builder Configuration Before Rendering

I encountered an issue on a Drupal 11 site recently where I had a block that was rendering an entity to display on a page.

There was nothing unusual about what was going on in the rendering process, but in this case I needed to add some attributes to the entity markup and I found that this process wasn't that simple. The solution was to intercept the rendering process half way through using a pre-rendering callback method.

As it wasn't that simple I took some notes and decided to convert them into an article to show how to do the same. In this article we will look at using the view builder to generate a renderable view of an entity and then look at how to alter the attributes of the view mode without using a preprocess hook.

Rendering An Entity

To get a content entity ready for rendering we need to use the entity_type.manager service to get the correct view builder for the entity in question. Once we have the view builder object we can use the view() method to get our renderable array.

For example, assuming that we have a Media entity of some sort, we get that entity ready for rendering using the following.

$viewBuilder = \Drupal::service('entity_type.manager')->getViewBuilder('media');
$mediaView = $viewBuilder->view($media, 'default');

The $mediaView variable will now contain the array needed to render the entity using the "default" view mode for the entity. The contents of the array depends on what you are rendering, but if its a media entity then the entity object will be present, along with the view builder object, the view mode and theme of the entity, and some cache information. 

In itself, this array doesn't contain enough information yet to allow us to alter certain things about the final render. For example, if we wanted to inject attributes into a image as then we would need to inject them into an attributes array, but this array doesn't exist at this level.

If we return this as a render array (or even part of one) then the media item will be rendered fully through the render pipeline.

Whilst this works well, the issue here is that we are stuck with this view mode. If we want to inject custom attributes into the media then we could create another view mode with more templates, but that quickly becomes difficult to manage if we have lots of custom properties to inject.

The solution to this might be to use a preprocess hook, but the issue with that is that we lose the context of what we were doing. The preprocess hook will intercept the entity rendering process, but we won't be sure where the entity came from so it might be difficult to alter it in the correct way.

The solution to this is to use the #pre_render attribute to change the output of the rendering process, which gives us greater control over the final product.

Using #pre_render To Alter The Render

Using #pre_render is pretty simple to add to our render array, but it doesn't work in isolation. The following code will show how to add height and width attributes to an image Media entity item using this method.

To get started we just need to inject the #pre_render render attribute into the $mediaView render array, giving it a callback function that it can call to perform the action.

In the below example we are setting the #pre_render attribute to be a method called setDimensions, which exists in the same class as we are setting up our render array.

$viewBuilder = \Drupal::service('entity_type.manager')->getViewBuilder('media');
$mediaView = $viewBuilder->view($media, 'default');
$mediaView['#pre_render'][] = static::class . '::setDimensions';

This code would be part of a method used to render items, for example, if we were in a block plugin then the code would load and prepare the entity ready for rendering and return this as an array.

Since a block is a prett self contained sort of class we will use this for the rest of the examples in this article. To get our media item rendered we could use something like the following to generate a render array for a media item in a block build() method.

public function build():array {
  $mediaId = 123;
  $media = $this->entityTypeManager->getStorage('media')
      ->load(mediaId);
  if ($media) {
    $view_builder = $this->entityTypeManager->getViewBuilder('media');
    $mediaView = $view_builder->view($media, 'default');
    $mediaView['#pre_render'][] = static::class . '::setDimensions';
    return $mediaView;
  }

  return [];
}

One thing to note about callbacks is that we need to inform Drupal that our callbacks are valid, which is a security mechanism built into the system. To do this we need to implement the interface Drupal\Core\Security\TrustedCallbackInterface, which requires a method called trustedCallbacks() to be added. Without these elements Drupal will throw an error and refuse to call the callback method.

Here is the heading of the block class, with the TrustedCallbackInterface added to the class signature.

namespace Drupal\my_module\Plugin\Block;

use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides a footer information block.
 */
#[Block(
  id: "render_entity",
  admin_label: new TranslatableMarkup("Render entity"),
  category: new TranslatableMarkup("Custom Components")
)]
class RenderEntity extends BlockBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface {
  // ...

The trustedCallbacks() method just needs to return an array that contains the callback methods used in this class. Since we only have one our implementation is pretty simple.

  /**
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return ['setDimensions'];
  }

We can now build the setDimentions() callback method, which accepts a single parameter called $build. This is the current state of the render array.

The setDimentions() callback just needs to make sure that the media image item exists before injecting height and width dimentions into the item attributes.

  /**
   * Injects dimensions into the set image.
   *
   * @param array<mixed> $build
   *   The render array for the embedded media.
   *
   * @return array<mixed>
   *   The updated render array.
   */
  public static function setDimensions(array $build):array {
    if (isset($build['field_media_image'][0])) {
      $build['field_media_svg'][0]['#item_attributes']['width'] = 500;
      $build['field_media_svg'][0]['#item_attributes']['height'] = 250;
    }
    return $build;
  }

A small note here to say that you might see the attributes stored as #item_attributes and #attributes, and it will depend on the type of formatter in use on the front end. If you find only the #item_attributes array key there then use this as normal. If, however, you are using the responsive image formatter here then any values passed to height and width will be ignored in favor of the responsive image styles that you have configured, which is the correct behavior. If you aren't using a formatter then the array will contain the array key #attributes, which you can use in the same way. What sort of attributes array exists will depending largely on what sort of entity you are trying to render.

The resulting HTML will be the standard rendered <img> tag from the media item, but it will also include the attributes that we want to inject.

<img loading="lazy" width="500" height="250" src="/sites/default/files/styles/wide/public/2025-12/20241119_093540.jpg?h=101e5a8c&amp;itok=ZC8g1gD4" alt="An example image.">

As a bonus, let's look at making the dimensions a little bit more dynamic, since hard coded image sizes are bound to come off the rails at some point.

Making Things More Dynamic

Instead of hard coding the dimentions into the setDimensions method we can instead add them to the generated render array so that it's passed to the callback. Anything we add to the initial render array (as long as it's prefixed with a #) will eventually be passed to the callback method. The attributes we add shouldn't interfere with the rendering process (unless you've named them to clash with something Drupal uses internally).

To follow on from the previous examples, we can expand on the image example by extracting the dimentions of the image from the original source. This is possible using the image.factory service, which can create a Image object using the location of the image itself. Once we have this Image object we can extract all sorts of information about the image, including our image dimentions. 

$view_builder = $this->entityTypeManager->getViewBuilder('media');
$mediaView = $view_builder->view($media, 'default');
$mediaView['#pre_render'][] = static::class . '::setDimensions';

// Use the image.factory service to get an image 
$imageFactory = \Drupal::service('image.factory');
$imageService = $imageFactory->get($img['#media']->get('field_media_image')->referencedEntities()[0]->get('uri')->getValue()[0]['value']);

// Extrac the height and weight of the image.
$height = $imageService->getHeight();
$width = $imageService->getWidth();

// Perform some manipulation on them. Here we are making the image 10% of the original size.
$height = $height * 0.1;
$width = $width * 0.1;

// Store these values in the render array for later use.
$img['#height'] = $height;
$img['#width'] = $width;

Inside the setDimensions method we just need to pick up the values we injected into the original render array and move them to the correct location.

public static function setDimensions(array $build):array {
  if (isset($build['field_media_image'][0])) {
    $build['field_media_image'][0]['#item_attributes']['width'] = $build['#width'];
    $build['field_media_image'][0]['#item_attributes']['height'] = $build['#height'];
  }
  return $build;
} 

Our image will now be rendered at 10% of the original size.

I found this technique useful when I was rendering SVG images that needed to be a very specific size in a block. This meant that now matter how large the original SVG image was, the resulting image was always contrained to the correct dimensions. Performing this method with SVG images made a lot of sense since it isn't possible to pass an SVG image through a image formatter. It is possible to restrict the size of an SVG in the display mode, but that would have meant having multiple different view modes for different images dotted around the site.

An example of this method in use can be seen in Drupal the class Drupal\media\Plugin\Filter\MediaEmbed, which uses a callback method called disableContextualLinks to remove the contextual links section of the render array when the media item is embedded in CKEditor windows.

I should also note here that the callback type #post_render exists. The post-render callback accepts a markup object that is basically the fully rendered output of the render pipelines. They also need to be registered in the same way with the trustedCallbacks() method.

I tend to avoid using post-render callbacks unless I really need to as they require string manipulations to alter the content. As general rule, it is easier to manipulate the content before it is rendered, rather than after.

Comments

Dear Phil Nortan,

        Please let me know  where can I get the documentation for following code block ? as I am getting  parsing error (missing semicolon)for the next . If you can have the git repository path then well in good. 

#[Block(
  id: "render_entity",
  admin_label: new TranslatableMarkup("Render entity"),
  category: new TranslatableMarkup("Custom Components")
)]

Thanks and Regards,

Amit Tarkar

Permalink

Hi Amit, thanks for commenting.

I'm not sure what the problem is there. Maybe your're missing a semi colon from the use statements at the top of the class definition? PHP (well, all end of line escaped languages really) has a habit of pushing semi-colon errors forwards.

Name
Philip Norton
Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.