function BigPipe::sendPlaceholders

Same name and namespace in other branches
  1. 9 core/modules/big_pipe/src/Render/BigPipe.php \Drupal\big_pipe\Render\BigPipe::sendPlaceholders()
  2. 8.9.x core/modules/big_pipe/src/Render/BigPipe.php \Drupal\big_pipe\Render\BigPipe::sendPlaceholders()
  3. 10 core/modules/big_pipe/src/Render/BigPipe.php \Drupal\big_pipe\Render\BigPipe::sendPlaceholders()

Sends BigPipe placeholders' replacements as embedded AJAX responses.

Parameters

array $placeholders: Associative array; the BigPipe placeholders. Keys are the BigPipe placeholder IDs.

array $placeholder_order: Indexed array; the order in which the BigPipe placeholders must be sent. Values are the BigPipe placeholder IDs. (These values correspond to keys in $placeholders.)

\Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets: The cumulative assets sent so far; to be updated while rendering BigPipe placeholders.

Throws

\Exception If an exception is thrown during the rendering of a placeholder, it is caught to allow the other placeholders to still be replaced. But when error logging is configured to be verbose, the exception is rethrown to simplify debugging.

1 call to BigPipe::sendPlaceholders()
BigPipe::sendContent in core/modules/big_pipe/src/Render/BigPipe.php
Sends an HTML response in chunks using the BigPipe technique.

File

core/modules/big_pipe/src/Render/BigPipe.php, line 471

Class

BigPipe
Service for sending an HTML response in chunks (to get faster page loads).

Namespace

Drupal\big_pipe\Render

Code

protected function sendPlaceholders(array $placeholders, array $placeholder_order, AttachedAssetsInterface $cumulative_assets) {
  // Return early if there are no BigPipe placeholders to send.
  if (empty($placeholders)) {
    return;
  }
  // Send the start signal.
  $this->sendChunk("\n" . static::START_SIGNAL . "\n");
  // A BigPipe response consists of an HTML response plus multiple embedded
  // AJAX responses. To process the attachments of those AJAX responses, we
  // need a fake request that is identical to the main request, but with
  // one change: it must have the right Accept header, otherwise the work-
  // around for a bug in IE9 will cause not JSON, but <textarea>-wrapped JSON
  // to be returned.
  // @see \Drupal\Core\EventSubscriber\AjaxResponseSubscriber::onResponse()
  $fake_request = $this->requestStack
    ->getMainRequest()
    ->duplicate();
  $fake_request->headers
    ->set('Accept', 'application/vnd.drupal-ajax');
  // Create a Fiber for each placeholder.
  $fibers = [];
  $cacheable_metadata = new CacheableMetadata();
  foreach ($placeholder_order as $placeholder_id) {
    if (!isset($placeholders[$placeholder_id])) {
      continue;
    }
    $placeholder_render_array = $placeholders[$placeholder_id];
    $fibers[$placeholder_id] = new \Fiber(fn() => $this->renderPlaceholder($placeholder_id, $placeholder_render_array));
  }
  $iterations = 0;
  while (count($fibers) > 0) {
    foreach ($fibers as $placeholder_id => $fiber) {
      try {
        if (!$fiber->isStarted()) {
          $fiber->start();
        }
        elseif ($fiber->isSuspended()) {
          $fiber->resume();
        }
        // If the Fiber hasn't terminated by this point, move onto the next
        // placeholder, we'll resume this Fiber again when we get back here.
        if (!$fiber->isTerminated()) {
          // If we've gone through the placeholders once already, and they're
          // still not finished, then start to allow code higher up the stack
          // to get on with something else.
          if ($iterations) {
            $fiber = \Fiber::getCurrent();
            if ($fiber !== NULL) {
              $fiber->suspend();
            }
          }
          continue;
        }
        $elements = $fiber->getReturn();
        unset($fibers[$placeholder_id]);
        if ($this->debugCacheabilityHeaders) {
          $cacheable_metadata->addCacheableDependency(CacheableMetadata::createFromRenderArray($elements));
        }
        // Create a new AjaxResponse.
        $ajax_response = new AjaxResponse();
        // JavaScript's querySelector automatically decodes HTML entities in
        // attributes, so we must decode the entities of the current BigPipe
        // placeholder ID (which has HTML entities encoded since we use it to
        // find the placeholders).
        $big_pipe_js_placeholder_id = Html::decodeEntities($placeholder_id);
        $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-placeholder-id="%s"]', $big_pipe_js_placeholder_id), $elements['#markup']));
        $ajax_response->setAttachments($elements['#attached']);
        // Delete all messages that were generated during the rendering of
        // this placeholder, to render them in a BigPipe-optimized way.
        $messages = $this->messenger
          ->deleteAll();
        foreach ($messages as $type => $type_messages) {
          foreach ($type_messages as $message) {
            $ajax_response->addCommand(new MessageCommand($message, NULL, [
              'type' => $type,
            ], FALSE));
          }
        }
        // Push a fake request with the asset libraries loaded so far and
        // dispatch KernelEvents::RESPONSE event. This results in the
        // attachments for the AJAX response being processed by
        // AjaxResponseAttachmentsProcessor and hence:
        // - the necessary AJAX commands to load the necessary missing asset
        //   libraries and updated AJAX page state are added to the AJAX
        //   response
        // - the attachments associated with the response are finalized,
        // which allows us to track the total set of asset libraries sent in
        // the initial HTML response plus all embedded AJAX responses sent so
        // far.
        $fake_request->query
          ->set('ajax_page_state', [
          'libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries()),
        ] + $cumulative_assets->getSettings()['ajaxPageState']);
        $ajax_response = $this->filterEmbeddedResponse($fake_request, $ajax_response);
        // Send this embedded AJAX response.
        $json = $ajax_response->getContent();
        $output = <<<EOF
<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="{<span class="php-variable">$placeholder_id</span>}">
{<span class="php-variable">$json</span>}
</script>
EOF;
        $this->sendChunk($output);
        // Another placeholder was rendered and sent, track the set of asset
        // libraries sent so far. Any new settings are already sent; we
        // don't need to track those.
        if (isset($ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries'])) {
          $cumulative_assets->setAlreadyLoadedLibraries(explode(',', $ajax_response->getAttachments()['drupalSettings']['ajaxPageState']['libraries']));
        }
      } catch (EnforcedResponseException $e) {
        $response = $e->getResponse();
        if (!$response instanceof RedirectResponse) {
          throw $e;
        }
        $ajax_response = new AjaxResponse();
        if ($response instanceof SecuredRedirectResponse) {
          // Only redirect to safe locations.
          $ajax_response->addCommand(new RedirectCommand($response->getTargetUrl()));
        }
        else {
          try {
            // SecuredRedirectResponse is an abstract class that requires a
            // concrete implementation. Default to LocalRedirectResponse,
            // which considers only redirects to within the same site as safe.
            $safe_response = LocalRedirectResponse::createFromRedirectResponse($response);
            $safe_response->setRequestContext($this->requestContext);
            $ajax_response->addCommand(new RedirectCommand($safe_response->getTargetUrl()));
          } catch (\InvalidArgumentException) {
            // If the above failed, it's because the redirect target wasn't
            // local. Do not follow that redirect. Log an error message
            // instead, then return a 400 response to the client with the
            // error message. We don't throw an exception, because this is a
            // client error rather than a server error.
            $message = 'Redirects to external URLs are not allowed by default, use \\Drupal\\Core\\Routing\\TrustedRedirectResponse for it.';
            $this->logger
              ->error($message);
            $ajax_response->addCommand(new MessageCommand($message));
          }
        }
        $ajax_response = $this->filterEmbeddedResponse($fake_request, $ajax_response);
        $json = $ajax_response->getContent();
        $output = <<<EOF
<script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="{<span class="php-variable">$placeholder_id</span>}">
{<span class="php-variable">$json</span>}
</script>
EOF;
        $this->sendChunk($output);
        // Send the stop signal.
        $this->sendChunk("\n" . static::STOP_SIGNAL . "\n");
        break;

      } catch (\Exception $e) {
        unset($fibers[$placeholder_id]);
        if ($this->configFactory
          ->get('system.logging')
          ->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
          throw $e;
        }
        else {
          trigger_error($e, E_USER_WARNING);
        }
      }
    }
    $iterations++;
  }
  if ($this->debugCacheabilityHeaders) {
    $this->sendChunk("\n<!-- big_pipe cache tags: " . implode(' ', $cacheable_metadata->getCacheTags()) . " -->\n");
    $this->sendChunk("\n<!-- big_pipe cache contexts: " . implode(' ', $cacheable_metadata->getCacheContexts()) . " -->\n");
  }
  // Send the stop signal.
  $this->sendChunk("\n" . static::STOP_SIGNAL . "\n");
}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.