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