Skip to main content
Drupal API
User account menu
  • Log in

Breadcrumb

  1. Drupal Core 11.1.x

BigPipeStrategy.php

Namespace

Drupal\big_pipe\Render\Placeholder

File

core/modules/big_pipe/src/Render/Placeholder/BigPipeStrategy.php

View source
<?php

namespace Drupal\big_pipe\Render\Placeholder;

use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Render\Placeholder\PlaceholderStrategyInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\SessionConfigurationInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Defines the BigPipe placeholder strategy, to send HTML in chunks.
 *
 * First: the BigPipe placeholder strategy only activates if the current request
 * is associated with a session. Without a session, it is assumed this response
 * is not actually dynamic: if none of the placeholders show session-dependent
 * information, then none of the placeholders are uncacheable or poorly
 * cacheable, which means the Page Cache (for anonymous users) can deal with it.
 * In other words: BigPipe works for all authenticated users and for anonymous
 * users that have a session (typical example: a shopping cart).
 *
 * (This is the default, and other modules can subclass this placeholder
 * strategy to have different rules for enabling BigPipe.)
 *
 * The BigPipe placeholder strategy actually consists of two substrategies,
 * depending on whether the current session is in a browser with JavaScript
 * enabled or not:
 * 1. with JavaScript enabled: #attached[big_pipe_js_placeholders]. Their
 *    replacements are streamed at the end of the page: chunk 1 is the entire
 *    page until the closing </body> tag, chunks 2 to (N-1) are replacement
 *    values for the placeholders, chunk N is </body> and everything after it.
 * 2. with JavaScript disabled: #attached[big_pipe_nojs_placeholders]. Their
 *    replacements are streamed in situ: chunk 1 is the entire page until the
 *    first no-JS BigPipe placeholder, chunk 2 is the replacement for that
 *    placeholder, chunk 3 is the chunk from after that placeholder until the
 *    next no-JS BigPipe placeholder, et cetera.
 *
 * JS BigPipe placeholders are preferred because they result in better perceived
 * performance: the entire page can be sent, minus the placeholders. But it
 * requires JavaScript.
 *
 * No-JS BigPipe placeholders result in more visible blocking: only the part of
 * the page can be sent until the first placeholder, after it is rendered until
 * the second, et cetera. (In essence: multiple flushes.)
 *
 * Finally, both of those substrategies can also be combined: some placeholders
 * live in places that cannot be efficiently replaced by JavaScript, for example
 * CSRF tokens in URLs. Using no-JS BigPipe placeholders in those cases allows
 * the first part of the page (until the first no-JS BigPipe placeholder) to be
 * sent sooner than when they would be replaced using SingleFlushStrategy, which
 * would prevent anything from being sent until all those non-HTML placeholders
 * would have been replaced.
 *
 * See \Drupal\big_pipe\Render\BigPipe for detailed documentation on how those
 * different placeholders are actually replaced.
 *
 * @see \Drupal\big_pipe\Render\BigPipe
 */
class BigPipeStrategy implements PlaceholderStrategyInterface {
    
    /**
     * BigPipe no-JS cookie name.
     */
    const NOJS_COOKIE = 'big_pipe_nojs';
    
    /**
     * The session configuration.
     *
     * @var \Drupal\Core\Session\SessionConfigurationInterface
     */
    protected $sessionConfiguration;
    
    /**
     * The request stack.
     *
     * @var \Symfony\Component\HttpFoundation\RequestStack
     */
    protected $requestStack;
    
    /**
     * The current route match.
     *
     * @var \Drupal\Core\Routing\RouteMatchInterface
     */
    protected $routeMatch;
    
    /**
     * Constructs a new BigPipeStrategy class.
     *
     * @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
     *   The session configuration.
     * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
     *   The request stack.
     * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
     *   The current route match.
     */
    public function __construct(SessionConfigurationInterface $session_configuration, RequestStack $request_stack, RouteMatchInterface $route_match) {
        $this->sessionConfiguration = $session_configuration;
        $this->requestStack = $request_stack;
        $this->routeMatch = $route_match;
    }
    
    /**
     * {@inheritdoc}
     */
    public function processPlaceholders(array $placeholders) {
        $request = $this->requestStack
            ->getCurrentRequest();
        // Prevent placeholders from being processed by BigPipe on uncacheable
        // request methods. For example, a form rendered inside a placeholder will
        // be rendered as soon as possible before any headers are sent, so that it
        // can be detected, submitted, and redirected immediately.
        // @todo https://www.drupal.org/node/2367555
        if (!$request->isMethodCacheable()) {
            return [];
        }
        // Routes can opt out from using the BigPipe HTML delivery technique.
        if ($this->routeMatch
            ->getRouteObject()
            ->getOption('_no_big_pipe')) {
            return [];
        }
        if (!$this->sessionConfiguration
            ->hasSession($request)) {
            return [];
        }
        return $this->doProcessPlaceholders($placeholders);
    }
    
    /**
     * Transforms placeholders to BigPipe placeholders, either no-JS or JS.
     *
     * @param array $placeholders
     *   The placeholders to process.
     *
     * @return array
     *   The BigPipe placeholders.
     */
    protected function doProcessPlaceholders(array $placeholders) {
        $overridden_placeholders = [];
        foreach ($placeholders as $placeholder => $placeholder_elements) {
            // BigPipe uses JavaScript and the DOM to find the placeholder to replace.
            // This means finding the placeholder to replace must be efficient. Most
            // placeholders are HTML, which we can find efficiently thanks to the
            // querySelector API. But some placeholders are HTML attribute values or
            // parts thereof, and potentially even plain text in DOM text nodes. For
            // BigPipe's JavaScript to find those placeholders, it would need to
            // iterate over all DOM text nodes. This is highly inefficient. Therefore,
            // the BigPipe placeholder strategy only converts HTML placeholders into
            // BigPipe placeholders. The other placeholders need to be replaced on the
            // server, not via BigPipe.
            // @see \Drupal\Core\Access\RouteProcessorCsrf::renderPlaceholderCsrfToken()
            // @see \Drupal\Core\Form\FormBuilder::renderFormTokenPlaceholder()
            // @see \Drupal\Core\Form\FormBuilder::renderPlaceholderFormAction()
            if (static::placeholderIsAttributeSafe($placeholder)) {
                $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, TRUE);
            }
            else {
                // If the current request/session doesn't have JavaScript, fall back to
                // no-JS BigPipe.
                if ($this->requestStack
                    ->getCurrentRequest()->cookies
                    ->has(static::NOJS_COOKIE)) {
                    $overridden_placeholders[$placeholder] = static::createBigPipeNoJsPlaceholder($placeholder, $placeholder_elements, FALSE);
                }
                else {
                    $overridden_placeholders[$placeholder] = static::createBigPipeJsPlaceholder($placeholder, $placeholder_elements);
                }
                $overridden_placeholders[$placeholder]['#cache']['contexts'][] = 'cookies:' . static::NOJS_COOKIE;
            }
        }
        return $overridden_placeholders;
    }
    
    /**
     * Determines whether the given placeholder is attribute-safe or not.
     *
     * @param string $placeholder
     *   A placeholder.
     *
     * @return bool
     *   Whether the placeholder is safe for use in an HTML attribute (in case
     *   it's a placeholder for an HTML attribute value or a subset of it).
     */
    protected static function placeholderIsAttributeSafe($placeholder) {
        assert(is_string($placeholder));
        return $placeholder[0] !== '<' || $placeholder !== Html::normalize($placeholder);
    }
    
    /**
     * Creates a BigPipe JS placeholder.
     *
     * @param string $original_placeholder
     *   The original placeholder.
     * @param array $placeholder_render_array
     *   The render array for a placeholder.
     *
     * @return array
     *   The resulting BigPipe JS placeholder render array.
     */
    protected static function createBigPipeJsPlaceholder($original_placeholder, array $placeholder_render_array) {
        $big_pipe_placeholder_id = static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array);
        $interface_preview = [];
        if (isset($placeholder_render_array['#lazy_builder'])) {
            $interface_preview = [
                '#theme' => 'big_pipe_interface_preview',
                '#callback' => $placeholder_render_array['#lazy_builder'][0],
                '#arguments' => $placeholder_render_array['#lazy_builder'][1],
            ];
            if (isset($placeholder_render_array['#preview'])) {
                $interface_preview['#preview'] = $placeholder_render_array['#preview'];
                unset($placeholder_render_array['#preview']);
            }
        }
        return [
            '#prefix' => '<span data-big-pipe-placeholder-id="' . Html::escape($big_pipe_placeholder_id) . '">',
            'interface_preview' => $interface_preview,
            '#suffix' => '</span>',
            '#cache' => [
                'max-age' => 0,
                'contexts' => [
                    'session.exists',
                ],
            ],
            '#attached' => [
                'library' => [
                    'big_pipe/big_pipe',
                ],
                // Inform BigPipe' JavaScript known BigPipe placeholder IDs.
'drupalSettings' => [
                    'bigPipePlaceholderIds' => [
                        $big_pipe_placeholder_id => TRUE,
                    ],
                ],
                'big_pipe_placeholders' => [
                    Html::escape($big_pipe_placeholder_id) => $placeholder_render_array,
                ],
            ],
        ];
    }
    
    /**
     * Creates a BigPipe no-JS placeholder.
     *
     * @param string $original_placeholder
     *   The original placeholder.
     * @param array $placeholder_render_array
     *   The render array for a placeholder.
     * @param bool $placeholder_must_be_attribute_safe
     *   Whether the placeholder must be safe for use in an HTML attribute (in
     *   case it's a placeholder for an HTML attribute value or a subset of it).
     *
     * @return array
     *   The resulting BigPipe no-JS placeholder render array.
     */
    protected static function createBigPipeNoJsPlaceholder($original_placeholder, array $placeholder_render_array, $placeholder_must_be_attribute_safe = FALSE) {
        if (!$placeholder_must_be_attribute_safe) {
            $big_pipe_placeholder = '<span data-big-pipe-nojs-placeholder-id="' . Html::escape(static::generateBigPipePlaceholderId($original_placeholder, $placeholder_render_array)) . '"></span>';
        }
        else {
            $big_pipe_placeholder = 'big_pipe_nojs_placeholder_attribute_safe:' . Html::escape($original_placeholder);
        }
        return [
            '#markup' => $big_pipe_placeholder,
            '#cache' => [
                'max-age' => 0,
                'contexts' => [
                    'session.exists',
                ],
            ],
            '#attached' => [
                'big_pipe_nojs_placeholders' => [
                    $big_pipe_placeholder => $placeholder_render_array,
                ],
            ],
        ];
    }
    
    /**
     * Generates a BigPipe placeholder ID.
     *
     * @param string $original_placeholder
     *   The original placeholder.
     * @param array $placeholder_render_array
     *   The render array for a placeholder.
     *
     * @return string
     *   The generated BigPipe placeholder ID.
     */
    protected static function generateBigPipePlaceholderId($original_placeholder, array $placeholder_render_array) {
        // Generate a BigPipe placeholder ID (to be used by BigPipe's JavaScript).
        // @see \Drupal\Core\Render\PlaceholderGenerator::createPlaceholder()
        if (isset($placeholder_render_array['#lazy_builder'])) {
            // Be sure cache contexts and tags are sorted before serializing them and
            // making hash. Issue #3225328 removes sort from contexts and tags arrays
            // for performances reasons.
            if (isset($placeholder_render_array['#cache']['contexts'])) {
                sort($placeholder_render_array['#cache']['contexts']);
            }
            if (isset($placeholder_render_array['#cache']['tags'])) {
                sort($placeholder_render_array['#cache']['tags']);
            }
            $callback = $placeholder_render_array['#lazy_builder'][0];
            $arguments = $placeholder_render_array['#lazy_builder'][1];
            $token = Crypt::hashBase64(serialize($placeholder_render_array));
            return UrlHelper::buildQuery([
                'callback' => $callback,
                'args' => $arguments,
                'token' => $token,
            ]);
        }
        else {
            return Html::getId($original_placeholder);
        }
    }

}

Classes

Title Deprecated Summary
BigPipeStrategy Defines the BigPipe placeholder strategy, to send HTML in chunks.
RSS feed
Powered by Drupal