/** * Returns the libraries that need to be loaded. * * For example, with core/a depending on core/c and core/b on core/d: * @code * $assets = new AttachedAssets(); * $assets->setLibraries(['core/a', 'core/b', 'core/c']); * $assets->setAlreadyLoadedLibraries(['core/c']); * $resolver->getLibrariesToLoad($assets) === ['core/a', 'core/b', 'core/d'] * @endcode * * @param \Drupal\Core\Asset\AttachedAssetsInterface $assets * The assets attached to the current response. * * @return string[] * A list of libraries and their dependencies, in the order they should be * loaded, excluding any libraries that have already been loaded. */ protected function getLibrariesToLoad(AttachedAssetsInterface $assets) { return array_diff($this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getLibraries()), $this->libraryDependencyResolver->getLibrariesWithDependencies($assets->getAlreadyLoadedLibraries())); }
/** * Sends BigPipe placeholders' replacements as embedded AJAX responses. * * @param array $placeholders * Associative array; the BigPipe placeholders. Keys are the BigPipe * placeholder IDs. * @param 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.) * @param \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. */ 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. print "\n"; print static::START_SIGNAL; print "\n"; flush(); // A BigPipe response consists of a 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 master 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->getMasterRequest()->duplicate(); $fake_request->headers->set('Accept', 'application/vnd.drupal-ajax'); 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); } catch (\Exception $e) { if (\Drupal::config('system.logging')->get('error_level') === ERROR_REPORTING_DISPLAY_VERBOSE) { throw $e; } else { trigger_error($e, E_USER_ERROR); continue; } } // 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']); // 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->request->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 (\Drupal::config('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 <script type="application/vnd.drupal-ajax" data-big-pipe-replacement-for-placeholder-with-id="{$placeholder_id}"> {$json} </script> EOF; print $output; flush(); // 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'])); } } // Send the stop signal. print "\n"; print static::STOP_SIGNAL; print "\n"; flush(); }
/** * Sends BigPipe placeholders' replacements as embedded AJAX responses. * * @param array $placeholders * Associative array; the BigPipe placeholders. Keys are the BigPipe * selectors. * @param array $placeholder_order * Indexed array; the order in which the BigPipe placeholders must be sent. * Values are the BigPipe selectors. (These values correspond to keys in * $placeholders.) * @param \Drupal\Core\Asset\AttachedAssetsInterface $cumulative_assets * The cumulative assets sent so far; to be updated while rendering BigPipe * placeholders. */ 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 a container and the start signal. print "\n"; print '<script type="application/json" data-big-pipe-event="start"></script>' . "\n"; flush(); // A BigPipe response consists of a 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 master 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->getMasterRequest()->duplicate(); $fake_request->headers->set('Accept', 'application/json'); foreach ($placeholder_order as $placeholder) { if (!isset($placeholders[$placeholder])) { continue; } // Render the placeholder. $placeholder_render_array = $placeholders[$placeholder]; $elements = $this->renderPlaceholder($placeholder, $placeholder_render_array); // 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 (which has HTML entities encoded since we use it to find // the placeholders). $big_pipe_js_selector = Html::decodeEntities($placeholder); $ajax_response->addCommand(new ReplaceCommand(sprintf('[data-big-pipe-selector="%s"]', $big_pipe_js_selector), $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: // - 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->request->set('ajax_page_state', ['libraries' => implode(',', $cumulative_assets->getAlreadyLoadedLibraries())] + $cumulative_assets->getSettings()['ajaxPageState']); $this->requestStack->push($fake_request); $event = new FilterResponseEvent($this->httpKernel, $fake_request, HttpKernelInterface::SUB_REQUEST, $ajax_response); $this->eventDispatcher->dispatch(KernelEvents::RESPONSE, $event); $ajax_response = $event->getResponse(); $this->requestStack->pop(); // Send this embedded AJAX response. $json = $ajax_response->getContent(); $output = <<<EOF <script type="application/json" data-big-pipe-placeholder="{$placeholder}" data-drupal-ajax-processor="big_pipe"> {$json} </script> EOF; print $output; flush(); // 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'])); } } // Send the stop signal. print '<script type="application/json" data-big-pipe-event="stop"></script>' . "\n"; flush(); }