BlockViewBuilder.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. <?php
  2. namespace Drupal\block;
  3. use Drupal\Core\Block\MainContentBlockPluginInterface;
  4. use Drupal\Core\Block\TitleBlockPluginInterface;
  5. use Drupal\Core\Cache\Cache;
  6. use Drupal\Core\Cache\CacheableMetadata;
  7. use Drupal\Core\Entity\EntityViewBuilder;
  8. use Drupal\Core\Entity\EntityInterface;
  9. use Drupal\Core\Extension\ModuleHandlerInterface;
  10. use Drupal\Core\Plugin\ContextAwarePluginInterface;
  11. use Drupal\Core\Render\Element;
  12. use Drupal\block\Entity\Block;
  13. use Drupal\Core\Security\TrustedCallbackInterface;
  14. /**
  15. * Provides a Block view builder.
  16. */
  17. class BlockViewBuilder extends EntityViewBuilder implements TrustedCallbackInterface {
  18. /**
  19. * {@inheritdoc}
  20. */
  21. public function buildComponents(array &$build, array $entities, array $displays, $view_mode) {
  22. }
  23. /**
  24. * {@inheritdoc}
  25. */
  26. public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
  27. $build = $this->viewMultiple([$entity], $view_mode, $langcode);
  28. return reset($build);
  29. }
  30. /**
  31. * {@inheritdoc}
  32. */
  33. public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) {
  34. /** @var \Drupal\block\BlockInterface[] $entities */
  35. $build = [];
  36. foreach ($entities as $entity) {
  37. $entity_id = $entity->id();
  38. $plugin = $entity->getPlugin();
  39. $cache_tags = Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags());
  40. $cache_tags = Cache::mergeTags($cache_tags, $plugin->getCacheTags());
  41. // Create the render array for the block as a whole.
  42. // @see template_preprocess_block().
  43. $build[$entity_id] = [
  44. '#cache' => [
  45. 'keys' => ['entity_view', 'block', $entity->id()],
  46. 'contexts' => Cache::mergeContexts(
  47. $entity->getCacheContexts(),
  48. $plugin->getCacheContexts()
  49. ),
  50. 'tags' => $cache_tags,
  51. 'max-age' => $plugin->getCacheMaxAge(),
  52. ],
  53. '#weight' => $entity->getWeight(),
  54. ];
  55. // Allow altering of cacheability metadata or setting #create_placeholder.
  56. $this->moduleHandler->alter(['block_build', "block_build_" . $plugin->getBaseId()], $build[$entity_id], $plugin);
  57. if ($plugin instanceof MainContentBlockPluginInterface || $plugin instanceof TitleBlockPluginInterface) {
  58. // Immediately build a #pre_render-able block, since this block cannot
  59. // be built lazily.
  60. $build[$entity_id] += static::buildPreRenderableBlock($entity, $this->moduleHandler());
  61. }
  62. else {
  63. // Assign a #lazy_builder callback, which will generate a #pre_render-
  64. // able block lazily (when necessary).
  65. $build[$entity_id] += [
  66. '#lazy_builder' => [static::class . '::lazyBuilder', [$entity_id, $view_mode, $langcode]],
  67. ];
  68. }
  69. }
  70. return $build;
  71. }
  72. /**
  73. * Builds a #pre_render-able block render array.
  74. *
  75. * @param \Drupal\block\BlockInterface $entity
  76. * A block config entity.
  77. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
  78. * The module handler service.
  79. *
  80. * @return array
  81. * A render array with a #pre_render callback to render the block.
  82. */
  83. protected static function buildPreRenderableBlock($entity, ModuleHandlerInterface $module_handler) {
  84. $plugin = $entity->getPlugin();
  85. $plugin_id = $plugin->getPluginId();
  86. $base_id = $plugin->getBaseId();
  87. $derivative_id = $plugin->getDerivativeId();
  88. $configuration = $plugin->getConfiguration();
  89. // Inject runtime contexts.
  90. if ($plugin instanceof ContextAwarePluginInterface) {
  91. $contexts = \Drupal::service('context.repository')->getRuntimeContexts($plugin->getContextMapping());
  92. \Drupal::service('context.handler')->applyContextMapping($plugin, $contexts);
  93. }
  94. // Create the render array for the block as a whole.
  95. // @see template_preprocess_block().
  96. $build = [
  97. '#theme' => 'block',
  98. '#attributes' => [],
  99. // All blocks get a "Configure block" contextual link.
  100. '#contextual_links' => [
  101. 'block' => [
  102. 'route_parameters' => ['block' => $entity->id()],
  103. ],
  104. ],
  105. '#weight' => $entity->getWeight(),
  106. '#configuration' => $configuration,
  107. '#plugin_id' => $plugin_id,
  108. '#base_plugin_id' => $base_id,
  109. '#derivative_plugin_id' => $derivative_id,
  110. '#id' => $entity->id(),
  111. '#pre_render' => [
  112. static::class . '::preRender',
  113. ],
  114. // Add the entity so that it can be used in the #pre_render method.
  115. '#block' => $entity,
  116. ];
  117. // If an alter hook wants to modify the block contents, it can append
  118. // another #pre_render hook.
  119. $module_handler->alter(['block_view', "block_view_$base_id"], $build, $plugin);
  120. return $build;
  121. }
  122. /**
  123. * {@inheritdoc}
  124. */
  125. public static function trustedCallbacks() {
  126. return ['preRender', 'lazyBuilder'];
  127. }
  128. /**
  129. * #lazy_builder callback; builds a #pre_render-able block.
  130. *
  131. * @param $entity_id
  132. * A block config entity ID.
  133. * @param $view_mode
  134. * The view mode the block is being viewed in.
  135. *
  136. * @return array
  137. * A render array with a #pre_render callback to render the block.
  138. */
  139. public static function lazyBuilder($entity_id, $view_mode) {
  140. return static::buildPreRenderableBlock(Block::load($entity_id), \Drupal::service('module_handler'));
  141. }
  142. /**
  143. * #pre_render callback for building a block.
  144. *
  145. * Renders the content using the provided block plugin, and then:
  146. * - if there is no content, aborts rendering, and makes sure the block won't
  147. * be rendered.
  148. * - if there is content, moves the contextual links from the block content to
  149. * the block itself.
  150. */
  151. public static function preRender($build) {
  152. $content = $build['#block']->getPlugin()->build();
  153. // Remove the block entity from the render array, to ensure that blocks
  154. // can be rendered without the block config entity.
  155. unset($build['#block']);
  156. if ($content !== NULL && !Element::isEmpty($content)) {
  157. // Place the $content returned by the block plugin into a 'content' child
  158. // element, as a way to allow the plugin to have complete control of its
  159. // properties and rendering (for instance, its own #theme) without
  160. // conflicting with the properties used above, or alternate ones used by
  161. // alternate block rendering approaches in contrib (for instance, Panels).
  162. // However, the use of a child element is an implementation detail of this
  163. // particular block rendering approach. Semantically, the content returned
  164. // by the plugin "is the" block, and in particular, #attributes and
  165. // #contextual_links is information about the *entire* block. Therefore,
  166. // we must move these properties from $content and merge them into the
  167. // top-level element.
  168. foreach (['#attributes', '#contextual_links'] as $property) {
  169. if (isset($content[$property])) {
  170. $build[$property] += $content[$property];
  171. unset($content[$property]);
  172. }
  173. }
  174. $build['content'] = $content;
  175. }
  176. // Either the block's content is completely empty, or it consists only of
  177. // cacheability metadata.
  178. else {
  179. // Abort rendering: render as the empty string and ensure this block is
  180. // render cached, so we can avoid the work of having to repeatedly
  181. // determine whether the block is empty. For instance, modifying or adding
  182. // entities could cause the block to no longer be empty.
  183. $build = [
  184. '#markup' => '',
  185. '#cache' => $build['#cache'],
  186. ];
  187. // If $content is not empty, then it contains cacheability metadata, and
  188. // we must merge it with the existing cacheability metadata. This allows
  189. // blocks to be empty, yet still bubble cacheability metadata, to indicate
  190. // why they are empty.
  191. if (!empty($content)) {
  192. CacheableMetadata::createFromRenderArray($build)
  193. ->merge(CacheableMetadata::createFromRenderArray($content))
  194. ->applyTo($build);
  195. }
  196. }
  197. return $build;
  198. }
  199. }