Drupal 11:如何使用预渲染回调在渲染前更改实体视图构建器配置

Drupal 11:如何在渲染前修改实体视图构建器配置

最近,我公司在开发一个基于 Drupal 11 的网站时遇到了一个问题。当时有一个块负责渲染一个实体,以便在页面上显示。

渲染过程本身并无异常,但在这个需求场景下,我公司需要为实体标记添加一些属性,然而实现起来并不简单。最后的解决办法是利用预渲染回调方法中途拦截渲染过程。

鉴于这个过程的复杂性,我公司做了一些笔记,并决定将其整理成文章,分享如何实现同样的操作。在本文中,我们将探讨如何使用视图构建器生成可渲染的实体视图,以及如何在不借助预处理钩子的情况下修改视图模式的属性。这对于 Drupal 开发和 Drupal 模块开发都有一定的参考价值。

一、渲染实体

若要让内容实体做好渲染准备,需使用 entity_type.manager 服务来获取相关实体的正确视图构建器。获取到视图构建器对象后,就可以运用 view() 方法来得到可渲染数组。

例如,假设我们有某种媒体实体,可使用以下代码让该实体做好渲染准备。

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

$mediaView 变量此时会包含使用实体的“默认”视图模式渲染该实体所需的数组。数组的具体内容取决于要渲染的实体,但若是媒体实体,其中会包含实体对象、视图构建器对象、实体的视图模式、主题以及一些缓存信息。

就这个数组本身而言,其包含的信息不足以让我们修改最终渲染内容。例如,若想给图像注入属性,就需要将其注入到一个属性数组中,但在当前层级这个数组并不存在。

若将其作为一个渲染数组(甚至只是其中一部分)返回,媒体项将通过渲染管道完全渲染出来。

虽然这样做可行,但问题在于只能使用现有的视图模式。若想给媒体注入自定义属性,可创建另一个带有更多模板的视图模式,但当需要注入大量自定义属性时,管理起来就会变得十分困难。

常规的解决办法可能是使用预处理钩子,但会失去操作的上下文。预处理钩子会拦截实体渲染过程,但我们无法确定实体的来源,也就难以正确地修改它。

更好的解决办法是使用 #pre_render 属性来改变渲染过程的输出,这样我们能对最终结果有更大的控制权。

二、使用 #pre_render 修改渲染结果

#pre_render 添加到渲染数组中并不难,但它不能单独发挥作用。以下代码展示了如何使用此方法为图像媒体实体项添加高度和宽度属性。

首先,只需将 #pre_render 渲染属性注入到 $mediaView 渲染数组中,并为其提供一个回调函数,在需要时调用该函数执行操作。

在下面的示例中,我们将 #pre_render 属性设置为一个名为 setDimensions 的方法,该方法与设置渲染数组的类在同一个类中。

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

这段代码是用于渲染项的方法的一部分。例如,若在一个块插件中,这段代码会加载并准备好实体以进行渲染,并将其作为数组返回。

由于块是一种相对独立的类,我们将在本文的其余示例中使用它。为了渲染媒体项,可在块的 build() 方法中使用如下代码来生成媒体项的渲染数组。

  
    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 [];
    }
  

关于回调函数需要留意的是,我们需要告知 Drupal 回调函数是有效的,这是系统内置的安全机制。为此,需要实现 Drupal\Core\Security\TrustedCallbackInterface 接口,该接口要求添加一个名为 trustedCallbacks() 的方法。若缺少这些元素,Drupal 将抛出错误并拒绝调用回调方法。

以下是块类的头部,其中在类签名中添加了 TrustedCallbackInterface。

  
    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;

    /**
     * 提供一个页脚信息块。
     */
    #[Block(
      id: "render_entity",
      admin_label: new TranslatableMarkup("Render entity"),
      category: new TranslatableMarkup("Custom Components")
    )]
    class RenderEntity extends BlockBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface {
      // ...
    

trustedCallbacks() 方法只需返回一个包含该类中使用的回调方法的数组。由于我们仅有一个回调方法,所以实现起来非常简单。

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

现在可以构建 setDimensions() 回调方法,该方法接受一个名为 $build 的单一参数,它代表渲染数组的当前状态。

setDimensions() 回调的作用是确保媒体图像项存在,然后将高度和宽度尺寸注入到该项的属性中。

  
    /**
     * 为设置的图像注入尺寸。
     *
     * @param array<mixed> $build
     *   嵌入式媒体的渲染数组。
     *
     * @return array<mixed>
     *   更新后的渲染数组。
     */
    public static function setDimensions(array $build):array {
      if (isset($build['field_media_image'][0])) {
        $build['field_media_image'][0]['#item_attributes']['width'] = 500;
        $build['field_media_image'][0]['#item_attributes']['height'] = 250;
      }
      return $build;
    }
  

这里有个小提示,你可能会看到属性存储为 #item_attributes#attributes,这取决于前端使用的格式化器类型。若只发现 #item_attributes 数组键,正常使用即可。然而,若使用的是响应式图像格式化器,传递给高度和宽度的任何值都将被忽略,而采用配置的响应式图像样式,这是正常行为。若未使用格式化器,数组将包含 #attributes 数组键,使用方式相同。存在哪种属性数组在很大程度上取决于要渲染的实体类型。

最终的 HTML 将是媒体项标准渲染的 <img> 标签,且会包含我们想要注入的属性。

  
    <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.">
  

作为额外内容,我们来看看如何让尺寸更具动态性,因为硬编码的图像尺寸迟早会产生问题。这对于 Drupal 开发和 Drupal 模块开发时的动态配置具有重要意义。

三、让操作更具动态性

我们可以将尺寸添加到生成的渲染数组中,而非将其硬编码到 setDimensions 方法中,这样它会被传递给回调函数。添加到初始渲染数组中的任何以 # 开头的内容,最终都会被传递给回调方法。添加的属性不应干扰渲染过程(除非名称与 Drupal 内部使用的内容冲突)。

延续前面的示例,可通过从原始源中提取图像的尺寸来扩展图像示例。这可以借助 image.factory 服务实现,该服务能使用图像本身的位置创建一个 Image 对象。有了这个 Image 对象,就可以提取关于图像的各种信息,包括图像尺寸。

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

    // 使用 image.factory 服务获取图像
    $imageFactory = \Drupal::service('image.factory');
    $imageService = $imageFactory->get($img['#media']->get('field_media_image')->referencedEntities()[0]->get('uri')->getValue()[0]['value']);

    // 提取图像的高度和宽度
    $height = $imageService->getHeight();
    $width = $imageService->getWidth();

    // 对它们进行一些处理。这里我们将图像缩小到原始大小的 10%。
    $height = $height * 0.1;
    $width = $width * 0.1;

    // 将这些值存储在渲染数组中以供后续使用。
    $img['#height'] = $height;
    $img['#width'] = $width;
  

setDimensions 方法内部,只需获取注入到原始渲染数组中的值,并将它们移动到正确的位置。

  
    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;
    } 
  

现在我们的图像将以原始大小的 10% 进行渲染。

我公司在块中渲染需要特定尺寸的 SVG 图像时,发现这种技术非常有用。这意味着无论原始 SVG 图像有多大,最终的图像总是被限制在正确的尺寸内。对 SVG 图像执行此方法很有意义,因为无法将 SVG 图像通过图像格式化器处理。可以在显示模式中限制 SVG 的大小,但这意味着要为网站上不同的图像设置多个不同的视图模式。

在 Drupal 的 Drupal\media\Plugin\Filter\MediaEmbed 类中可以看到这种方法的使用示例,当媒体项嵌入到 CKEditor 窗口中时,该类使用一个名为 disableContextualLinks 的回调方法来移除渲染数组中的上下文链接部分。

这里还需要注意存在 #post_render 类型的回调。后置渲染回调接受一个标记对象,该对象基本上是渲染管道的完全渲染输出。它们也需要以同样的方式在 trustedCallbacks() 方法中注册。

除非确实有必要,否则我公司倾向于避免使用后置渲染回调,因为它们需要进行字符串操作来修改内容。一般来说,在内容渲染之前进行修改要比渲染之后更容易。在 Drupal 升级和 Drupal 开发的过程中,合理选择预渲染和后置渲染回调方式十分重要。