Renderer.php 26.9 KB
Newer Older
1 2 3 4 5 6 7 8 9
<?php

/**
 * @file
 * Contains \Drupal\Core\Render\Renderer.
 */

namespace Drupal\Core\Render;

10
use Drupal\Component\Utility\NestedArray;
11
use Drupal\Component\Utility\SafeMarkup;
12
use Drupal\Component\Utility\UrlHelper;
13
use Drupal\Core\Cache\Cache;
14
use Drupal\Core\Cache\CacheableMetadata;
15
use Drupal\Core\Controller\ControllerResolverInterface;
16
use Drupal\Core\Template\Attribute;
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
use Drupal\Core\Theme\ThemeManagerInterface;

/**
 * Turns a render array into a HTML string.
 */
class Renderer implements RendererInterface {

  /**
   * The theme manager.
   *
   * @var \Drupal\Core\Theme\ThemeManagerInterface
   */
  protected $theme;

  /**
   * The controller resolver.
   *
   * @var \Drupal\Core\Controller\ControllerResolverInterface
   */
  protected $controllerResolver;

  /**
   * The element info.
   *
   * @var \Drupal\Core\Render\ElementInfoManagerInterface
   */
  protected $elementInfo;

45
  /**
46
   * The render cache service.
47
   *
48
   * @var \Drupal\Core\Render\RenderCacheInterface
49
   */
50
  protected $renderCache;
51

52 53 54 55 56 57 58
  /**
   * The renderer configuration array.
   *
   * @var array
   */
  protected $rendererConfig;

59 60 61 62 63 64 65
  /**
   * The stack containing bubbleable rendering metadata.
   *
   * @var \SplStack|null
   */
  protected static $stack;

66 67 68 69 70 71 72 73 74
  /**
   * Constructs a new Renderer.
   *
   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
   *   The controller resolver.
   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme
   *   The theme manager.
   * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
   *   The element info.
75 76
   * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
   *   The render cache service.
77 78
   * @param array $renderer_config
   *   The renderer configuration array.
79
   */
80
  public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, RenderCacheInterface $render_cache, array $renderer_config) {
81 82 83
    $this->controllerResolver = $controller_resolver;
    $this->theme = $theme;
    $this->elementInfo = $element_info;
84
    $this->renderCache = $render_cache;
85
    $this->rendererConfig = $renderer_config;
86 87 88 89 90 91 92 93 94 95 96 97
  }

  /**
   * {@inheritdoc}
   */
  public function renderRoot(&$elements) {
    return $this->render($elements, TRUE);
  }

  /**
   * {@inheritdoc}
   */
98
  public function renderPlain(&$elements) {
99
    $current_stack = static::$stack;
100 101
    $this->resetStack();
    $output = $this->renderRoot($elements);
102
    static::$stack = $current_stack;
103 104
    return $output;
  }
105

106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
  /**
   * Renders final HTML for a placeholder.
   *
   * Renders the placeholder in isolation.
   *
   * @param string $placeholder
   *   An attached placeholder to render. (This must be a key of one of the
   *   values of $elements['#attached']['placeholders'].)
   * @param array $elements
   *   The structured array describing the data to be rendered.
   *
   * @return array
   *   The updated $elements.
   *
   * @see ::replacePlaceholders()
   *
   * @todo Make public as part of https://www.drupal.org/node/2469431
   */
  protected function renderPlaceholder($placeholder, array $elements) {
    // Get the render array for the given placeholder
    $placeholder_elements = $elements['#attached']['placeholders'][$placeholder];

    // Render the placeholder into markup.
    $markup = $this->renderPlain($placeholder_elements);

    // Replace the placeholder with its rendered markup, and merge its
    // bubbleable metadata with the main elements'.
    $elements['#markup'] = str_replace($placeholder, $markup, $elements['#markup']);
    $elements = $this->mergeBubbleableMetadata($elements, $placeholder_elements);

    // Remove the placeholder that we've just rendered.
    unset($elements['#attached']['placeholders'][$placeholder]);

    return $elements;
  }


143 144 145 146
  /**
   * {@inheritdoc}
   */
  public function render(&$elements, $is_root_call = FALSE) {
147 148 149 150 151
    // Since #pre_render, #post_render, #lazy_builder callbacks and theme
    // functions or templates may be used for generating a render array's
    // content, and we might be rendering the main content for the page, it is
    // possible that any of them throw an exception that will cause a different
    // page to be rendered (e.g. throwing
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170
    // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause
    // the 404 page to be rendered). That page might also use Renderer::render()
    // but if exceptions aren't caught here, the stack will be left in an
    // inconsistent state.
    // Hence, catch all exceptions and reset the stack and re-throw them.
    try {
      return $this->doRender($elements, $is_root_call);
    }
    catch (\Exception $e) {
      // Reset stack and re-throw exception.
      $this->resetStack();
      throw $e;
    }
  }

  /**
   * See the docs for ::render().
   */
  protected function doRender(&$elements, $is_root_call = FALSE) {
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
    if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
      if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
        $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
      }
      $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
    }

    // Early-return nothing if user does not have access.
    if (empty($elements) || (isset($elements['#access']) && !$elements['#access'])) {
      return '';
    }

    // Do not print elements twice.
    if (!empty($elements['#printed'])) {
      return '';
    }

188 189
    if (!isset(static::$stack)) {
      static::$stack = new \SplStack();
190
    }
191
    static::$stack->push(new BubbleableMetadata());
192

193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
    // Set the bubbleable rendering metadata that has configurable defaults, if:
    // - this is the root call, to ensure that the final render array definitely
    //   has these configurable defaults, even when no subtree is render cached.
    // - this is a render cacheable subtree, to ensure that the cached data has
    //   the configurable defaults (which may affect the ID and invalidation).
    if ($is_root_call || isset($elements['#cache']['keys'])) {
      $required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
      if (isset($elements['#cache']['contexts'])) {
        $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts);
      }
      else {
        $elements['#cache']['contexts'] = $required_cache_contexts;
      }
    }

208 209
    // Try to fetch the prerendered element from cache, replace any placeholders
    // and return the final markup.
210
    if (isset($elements['#cache']['keys'])) {
211
      $cached_element = $this->renderCache->get($elements);
212 213
      if ($cached_element !== FALSE) {
        $elements = $cached_element;
214 215 216
        // Only when we're in a root (non-recursive) Renderer::render() call,
        // placeholders must be processed, to prevent breaking the render cache
        // in case of nested elements with #cache set.
217
        if ($is_root_call) {
218
          $this->replacePlaceholders($elements);
219
        }
220 221 222 223
        // Mark the element markup as safe. If we have cached children, we need
        // to mark them as safe too. The parent markup contains the child
        // markup, so if the parent markup is safe, then the markup of the
        // individual children must be safe as well.
224
        $elements['#markup'] = SafeMarkup::set($elements['#markup']);
225 226 227 228 229
        if (!empty($elements['#cache_properties'])) {
          foreach (Element::children($cached_element) as $key) {
            SafeMarkup::set($cached_element[$key]['#markup']);
          }
        }
230 231
        // The render cache item contains all the bubbleable rendering metadata
        // for the subtree.
232
        $this->updateStack($elements);
233 234
        // Render cache hit, so rendering is finished, all necessary info
        // collected!
235
        $this->bubbleStack();
236 237 238
        return $elements['#markup'];
      }
    }
239 240
    // Two-tier caching: track pre-bubbling elements' #cache for later
    // comparison.
241 242
    // @see \Drupal\Core\Render\RenderCacheInterface::get()
    // @see \Drupal\Core\Render\RenderCacheInterface::set()
243 244
    $pre_bubbling_elements = [];
    $pre_bubbling_elements['#cache'] = isset($elements['#cache']) ? $elements['#cache'] : [];
245 246 247 248 249 250 251

    // If the default values for this element have not been loaded yet, populate
    // them.
    if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
      $elements += $this->elementInfo->getInfo($elements['#type']);
    }

252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311
    // First validate the usage of #lazy_builder; both of the next if-statements
    // use it if available.
    if (isset($elements['#lazy_builder'])) {
      // @todo Convert to assertions once https://www.drupal.org/node/2408013
      //   lands.
      if (!is_array($elements['#lazy_builder'])) {
        throw new \DomainException('The #lazy_builder property must have an array as a value.');
      }
      if (count($elements['#lazy_builder']) !== 2) {
        throw new \DomainException('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.');
      }
      if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function($v) { return is_null($v) || is_scalar($v); }))) {
        throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL.");
      }
      $children = Element::children($elements);
      if ($children) {
        throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: %s.', implode(', ', $children)));
      }
      $supported_keys = [
        '#lazy_builder',
        '#cache',
        '#create_placeholder',
        // These keys are not actually supported, but they are added automatically
        // by the Renderer, so we don't crash on them; them being missing when
        // their #lazy_builder callback is invoked won't surprise the developer.
        '#weight',
        '#printed'
      ];
      $unsupported_keys = array_diff(array_keys($elements), $supported_keys);
      if (count($unsupported_keys)) {
        throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys)));
      }
    }
    // If instructed to create a placeholder, and a #lazy_builder callback is
    // present (without such a callback, it would be impossible to replace the
    // placeholder), replace the current element with a placeholder.
    if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE) {
      if (!isset($elements['#lazy_builder'])) {
        throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.');
      }
      $elements = $this->createPlaceholder($elements);
    }
    // Build the element if it is still empty.
    if (isset($elements['#lazy_builder'])) {
      $callable = $elements['#lazy_builder'][0];
      $args = $elements['#lazy_builder'][1];
      if (is_string($callable) && strpos($callable, '::') === FALSE) {
        $callable = $this->controllerResolver->getControllerFromDefinition($callable);
      }
      $new_elements = call_user_func_array($callable, $args);
      // Retain the original cacheability metadata, plus cache keys.
      CacheableMetadata::createFromRenderArray($elements)
        ->merge(CacheableMetadata::createFromRenderArray($new_elements))
        ->applyTo($new_elements);
      if (isset($elements['#cache']['keys'])) {
        $new_elements['#cache']['keys'] = $elements['#cache']['keys'];
      }
      $elements = $new_elements;
      $elements['#lazy_builder_built'] = TRUE;
    }
312 313 314 315 316 317 318 319
    // Make any final changes to the element before it is rendered. This means
    // that the $element or the children can be altered or corrected before the
    // element is rendered into the final text.
    if (isset($elements['#pre_render'])) {
      foreach ($elements['#pre_render'] as $callable) {
        if (is_string($callable) && strpos($callable, '::') === FALSE) {
          $callable = $this->controllerResolver->getControllerFromDefinition($callable);
        }
320
        $elements = call_user_func($callable, $elements);
321 322 323 324 325
      }
    }

    // Defaults for bubbleable rendering metadata.
    $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array();
326
    $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
327 328 329 330 331 332
    $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array();

    // Allow #pre_render to abort rendering.
    if (!empty($elements['#printed'])) {
      // The #printed element contains all the bubbleable rendering metadata for
      // the subtree.
333
      $this->updateStack($elements);
334
      // #printed, so rendering is finished, all necessary info collected!
335
      $this->bubbleStack();
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352
      return '';
    }

    // Add any JavaScript state information associated with the element.
    if (!empty($elements['#states'])) {
      drupal_process_states($elements);
    }

    // Get the children of the element, sorted by weight.
    $children = Element::children($elements, TRUE);

    // Initialize this element's #children, unless a #pre_render callback
    // already preset #children.
    if (!isset($elements['#children'])) {
      $elements['#children'] = '';
    }

353
    // @todo Simplify after https://www.drupal.org/node/2273925.
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
    if (isset($elements['#markup'])) {
      $elements['#markup'] = SafeMarkup::set($elements['#markup']);
    }

    // Assume that if #theme is set it represents an implemented hook.
    $theme_is_implemented = isset($elements['#theme']);
    // Check the elements for insecure HTML and pass through sanitization.
    if (isset($elements)) {
      $markup_keys = array(
        '#description',
        '#field_prefix',
        '#field_suffix',
      );
      foreach ($markup_keys as $key) {
        if (!empty($elements[$key]) && is_scalar($elements[$key])) {
          $elements[$key] = SafeMarkup::checkAdminXss($elements[$key]);
        }
      }
    }

    // Call the element's #theme function if it is set. Then any children of the
    // element have to be rendered there. If the internal #render_children
    // property is set, do not call the #theme function to prevent infinite
    // recursion.
    if ($theme_is_implemented && !isset($elements['#render_children'])) {
      $elements['#children'] = $this->theme->render($elements['#theme'], $elements);

      // If ThemeManagerInterface::render() returns FALSE this means that the
      // hook in #theme was not found in the registry and so we need to update
      // our flag accordingly. This is common for theme suggestions.
      $theme_is_implemented = ($elements['#children'] !== FALSE);
    }

    // If #theme is not implemented or #render_children is set and the element
    // has an empty #children attribute, render the children now. This is the
    // same process as Renderer::render() but is inlined for speed.
    if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
      foreach ($children as $key) {
392
        $elements['#children'] .= $this->doRender($elements[$key]);
393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451
      }
      $elements['#children'] = SafeMarkup::set($elements['#children']);
    }

    // If #theme is not implemented and the element has raw #markup as a
    // fallback, prepend the content in #markup to #children. In this case
    // #children will contain whatever is provided by #pre_render prepended to
    // what is rendered recursively above. If #theme is implemented then it is
    // the responsibility of that theme implementation to render #markup if
    // required. Eventually #theme_wrappers will expect both #markup and
    // #children to be a single string as #children.
    if (!$theme_is_implemented && isset($elements['#markup'])) {
      $elements['#children'] = SafeMarkup::set($elements['#markup'] . $elements['#children']);
    }

    // Let the theme functions in #theme_wrappers add markup around the rendered
    // children.
    // #states and #attached have to be processed before #theme_wrappers,
    // because the #type 'page' render array from drupal_prepare_page() would
    // render the $page and wrap it into the html.html.twig template without the
    // attached assets otherwise.
    // If the internal #render_children property is set, do not call the
    // #theme_wrappers function(s) to prevent infinite recursion.
    if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
      foreach ($elements['#theme_wrappers'] as $key => $value) {
        // If the value of a #theme_wrappers item is an array then the theme
        // hook is found in the key of the item and the value contains attribute
        // overrides. Attribute overrides replace key/value pairs in $elements
        // for only this ThemeManagerInterface::render() call. This allows
        // #theme hooks and #theme_wrappers hooks to share variable names
        // without conflict or ambiguity.
        $wrapper_elements = $elements;
        if (is_string($key)) {
          $wrapper_hook = $key;
          foreach ($value as $attribute => $override) {
            $wrapper_elements[$attribute] = $override;
          }
        }
        else {
          $wrapper_hook = $value;
        }

        $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements);
      }
    }

    // Filter the outputted content and make any last changes before the content
    // is sent to the browser. The changes are made on $content which allows the
    // outputted text to be filtered.
    if (isset($elements['#post_render'])) {
      foreach ($elements['#post_render'] as $callable) {
        if (is_string($callable) && strpos($callable, '::') === FALSE) {
          $callable = $this->controllerResolver->getControllerFromDefinition($callable);
        }
        $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
      }
    }

    // We store the resulting output in $elements['#markup'], to be consistent
452 453 454
    // with how render cached output gets stored. This ensures that placeholder
    // replacement logic gets the same data to work with, no matter if #cache is
    // disabled, #cache is enabled, there is a cache hit or miss.
455 456 457 458 459 460
    $prefix = isset($elements['#prefix']) ? SafeMarkup::checkAdminXss($elements['#prefix']) : '';
    $suffix = isset($elements['#suffix']) ? SafeMarkup::checkAdminXss($elements['#suffix']) : '';

    $elements['#markup'] = $prefix . $elements['#children'] . $suffix;

    // We've rendered this element (and its subtree!), now update the stack.
461
    $this->updateStack($elements);
462

463 464 465 466 467 468
    // Cache the processed element if both $pre_bubbling_elements and $elements
    // have the metadata necessary to generate a cache ID.
    if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) {
      if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) {
        throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.');
      }
469
      $this->renderCache->set($elements, $pre_bubbling_elements);
470 471
    }

472 473 474
    // Only when we're in a root (non-recursive) Renderer::render() call,
    // placeholders must be processed, to prevent breaking the render cache in
    // case of nested elements with #cache set.
475 476 477 478 479 480 481
    //
    // By running them here, we ensure that:
    // - they run when #cache is disabled,
    // - they run when #cache is enabled and there is a cache miss.
    // Only the case of a cache hit when #cache is enabled, is not handled here,
    // that is handled earlier in Renderer::render().
    if ($is_root_call) {
482
      $this->replacePlaceholders($elements);
483
      if (static::$stack->count() !== 1) {
484 485 486 487 488
        throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
      }
    }

    // Rendering is finished, all necessary info collected!
489
    $this->bubbleStack();
490 491 492 493 494 495

    $elements['#printed'] = TRUE;
    $elements['#markup'] = SafeMarkup::set($elements['#markup']);
    return $elements['#markup'];
  }

496 497 498 499 500 501 502
  /**
   * Resets the renderer service's internal stack (used for bubbling metadata).
   *
   * Only necessary in very rare/advanced situations, such as when rendering an
   * error page if an exception occurred *during* rendering.
   */
  protected function resetStack() {
503
    static::$stack = NULL;
504 505 506 507 508 509 510 511 512 513 514 515
  }

  /**
   * Updates the stack.
   *
   * @param array &$element
   *   The element of the render array that has just been rendered. The stack
   *   frame for this element will be updated with the bubbleable rendering
   *   metadata of this element.
   */
  protected function updateStack(&$element) {
    // The latest frame represents the bubbleable metadata for the subtree.
516
    $frame = static::$stack->pop();
517 518
    // Update the frame, but also update the current element, to ensure it
    // contains up-to-date information in case it gets render cached.
519 520 521
    $updated_frame = BubbleableMetadata::createFromRenderArray($element)->merge($frame);
    $updated_frame->applyTo($element);
    static::$stack->push($updated_frame);
522 523 524 525 526 527 528 529 530 531 532 533
  }

  /**
   * Bubbles the stack.
   *
   * Whenever another level in the render array has been rendered, the stack
   * must be bubbled, to merge its rendering metadata with that of the parent
   * element.
   */
  protected function bubbleStack() {
    // If there's only one frame on the stack, then this is the root call, and
    // we can't bubble up further. Reset the stack for the next root call.
534
    if (static::$stack->count() === 1) {
535 536 537 538 539
      $this->resetStack();
      return;
    }

    // Merge the current and the parent stack frame.
540 541
    $current = static::$stack->pop();
    $parent = static::$stack->pop();
542
    static::$stack->push($current->merge($parent));
543 544
  }

545
  /**
546
   * Replaces placeholders.
547
   *
548 549 550 551
   * Placeholders may have:
   * - #lazy_builder callback, to build a render array to be rendered into
   *   markup that can replace the placeholder
   * - #cache: to cache the result of the placeholder
552
   *
553 554 555
   * Also merges the bubbleable metadata resulting from the rendering of the
   * contents of the placeholders. Hence $elements will be contain the entirety
   * of bubbleable metadata.
556 557
   *
   * @param array &$elements
558 559 560 561 562 563
   *   The structured array describing the data being rendered. Including the
   *   bubbleable metadata associated with the markup that replaced the
   *   placeholders.
   *
   * @returns bool
   *   Whether placeholders were replaced.
564
   */
565 566 567 568
  protected function replacePlaceholders(array &$elements) {
    if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) {
      return FALSE;
    }
569

570 571
    foreach (array_keys($elements['#attached']['placeholders']) as $placeholder) {
      $elements = $this->renderPlaceholder($placeholder, $elements);
572
    }
573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618

    return TRUE;
  }

  /**
   * Turns this element into a placeholder.
   *
   * Placeholdering allows us to avoid "poor cacheability contamination": this
   * maps the current render array to one that only has #markup and #attached,
   * and #attached contains a placeholder with this element's prior cacheability
   * metadata. In other words: this placeholder is perfectly cacheable, the
   * placeholder replacement logic effectively cordons off poor cacheability.
   *
   * @param array $element
   *   The render array to create a placeholder for.
   *
   * @return array
   *   Render array with placeholder markup and the attached placeholder
   *   replacement metadata.
   */
  protected function createPlaceholder(array $element) {
    $placeholder_render_array = array_intersect_key($element, [
      // Placeholders are replaced with markup by executing the associated
      // #lazy_builder callback, which generates a render array, and which the
      // Renderer will render and replace the placeholder with.
      '#lazy_builder' => TRUE,
      // The cacheability metadata for the placeholder. The rendered result of
      // the placeholder may itself be cached, if [#cache][keys] are specified.
      '#cache' => TRUE,
    ]);

    // Generate placeholder markup. Note that the only requirement is that this
    // is unique markup that isn't easily guessable. The #lazy_builder callback
    // and its arguments are put in the placeholder markup solely to simplify
    // debugging.
    $attributes = new Attribute();
    $attributes['callback'] = $placeholder_render_array['#lazy_builder'][0];
    $attributes['arguments'] = UrlHelper::buildQuery($placeholder_render_array['#lazy_builder'][1]);
    $attributes['token'] = hash('sha1', serialize($placeholder_render_array));
    $placeholder_markup = '<drupal-render-placeholder' . $attributes . '></drupal-render-placeholder>';

    // Build the placeholder element to return.
    $placeholder_element = [];
    $placeholder_element['#markup'] = $placeholder_markup;
    $placeholder_element['#attached']['placeholders'][$placeholder_markup] = $placeholder_render_array;
    return $placeholder_element;
619 620
  }

621 622 623
  /**
   * {@inheritdoc}
   */
624
  public function mergeBubbleableMetadata(array $a, array $b) {
625 626 627 628 629 630
    $meta_a = BubbleableMetadata::createFromRenderArray($a);
    $meta_b = BubbleableMetadata::createFromRenderArray($b);
    $meta_a->merge($meta_b)->applyTo($a);
    return $a;
  }

631 632 633
  /**
   * {@inheritdoc}
   */
634
  public function addCacheableDependency(array &$elements, $dependency) {
635 636
    $meta_a = CacheableMetadata::createFromRenderArray($elements);
    $meta_b = CacheableMetadata::createFromObject($dependency);
637 638 639
    $meta_a->merge($meta_b)->applyTo($elements);
  }

640 641 642
  /**
   * {@inheritdoc}
   */
643
  public function mergeAttachments(array $a, array $b) {
644 645 646 647
    // If both #attached arrays contain drupalSettings, then merge them
    // correctly; adding the same settings multiple times needs to behave
    // idempotently.
    if (!empty($a['drupalSettings']) && !empty($b['drupalSettings'])) {
648 649 650
      $drupalSettings = NestedArray::mergeDeepArray(array($a['drupalSettings'], $b['drupalSettings']), TRUE);
      // No need for re-merging them.
      unset($a['drupalSettings']);
651 652
      unset($b['drupalSettings']);
    }
653 654 655 656 657 658 659
    // Optimize merging of placeholders: no need for deep merging.
    if (!empty($a['placeholders']) && !empty($b['placeholders'])) {
      $placeholders = $a['placeholders'] + $b['placeholders'];
      // No need for re-merging them.
      unset($a['placeholders']);
      unset($b['placeholders']);
    }
660 661 662 663 664 665
    // Apply the normal merge.
    $a = array_merge_recursive($a, $b);
    if (isset($drupalSettings)) {
      // Save the custom merge for the drupalSettings.
      $a['drupalSettings'] = $drupalSettings;
    }
666 667 668
    if (isset($placeholders)) {
      // Save the custom merge for the placeholders.
      $a['placeholders'] = $placeholders;
669
    }
670
    return $a;
671 672
  }

673
}