Verified Commit 108150c6 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3377570 by catch, Wim Leers, mglaman, marcelovani, Kingdutch: Add PHP...

Issue #3377570 by catch, Wim Leers, mglaman, marcelovani, Kingdutch: Add PHP Fibers support to BigPipe
parent 6a023838
Loading
Loading
Loading
Loading
+34 −10
Original line number Diff line number Diff line
@@ -237,22 +237,46 @@ protected function init($theme_name = NULL) {
   */
  public function get() {
    $this->init($this->themeName);
    if (isset($this->registry[$this->theme->getName()])) {
      return $this->registry[$this->theme->getName()];
    if ($cached = $this->cacheGet()) {
      return $cached;
    }
    // If called from inside a Fiber, suspend it, this may allow another code
    // path to begin an asynchronous operation before we do the CPU-intensive
    // task of building the theme registry.
    if (\Fiber::getCurrent() !== NULL) {
      \Fiber::suspend();
      // When the Fiber is resumed, check the cache again since it may have been
      // built in the meantime, either in this process or via a different
      // request altogether.
      if ($cached = $this->cacheGet()) {
        return $cached;
      }
    if ($cache = $this->cache->get('theme_registry:' . $this->theme->getName())) {
      $this->registry[$this->theme->getName()] = $cache->data;
    }
    else {
    $this->build();
    // Only persist it if all modules are loaded to ensure it is complete.
    if ($this->moduleHandler->isLoaded()) {
      $this->setCache();
    }
    }
    return $this->registry[$this->theme->getName()];
  }

  /**
   * Gets the theme registry cache.
   *
   * @return array|null
   */
  protected function cacheGet(): ?array {
    $theme_name = $this->theme->getName();
    if (isset($this->registry[$theme_name])) {
      return $this->registry[$theme_name];
    }
    elseif ($cache = $this->cache->get('theme_registry:' . $theme_name)) {
      $this->registry[$theme_name] = $cache->data;
      return $this->registry[$theme_name];
    }
    return NULL;
  }

  /**
   * Returns the incomplete, runtime theme registry.
   *
+96 −62
Original line number Diff line number Diff line
@@ -536,59 +536,77 @@ protected function sendPlaceholders(array $placeholders, array $placeholder_orde
    $fake_request = $this->requestStack->getMainRequest()->duplicate();
    $fake_request->headers->set('Accept', 'application/vnd.drupal-ajax');

    // Create a Fiber for each placeholder.
    $fibers = [];
    $message_placeholder_id = NULL;
    foreach ($placeholder_order as $placeholder_id) {
      if (!isset($placeholders[$placeholder_id])) {
        continue;
      }

      // Render the placeholder.
      $placeholder_render_array = $placeholders[$placeholder_id];
      try {
        $elements = $this->renderPlaceholder($placeholder_id, $placeholder_render_array);

      // Ensure the messages placeholder renders last, the render order of every
      // other placeholder is safe to change.
      // @see static::getPlaceholderOrder()
      if (isset($placeholder_render_array['#lazy_builder']) && $placeholder_render_array['#lazy_builder'][0] === 'Drupal\Core\Render\Element\StatusMessages::renderMessages') {
        $message_placeholder_id = $placeholder_id;
      }
      catch (\Exception $e) {
        if ($this->configFactory->get('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
          throw $e;
      $fibers[$placeholder_id] = new \Fiber(fn() => $this->renderPlaceholder($placeholder_id, $placeholder_render_array));
    }
        else {
          trigger_error($e, E_USER_ERROR);
    while (count($fibers) > 0) {
      $iterations = 0;
      foreach ($fibers as $placeholder_id => $fiber) {
        // Keep skipping the messages placeholder until it's the only Fiber
        // remaining. @todo https://www.drupal.org/project/drupal/issues/3379885
        if (isset($message_placeholder_id) && $placeholder_id === $message_placeholder_id && count($fibers) > 1) {
          continue;
        }
        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]);
          // 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).
          // 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']);

      // 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:
          // 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.
          //   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']);
      try {
          $ajax_response = $this->filterEmbeddedResponse($fake_request, $ajax_response);
      }
      catch (\Exception $e) {
        if ($this->configFactory->get('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) {
          throw $e;
        }
        else {
          trigger_error($e, E_USER_ERROR);
          continue;
        }
      }

          // Send this embedded AJAX response.
          $json = $ajax_response->getContent();
          $output = <<<EOF
@@ -599,12 +617,24 @@ protected function sendPlaceholders(array $placeholders, array $placeholder_orde
          $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.
          // 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 (\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_ERROR);
          }
        }
      }
      $iterations++;
    }

    // Send the stop signal.
    $this->sendChunk("\n" . static::STOP_SIGNAL . "\n");
@@ -696,7 +726,7 @@ protected function renderPlaceholder($placeholder, array $placeholder_render_arr
  /**
   * Gets the BigPipe placeholder order.
   *
   * Determines the order in which BigPipe placeholders must be replaced.
   * Determines the order in which BigPipe placeholders are executed.
   *
   * @param string $html
   *   HTML markup.
@@ -705,10 +735,14 @@ protected function renderPlaceholder($placeholder, array $placeholder_render_arr
   *   placeholder IDs.
   *
   * @return array
   *   Indexed array; the order in which the BigPipe placeholders must be sent.
   *   Values are the BigPipe placeholder IDs. Note that only unique
   *   placeholders are kept: if the same placeholder occurs multiple times, we
   *   only keep the first occurrence.
   *   Indexed array; the order in which the BigPipe placeholders will start
   *   execution. Placeholders begin execution in DOM order, except for the
   *   messages placeholder which must always be executed last. Note that due to
   *   the Fibers implementation of BigPipe, although placeholders will start
   *   executing in DOM order, they may finish and render in any order. Values
   *   are the BigPipe placeholder IDs. Note that only unique placeholders are
   *   kept: if the same placeholder occurs multiple times, we only keep the
   *   first occurrence.
   */
  protected function getPlaceholderOrder($html, $placeholders) {
    $placeholder_ids = [];
+61 −3
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@
 * @file
 */
// cSpell:ignore Vxezb
// cSpell:ignore divpiggydiv

namespace Drupal\big_pipe_test;

@@ -188,7 +189,63 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
    ];
    $hello->embeddedHtmlResponse = '<marquee>Yarhar llamas forever!</marquee>';

    // 5. Edge case: non-#lazy_builder placeholder.
    // 5. Edge case: non-#lazy_builder placeholder that calls Fiber::suspend().
    $piggy = new BigPipePlaceholderTestCase(
      [
        '#markup' => BigPipeMarkup::create('<div>piggy</div>'),
        '#attached' => [
          'placeholders' => [
            '<div>piggy</div>' => [
              '#pre_render' => [
                '\Drupal\big_pipe_test\BigPipeTestController::piggy',
              ],
            ],
          ],
        ],
      ],
      '<div>piggy</div>',
      []
    );
    $piggy->bigPipePlaceholderId = 'divpiggydiv';
    $piggy->bigPipePlaceholderRenderArray = [
      '#prefix' => '<span data-big-pipe-placeholder-id="divpiggydiv">',
      'interface_preview' => [],
      '#suffix' => '</span>',
      '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
      '#attached' => [
        'library' => ['big_pipe/big_pipe'],
        'drupalSettings' => [
          'bigPipePlaceholderIds' => [
            'divpiggydiv' => TRUE,
          ],
        ],
        'big_pipe_placeholders' => [
          'divpiggydiv' => $piggy->placeholderRenderArray,
        ],
      ],
    ];
    $piggy->embeddedAjaxResponseCommands = [
      [
        'command' => 'insert',
        'method' => 'replaceWith',
        'selector' => '[data-big-pipe-placeholder-id="divpiggydiv"]',
        'data' => '<span>This 🐷 little 🐽 piggy 🐖 stayed 🐽 at 🐷 home.</span>',
        'settings' => NULL,
      ],
    ];
    $piggy->bigPipeNoJsPlaceholder = '<span data-big-pipe-nojs-placeholder-id="divpiggydiv></span>';
    $piggy->bigPipeNoJsPlaceholderRenderArray = [
      '#markup' => '<span data-big-pipe-nojs-placeholder-id="divpiggydiv"></span>',
      '#cache' => $cacheability_depends_on_session_and_nojs_cookie,
      '#attached' => [
        'big_pipe_nojs_placeholders' => [
          '<span data-big-pipe-nojs-placeholder-id="divpiggydiv"></span>' => $piggy->placeholderRenderArray,
        ],
      ],
    ];
    $piggy->embeddedHtmlResponse = '<span>This 🐷 little 🐽 piggy 🐖 stayed 🐽 at 🐷 home.</span>';

    // 6. Edge case: non-#lazy_builder placeholder.
    $current_time = new BigPipePlaceholderTestCase(
      [
        '#markup' => BigPipeMarkup::create('<time>CURRENT TIME</time>'),
@@ -249,7 +306,7 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
    ];
    $current_time->embeddedHtmlResponse = '<time datetime="1991-03-14"></time>';

    // 6. Edge case: #lazy_builder that throws an exception.
    // 7. Edge case: #lazy_builder that throws an exception.
    $exception = new BigPipePlaceholderTestCase(
      [
        '#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::exception', ['llamas', 'suck']],
@@ -297,7 +354,7 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf

    // cSpell:disable-next-line.
    $token = 'PxOHfS_QL-T01NjBgu7Z7I04tIwMp6La5vM-mVxezbU';
    // 7. Edge case: response filter throwing an exception for this placeholder.
    // 8. Edge case: response filter throwing an exception for this placeholder.
    $embedded_response_exception = new BigPipePlaceholderTestCase(
      [
        '#lazy_builder' => ['\Drupal\big_pipe_test\BigPipeTestController::responseException', []],
@@ -348,6 +405,7 @@ public static function cases(ContainerInterface $container = NULL, AccountInterf
      'html_attribute_value' => $form_action,
      'html_attribute_value_subset' => $csrf_token,
      'edge_case__invalid_html' => $hello,
      'edge_case__html_non_lazy_builder_suspend' => $piggy,
      'edge_case__html_non_lazy_builder' => $current_time,
      'exception__lazy_builder' => $exception,
      'exception__embedded_response' => $embedded_response_exception,
+25 −4
Original line number Diff line number Diff line
@@ -39,13 +39,16 @@ public function test() {
    // happens to not be valid HTML.
    $build['edge_case__invalid_html'] = $cases['edge_case__invalid_html']->renderArray;

    // 5. Edge case: non-#lazy_builder placeholder.
    // 5. Edge case: non-#lazy_builder placeholder that suspends.
    $build['edge_case__html_non_lazy_builder_suspend'] = $cases['edge_case__html_non_lazy_builder_suspend']->renderArray;

    // 6. Edge case: non-#lazy_builder placeholder.
    $build['edge_case__html_non_lazy_builder'] = $cases['edge_case__html_non_lazy_builder']->renderArray;

    // 6. Exception: #lazy_builder that throws an exception.
    // 7. Exception: #lazy_builder that throws an exception.
    $build['exception__lazy_builder'] = $cases['exception__lazy_builder']->renderArray;

    // 7. Exception: placeholder that causes response filter to throw exception.
    // 8. Exception: placeholder that causes response filter to throw exception.
    $build['exception__embedded_response'] = $cases['exception__embedded_response']->renderArray;

    return $build;
@@ -127,6 +130,24 @@ public static function currentTime() {
    ];
  }

  /**
   * #lazy_builder callback; suspends its own execution then returns markup.
   *
   * @return array
   */
  public static function piggy(): array {
    // Immediately call Fiber::suspend(), so that other placeholders are
    // executed next. When this is resumed, it will immediately return the
    // render array.
    if (\Fiber::getCurrent() !== NULL) {
      \Fiber::suspend();
    }
    return [
      '#markup' => '<span>This 🐷 little 🐽 piggy 🐖 stayed 🐽 at 🐷 home.</span>',
      '#cache' => ['max-age' => 0],
    ];
  }

  /**
   * #lazy_builder callback; says "hello" or "yarhar".
   *
@@ -193,7 +214,7 @@ public static function counter() {
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return ['currentTime', 'helloOrYarhar', 'exception', 'responseException', 'counter'];
    return ['currentTime', 'piggy', 'helloOrYarhar', 'exception', 'responseException', 'counter'];
  }

}
+7 −2
Original line number Diff line number Diff line
@@ -170,14 +170,19 @@ public function testBigPipe() {
    ]);
    $this->assertBigPipePlaceholders([
      $cases['html']->bigPipePlaceholderId                             => Json::encode($cases['html']->embeddedAjaxResponseCommands),
      $cases['edge_case__html_non_lazy_builder_suspend']->bigPipePlaceholderId => Json::encode($cases['edge_case__html_non_lazy_builder_suspend']->embeddedAjaxResponseCommands),
      $cases['edge_case__html_non_lazy_builder']->bigPipePlaceholderId => Json::encode($cases['edge_case__html_non_lazy_builder']->embeddedAjaxResponseCommands),
      $cases['exception__lazy_builder']->bigPipePlaceholderId          => NULL,
      $cases['exception__embedded_response']->bigPipePlaceholderId     => NULL,
    ], [
      0 => $cases['edge_case__html_non_lazy_builder']->bigPipePlaceholderId,
      // The suspended placeholder is replaced after the non-suspended
      // placeholder even though it appears first in the page.
      // @see Drupal\big_pipe\Render\BigPipe\Render::sendPlaceholders()
      1 => $cases['edge_case__html_non_lazy_builder_suspend']->bigPipePlaceholderId,
       // The 'html' case contains the 'status messages' placeholder, which is
      // always rendered last.
      1 => $cases['html']->bigPipePlaceholderId,
      2 => $cases['html']->bigPipePlaceholderId,
    ]);

    $this->assertSession()->responseContains('</body>');
Loading