annotate core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents 129ea1e6d783
children
rev   line source
Chris@0 1 <?php
Chris@0 2
Chris@0 3 namespace Drupal\Core\EventSubscriber;
Chris@0 4
Chris@0 5 use Drupal\Core\Ajax\AjaxResponse;
Chris@0 6 use Drupal\Core\Cache\CacheableDependencyInterface;
Chris@0 7 use Drupal\Core\Cache\CacheableResponseInterface;
Chris@0 8 use Drupal\Core\Render\AttachmentsInterface;
Chris@0 9 use Drupal\Core\Render\BubbleableMetadata;
Chris@0 10 use Drupal\Core\Render\RenderContext;
Chris@0 11 use Drupal\Core\Render\RendererInterface;
Chris@0 12 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
Chris@17 13 use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface;
Chris@0 14 use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
Chris@0 15 use Symfony\Component\HttpKernel\KernelEvents;
Chris@0 16
Chris@0 17 /**
Chris@0 18 * Subscriber that wraps controllers, to handle early rendering.
Chris@0 19 *
Chris@0 20 * When controllers call drupal_render() (RendererInterface::render()) outside
Chris@0 21 * of a render context, we call that "early rendering". Controllers should
Chris@0 22 * return only render arrays, but we cannot prevent controllers from doing early
Chris@0 23 * rendering. The problem with early rendering is that the bubbleable metadata
Chris@0 24 * (cacheability & attachments) are lost.
Chris@0 25 *
Chris@0 26 * This can lead to broken pages (missing assets), stale pages (missing cache
Chris@0 27 * tags causing a page not to be invalidated) or even security problems (missing
Chris@0 28 * cache contexts causing a cached page not to be varied sufficiently).
Chris@0 29 *
Chris@0 30 * This event subscriber wraps all controller executions in a closure that sets
Chris@0 31 * up a render context. Consequently, any early rendering will have their
Chris@0 32 * bubbleable metadata (assets & cacheability) stored on that render context.
Chris@0 33 *
Chris@0 34 * If the render context is empty, then the controller either did not do any
Chris@0 35 * rendering at all, or used the RendererInterface::renderRoot() or
Chris@0 36 * ::renderPlain() methods. In that case, no bubbleable metadata is lost.
Chris@0 37 *
Chris@0 38 * If the render context is not empty, then the controller did use
Chris@0 39 * drupal_render(), and bubbleable metadata was collected. This bubbleable
Chris@0 40 * metadata is then merged onto the render array.
Chris@0 41 *
Chris@0 42 * In other words: this just exists to ease the transition to Drupal 8: it
Chris@0 43 * allows controllers that return render arrays (the majority) and
Chris@0 44 * \Drupal\Core\Ajax\AjaxResponse\AjaxResponse objects (a sizable minority that
Chris@0 45 * often involve a fair amount of rendering) to still do early rendering. But
Chris@0 46 * controllers that return any other kind of response are already expected to
Chris@0 47 * do the right thing, so if early rendering is detected in such a case, an
Chris@0 48 * exception is thrown.
Chris@0 49 *
Chris@0 50 * @see \Drupal\Core\Render\RendererInterface
Chris@0 51 * @see \Drupal\Core\Render\Renderer
Chris@0 52 *
Chris@0 53 * @todo Remove in Drupal 9.0.0, by disallowing early rendering.
Chris@0 54 */
Chris@0 55 class EarlyRenderingControllerWrapperSubscriber implements EventSubscriberInterface {
Chris@0 56
Chris@0 57 /**
Chris@17 58 * The argument resolver.
Chris@0 59 *
Chris@17 60 * @var \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface
Chris@0 61 */
Chris@17 62 protected $argumentResolver;
Chris@0 63
Chris@0 64 /**
Chris@0 65 * The renderer.
Chris@0 66 *
Chris@0 67 * @var \Drupal\Core\Render\RendererInterface
Chris@0 68 */
Chris@0 69 protected $renderer;
Chris@0 70
Chris@0 71 /**
Chris@0 72 * Constructs a new EarlyRenderingControllerWrapperSubscriber instance.
Chris@0 73 *
Chris@17 74 * @param \Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface $argument_resolver
Chris@17 75 * The argument resolver.
Chris@0 76 * @param \Drupal\Core\Render\RendererInterface $renderer
Chris@0 77 * The renderer.
Chris@0 78 */
Chris@17 79 public function __construct(ArgumentResolverInterface $argument_resolver, RendererInterface $renderer) {
Chris@17 80 $this->argumentResolver = $argument_resolver;
Chris@0 81 $this->renderer = $renderer;
Chris@0 82 }
Chris@0 83
Chris@0 84 /**
Chris@0 85 * Ensures bubbleable metadata from early rendering is not lost.
Chris@0 86 *
Chris@0 87 * @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
Chris@0 88 * The controller event.
Chris@0 89 */
Chris@0 90 public function onController(FilterControllerEvent $event) {
Chris@0 91 $controller = $event->getController();
Chris@0 92
Chris@0 93 // See \Symfony\Component\HttpKernel\HttpKernel::handleRaw().
Chris@17 94 $arguments = $this->argumentResolver->getArguments($event->getRequest(), $controller);
Chris@0 95
Chris@0 96 $event->setController(function () use ($controller, $arguments) {
Chris@0 97 return $this->wrapControllerExecutionInRenderContext($controller, $arguments);
Chris@0 98 });
Chris@0 99 }
Chris@0 100
Chris@0 101 /**
Chris@0 102 * Wraps a controller execution in a render context.
Chris@0 103 *
Chris@0 104 * @param callable $controller
Chris@0 105 * The controller to execute.
Chris@0 106 * @param array $arguments
Chris@0 107 * The arguments to pass to the controller.
Chris@0 108 *
Chris@0 109 * @return mixed
Chris@0 110 * The return value of the controller.
Chris@0 111 *
Chris@0 112 * @throws \LogicException
Chris@0 113 * When early rendering has occurred in a controller that returned a
Chris@0 114 * Response or domain object that cares about attachments or cacheability.
Chris@0 115 *
Chris@0 116 * @see \Symfony\Component\HttpKernel\HttpKernel::handleRaw()
Chris@0 117 */
Chris@0 118 protected function wrapControllerExecutionInRenderContext($controller, array $arguments) {
Chris@0 119 $context = new RenderContext();
Chris@0 120
Chris@0 121 $response = $this->renderer->executeInRenderContext($context, function () use ($controller, $arguments) {
Chris@0 122 // Now call the actual controller, just like HttpKernel does.
Chris@0 123 return call_user_func_array($controller, $arguments);
Chris@0 124 });
Chris@0 125
Chris@0 126 // If early rendering happened, i.e. if code in the controller called
Chris@0 127 // drupal_render() outside of a render context, then the bubbleable metadata
Chris@0 128 // for that is stored in the current render context.
Chris@0 129 if (!$context->isEmpty()) {
Chris@0 130 /** @var \Drupal\Core\Render\BubbleableMetadata $early_rendering_bubbleable_metadata */
Chris@0 131 $early_rendering_bubbleable_metadata = $context->pop();
Chris@0 132
Chris@0 133 // If a render array or AjaxResponse is returned by the controller, merge
Chris@0 134 // the "lost" bubbleable metadata.
Chris@0 135 if (is_array($response)) {
Chris@0 136 BubbleableMetadata::createFromRenderArray($response)
Chris@0 137 ->merge($early_rendering_bubbleable_metadata)
Chris@0 138 ->applyTo($response);
Chris@0 139 }
Chris@0 140 elseif ($response instanceof AjaxResponse) {
Chris@0 141 $response->addAttachments($early_rendering_bubbleable_metadata->getAttachments());
Chris@0 142 // @todo Make AjaxResponse cacheable in
Chris@0 143 // https://www.drupal.org/node/956186. Meanwhile, allow contrib
Chris@0 144 // subclasses to be.
Chris@0 145 if ($response instanceof CacheableResponseInterface) {
Chris@0 146 $response->addCacheableDependency($early_rendering_bubbleable_metadata);
Chris@0 147 }
Chris@0 148 }
Chris@0 149 // If a non-Ajax Response or domain object is returned and it cares about
Chris@0 150 // attachments or cacheability, then throw an exception: early rendering
Chris@0 151 // is not permitted in that case. It is the developer's responsibility
Chris@0 152 // to not use early rendering.
Chris@0 153 elseif ($response instanceof AttachmentsInterface || $response instanceof CacheableResponseInterface || $response instanceof CacheableDependencyInterface) {
Chris@0 154 throw new \LogicException(sprintf('The controller result claims to be providing relevant cache metadata, but leaked metadata was detected. Please ensure you are not rendering content too early. Returned object class: %s.', get_class($response)));
Chris@0 155 }
Chris@0 156 else {
Chris@0 157 // A Response or domain object is returned that does not care about
Chris@0 158 // attachments nor cacheability; for instance, a RedirectResponse. It is
Chris@0 159 // safe to discard any early rendering metadata.
Chris@0 160 }
Chris@0 161 }
Chris@0 162
Chris@0 163 return $response;
Chris@0 164 }
Chris@0 165
Chris@0 166 /**
Chris@0 167 * {@inheritdoc}
Chris@0 168 */
Chris@0 169 public static function getSubscribedEvents() {
Chris@0 170 $events[KernelEvents::CONTROLLER][] = ['onController'];
Chris@0 171
Chris@0 172 return $events;
Chris@0 173 }
Chris@0 174
Chris@0 175 }