core/core.services.yml | 4 +- ... EarlyRenderingControllerWrapperSubscriber.php} | 95 ++++++++++++++-------- core/lib/Drupal/Core/Render/RenderContext.php | 4 +- core/lib/Drupal/Core/Render/RendererInterface.php | 2 +- 4 files changed, 66 insertions(+), 39 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index 5fdfe30..a0122c5 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -1392,8 +1392,8 @@ services: renderer: class: Drupal\Core\Render\Renderer arguments: ['@controller_resolver', '@theme.manager', '@plugin.manager.element_info', '@render_cache', '%renderer.config%'] - early_rendering_controller_wrapper: - class: Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapper + early_rendering_controller_wrapper_subscriber: + class: Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber arguments: ['@controller_resolver', '@renderer'] tags: - { name: event_subscriber } diff --git a/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapper.php b/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php similarity index 57% rename from core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapper.php rename to core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php index b6f65b5..ad2737e 100644 --- a/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapper.php +++ b/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php @@ -2,7 +2,7 @@ /** * @file - * Contains \Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapper. + * Contains \Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber. */ namespace Drupal\Core\EventSubscriber; @@ -40,10 +40,10 @@ * ::renderPlain() methods. In that case, no bubbleable metadata is lost. * * If the render context is not empty, then the controller did use - * drupal_render(), and bubbleable metadata is lost. But, rather then letting it - * go lost, the closure will update the render array returned by the controller - * by merging the "lost" bubbleable metadata onto the render array. Disaster - * averted. + * drupal_render(), and bubbleable metadata was collected. Without this + * subscriber, that bubbleable metadata would have been lost. But since it is + * collected by this subscriber, this bubbleable metadata can be merged onto the + * render array. Disaster averted. * * In other words: this just exists to ease the transition to Drupal 8: it * allows controllers that return render arrays (the majority) to still do early @@ -56,7 +56,7 @@ * * @todo Remove in Drupal 9.0.0, by disallowing early rendering. */ -class EarlyRenderingControllerWrapper implements EventSubscriberInterface { +class EarlyRenderingControllerWrapperSubscriber implements EventSubscriberInterface { /** * The controller resolver. @@ -73,7 +73,7 @@ class EarlyRenderingControllerWrapper implements EventSubscriberInterface { protected $renderer; /** - * Constructs a new EarlyRenderingControllerWrapper instance. + * Constructs a new EarlyRenderingControllerWrapperSubscriber instance. * * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver * The controller resolver. @@ -98,36 +98,61 @@ public function onController(FilterControllerEvent $event) { $arguments = $this->controllerResolver->getArguments($event->getRequest(), $controller); $event->setController(function() use ($controller, $arguments) { - $context = new RenderContext(); - - $response = $this->renderer->executeInRenderContext($context, function() use ($controller, $arguments) { - // Now call the actual controller, just like HttpKernel does. - return call_user_func_array($controller, $arguments); - }); - - // If early rendering happened, i.e. if code in the controller called - // drupal_render(), then the bubbleable metadata for that is stored in - // the current render context. - if (!$context->isEmpty()) { - // If a render array is returned by the controller, merge the "lost" - // bubbleable metadata. - if (is_array($response)) { - $early_rendering_bubbleable_metadata = $context->pop(); - BubbleableMetadata::createFromRenderArray($response) - ->merge($early_rendering_bubbleable_metadata) - ->applyTo($response); - } - // If a response object is returned, and the response object cares about - // attachments or cacheability, then throw an exception: early rendering - // is not permitted in that case. It is the developer's responsibility - // to not use early rendering. - elseif ($response instanceof AttachmentsInterface || $response instanceof CacheableDependencyInterface) { - throw new \LogicException(sprintf('Early rendering is not permitted for controllers returning anything else than render arrays. Response class: %s.', get_class($response))); - } - } + return $this->wrapControllerExcecutionInRenderContext($controller, $arguments); + }); + } - return $response; + /** + * Wraps a controller execution in a render context. + * + * @param callable $controller + * The controller to execute. + * @param array $arguments + * The arguments to pass to the controller. + * + * @return mixed + * The return value of the controller. + * + * @throws \LogicException + * When early rendering occurs + * + * @see \Symfony\Component\HttpKernel\HttpKernel::handleRaw() + */ + protected function wrapControllerExcecutionInRenderContext($controller, array $arguments) { + $context = new RenderContext(); + + $response = $this->renderer->executeInRenderContext($context, function() use ($controller, $arguments) { + // Now call the actual controller, just like HttpKernel does. + return call_user_func_array($controller, $arguments); }); + + // If early rendering happened, i.e. if code in the controller called + // drupal_render(), then the bubbleable metadata for that is stored in + // the current render context. + if (!$context->isEmpty()) { + // If a render array is returned by the controller, merge the "lost" + // bubbleable metadata. + if (is_array($response)) { + $early_rendering_bubbleable_metadata = $context->pop(); + BubbleableMetadata::createFromRenderArray($response) + ->merge($early_rendering_bubbleable_metadata) + ->applyTo($response); + } + // If a response object is returned, and the response object cares about + // attachments or cacheability, then throw an exception: early rendering + // is not permitted in that case. It is the developer's responsibility + // to not use early rendering. + elseif ($response instanceof AttachmentsInterface || $response instanceof CacheableDependencyInterface) { + throw new \LogicException(sprintf('Early rendering is not permitted for controllers returning anything else than render arrays. Response class: %s.', get_class($response))); + } + else { + // A response object is returned that does not care about attachments + // nor cacheability. E.g. a RedirectResponse. It is safe to discard any + // early rendering metadata. + } + } + + return $response; } /** diff --git a/core/lib/Drupal/Core/Render/RenderContext.php b/core/lib/Drupal/Core/Render/RenderContext.php index e255005..e49c291 100644 --- a/core/lib/Drupal/Core/Render/RenderContext.php +++ b/core/lib/Drupal/Core/Render/RenderContext.php @@ -9,6 +9,8 @@ /** * The render context: a stack containing bubbleable rendering metadata. * + * A stack of \Drupal\Core\Render\BubbleableMetadata objects. + * * @see \Drupal\Core\Render\RendererInterface * @see \Drupal\Core\Render\Renderer * @see \Drupal\Core\Render\BubbleableMetadata @@ -18,7 +20,7 @@ class RenderContext extends \SplStack { /** - * Updates the stack. + * Updates the current frame of the stack. * * @param array &$element * The element of the render array that has just been rendered. The stack diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php index 1d40f35..394c355 100644 --- a/core/lib/Drupal/Core/Render/RendererInterface.php +++ b/core/lib/Drupal/Core/Render/RendererInterface.php @@ -324,7 +324,7 @@ public function render(&$elements, $is_root_call = FALSE); /** * Executes a callable within a render context. * - * Only for very advanced use cases. Prefer to use ::renderRoot() and + * Only for very advanced use cases. Prefer using ::renderRoot() and * ::renderPlain() instead. * * All rendering must happen within a render context: within a render context,